mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-15 05:50:24 +00:00
Compare commits
8 Commits
renovate/n
...
add-automa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6936a9294 | ||
|
|
5ad73287a2 | ||
|
|
591b464508 | ||
|
|
acf963d38c | ||
|
|
68f383c293 | ||
|
|
0a25d8106c | ||
|
|
5c3cf17df9 | ||
|
|
e905fa6f23 |
@@ -260,6 +260,7 @@ const createRspackConfig = ({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
experiments: {
|
experiments: {
|
||||||
|
layers: true,
|
||||||
outputModule: true,
|
outputModule: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import "../../../../src/components/ha-yaml-editor";
|
import "../../../../src/components/ha-yaml-editor";
|
||||||
import type { LegacyTrigger } from "../../../../src/data/automation";
|
import type { Trigger } from "../../../../src/data/automation";
|
||||||
import { describeTrigger } from "../../../../src/data/automation_i18n";
|
import { describeTrigger } from "../../../../src/data/automation_i18n";
|
||||||
import { getEntity } from "../../../../src/fake_data/entity";
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
@@ -66,7 +66,7 @@ const triggers = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const initialTrigger: LegacyTrigger = {
|
const initialTrigger: Trigger = {
|
||||||
trigger: "state",
|
trigger: "state",
|
||||||
entity_id: "light.kitchen",
|
entity_id: "light.kitchen",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -237,6 +237,6 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "yarn@4.10.3",
|
"packageManager": "yarn@4.10.3",
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "24.11.1"
|
"node": "22.21.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* Executes a callback within a View Transition if supported, otherwise runs it directly.
|
|
||||||
*
|
|
||||||
* @param callback - Function to execute. Can be synchronous or return a Promise. The callback will be passed a boolean indicating whether the view transition is available.
|
|
||||||
* @returns Promise that resolves when the transition completes (or immediately if not supported)
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // Synchronous callback
|
|
||||||
* withViewTransition(() => {
|
|
||||||
* this.large = !this.large;
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Async callback
|
|
||||||
* await withViewTransition(async () => {
|
|
||||||
* await this.updateData();
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const withViewTransition = (
|
|
||||||
callback: (viewTransitionAvailable: boolean) => void | Promise<void>
|
|
||||||
): Promise<void> => {
|
|
||||||
if (document.startViewTransition) {
|
|
||||||
return document.startViewTransition(() => callback(true)).finished;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Execute callback directly without transition
|
|
||||||
const result = callback(false);
|
|
||||||
return result instanceof Promise ? result : Promise.resolve();
|
|
||||||
};
|
|
||||||
@@ -154,7 +154,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this._getLabelsMemoized(
|
return this._getLabelsMemoized(
|
||||||
this.hass,
|
this.hass.states,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.entities,
|
||||||
this._labels,
|
this._labels,
|
||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
this.excludeDomains,
|
this.excludeDomains,
|
||||||
|
|||||||
28
src/components/ha-section-title.ts
Normal file
28
src/components/ha-section-title.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
|
||||||
|
@customElement("ha-section-title")
|
||||||
|
class HaSectionTitle extends LitElement {
|
||||||
|
protected render() {
|
||||||
|
return html`<slot></slot>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||||
|
padding: var(--ha-space-1) var(--ha-space-2);
|
||||||
|
font-weight: var(--ha-font-weight-bold);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
min-height: var(--ha-space-6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-section-title": HaSectionTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
|
||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { StateSelector } from "../../data/selector";
|
import type { StateSelector } from "../../data/selector";
|
||||||
import { extractFromTarget } from "../../data/target";
|
|
||||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../entity/ha-entity-state-picker";
|
import "../entity/ha-entity-state-picker";
|
||||||
@@ -27,29 +25,15 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
|||||||
@property({ attribute: false }) public context?: {
|
@property({ attribute: false }) public context?: {
|
||||||
filter_attribute?: string;
|
filter_attribute?: string;
|
||||||
filter_entity?: string | string[];
|
filter_entity?: string | string[];
|
||||||
filter_target?: HassServiceTarget;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@state() private _entityIds?: string | string[];
|
|
||||||
|
|
||||||
willUpdate(changedProps) {
|
|
||||||
if (changedProps.has("selector") || changedProps.has("context")) {
|
|
||||||
this._resolveEntityIds(
|
|
||||||
this.selector.state?.entity_id,
|
|
||||||
this.context?.filter_entity,
|
|
||||||
this.context?.filter_target
|
|
||||||
).then((entityIds) => {
|
|
||||||
this._entityIds = entityIds;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (this.selector.state?.multiple) {
|
if (this.selector.state?.multiple) {
|
||||||
return html`
|
return html`
|
||||||
<ha-entity-states-picker
|
<ha-entity-states-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entityId=${this._entityIds}
|
.entityId=${this.selector.state?.entity_id ||
|
||||||
|
this.context?.filter_entity}
|
||||||
.attribute=${this.selector.state?.attribute ||
|
.attribute=${this.selector.state?.attribute ||
|
||||||
this.context?.filter_attribute}
|
this.context?.filter_attribute}
|
||||||
.extraOptions=${this.selector.state?.extra_options}
|
.extraOptions=${this.selector.state?.extra_options}
|
||||||
@@ -66,7 +50,8 @@ 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._entityIds}
|
.entityId=${this.selector.state?.entity_id ||
|
||||||
|
this.context?.filter_entity}
|
||||||
.attribute=${this.selector.state?.attribute ||
|
.attribute=${this.selector.state?.attribute ||
|
||||||
this.context?.filter_attribute}
|
this.context?.filter_attribute}
|
||||||
.extraOptions=${this.selector.state?.extra_options}
|
.extraOptions=${this.selector.state?.extra_options}
|
||||||
@@ -80,24 +65,6 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
|||||||
></ha-entity-state-picker>
|
></ha-entity-state-picker>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _resolveEntityIds(
|
|
||||||
selectorEntityId: string | string[] | undefined,
|
|
||||||
contextFilterEntity: string | string[] | undefined,
|
|
||||||
contextFilterTarget: HassServiceTarget | undefined
|
|
||||||
): Promise<string | string[] | undefined> {
|
|
||||||
if (selectorEntityId !== undefined) {
|
|
||||||
return selectorEntityId;
|
|
||||||
}
|
|
||||||
if (contextFilterEntity !== undefined) {
|
|
||||||
return contextFilterEntity;
|
|
||||||
}
|
|
||||||
if (contextFilterTarget !== undefined) {
|
|
||||||
const result = await extractFromTarget(this.hass, contextFilterTarget);
|
|
||||||
return result.referenced_entities;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -858,7 +858,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
if (!filterType || filterType === "label") {
|
if (!filterType || filterType === "label") {
|
||||||
let labels = this._getLabelsMemoized(
|
let labels = this._getLabelsMemoized(
|
||||||
this.hass,
|
this.hass.states,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.entities,
|
||||||
this._labelRegistry,
|
this._labelRegistry,
|
||||||
includeDomains,
|
includeDomains,
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
import {
|
|
||||||
mdiAvTimer,
|
|
||||||
mdiCalendar,
|
|
||||||
mdiClockOutline,
|
|
||||||
mdiCodeBraces,
|
|
||||||
mdiDevices,
|
|
||||||
mdiFormatListBulleted,
|
|
||||||
mdiGestureDoubleTap,
|
|
||||||
mdiHomeAssistant,
|
|
||||||
mdiMapMarker,
|
|
||||||
mdiMapMarkerRadius,
|
|
||||||
mdiMessageAlert,
|
|
||||||
mdiMicrophoneMessage,
|
|
||||||
mdiNfcVariant,
|
|
||||||
mdiNumeric,
|
|
||||||
mdiStateMachine,
|
|
||||||
mdiSwapHorizontal,
|
|
||||||
mdiWeatherSunny,
|
|
||||||
mdiWebhook,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import { html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { until } from "lit/directives/until";
|
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
|
||||||
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./ha-icon";
|
|
||||||
import "./ha-svg-icon";
|
|
||||||
|
|
||||||
export const TRIGGER_ICONS = {
|
|
||||||
calendar: mdiCalendar,
|
|
||||||
device: mdiDevices,
|
|
||||||
event: mdiGestureDoubleTap,
|
|
||||||
state: mdiStateMachine,
|
|
||||||
geo_location: mdiMapMarker,
|
|
||||||
homeassistant: mdiHomeAssistant,
|
|
||||||
mqtt: mdiSwapHorizontal,
|
|
||||||
numeric_state: mdiNumeric,
|
|
||||||
sun: mdiWeatherSunny,
|
|
||||||
conversation: mdiMicrophoneMessage,
|
|
||||||
tag: mdiNfcVariant,
|
|
||||||
template: mdiCodeBraces,
|
|
||||||
time: mdiClockOutline,
|
|
||||||
time_pattern: mdiAvTimer,
|
|
||||||
webhook: mdiWebhook,
|
|
||||||
persistent_notification: mdiMessageAlert,
|
|
||||||
zone: mdiMapMarkerRadius,
|
|
||||||
list: mdiFormatListBulleted,
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("ha-trigger-icon")
|
|
||||||
export class HaTriggerIcon extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public trigger?: string;
|
|
||||||
|
|
||||||
@property() public icon?: string;
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (this.icon) {
|
|
||||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.trigger) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.hass) {
|
|
||||||
return this._renderFallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
const icon = triggerIcon(this.hass, this.trigger).then((icn) => {
|
|
||||||
if (icn) {
|
|
||||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
|
||||||
}
|
|
||||||
return this._renderFallback();
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`${until(icon)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderFallback() {
|
|
||||||
const domain = computeDomain(this.trigger!);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${TRIGGER_ICONS[this.trigger!] || FALLBACK_DOMAIN_ICONS[domain]}
|
|
||||||
></ha-svg-icon>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-trigger-icon": HaTriggerIcon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -50,7 +50,7 @@ export const ACTION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
|||||||
{
|
{
|
||||||
groups: {
|
groups: {
|
||||||
device_id: {},
|
device_id: {},
|
||||||
dynamicGroups: {},
|
serviceGroups: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -117,6 +117,14 @@ export const VIRTUAL_ACTIONS: Partial<
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const SERVICE_PREFIX = "__SERVICE__";
|
||||||
|
|
||||||
|
export const isService = (key: string | undefined): boolean | undefined =>
|
||||||
|
key?.startsWith(SERVICE_PREFIX);
|
||||||
|
|
||||||
|
export const getService = (key: string): string =>
|
||||||
|
key.substring(SERVICE_PREFIX.length);
|
||||||
|
|
||||||
export const COLLAPSIBLE_ACTION_ELEMENTS = [
|
export const COLLAPSIBLE_ACTION_ELEMENTS = [
|
||||||
"ha-automation-action-choose",
|
"ha-automation-action-choose",
|
||||||
"ha-automation-action-condition",
|
"ha-automation-action-condition",
|
||||||
|
|||||||
@@ -24,11 +24,54 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
|
|||||||
area?: AreaRegistryEntry;
|
area?: AreaRegistryEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
|
||||||
|
floor?: FloorRegistryEntry;
|
||||||
|
areas: FloorComboBoxItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
|
||||||
|
areas: FloorComboBoxItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AreaFloorValue {
|
export interface AreaFloorValue {
|
||||||
id: string;
|
id: string;
|
||||||
type: "floor" | "area";
|
type: "floor" | "area";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAreasNestedInFloors = (
|
||||||
|
states: HomeAssistant["states"],
|
||||||
|
haFloors: HomeAssistant["floors"],
|
||||||
|
haAreas: HomeAssistant["areas"],
|
||||||
|
haDevices: HomeAssistant["devices"],
|
||||||
|
haEntities: HomeAssistant["entities"],
|
||||||
|
formatId: (value: AreaFloorValue) => string,
|
||||||
|
includeDomains?: string[],
|
||||||
|
excludeDomains?: string[],
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
|
excludeAreas?: string[],
|
||||||
|
excludeFloors?: string[],
|
||||||
|
includeEmptyFloors = false
|
||||||
|
) =>
|
||||||
|
getAreasAndFloorsItems(
|
||||||
|
states,
|
||||||
|
haFloors,
|
||||||
|
haAreas,
|
||||||
|
haDevices,
|
||||||
|
haEntities,
|
||||||
|
formatId,
|
||||||
|
includeDomains,
|
||||||
|
excludeDomains,
|
||||||
|
includeDeviceClasses,
|
||||||
|
deviceFilter,
|
||||||
|
entityFilter,
|
||||||
|
excludeAreas,
|
||||||
|
excludeFloors,
|
||||||
|
includeEmptyFloors,
|
||||||
|
true
|
||||||
|
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
|
||||||
|
|
||||||
export const getAreasAndFloors = (
|
export const getAreasAndFloors = (
|
||||||
states: HomeAssistant["states"],
|
states: HomeAssistant["states"],
|
||||||
haFloors: HomeAssistant["floors"],
|
haFloors: HomeAssistant["floors"],
|
||||||
@@ -42,8 +85,47 @@ export const getAreasAndFloors = (
|
|||||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
excludeAreas?: string[],
|
excludeAreas?: string[],
|
||||||
excludeFloors?: string[]
|
excludeFloors?: string[],
|
||||||
): FloorComboBoxItem[] => {
|
includeEmptyFloors = false
|
||||||
|
) =>
|
||||||
|
getAreasAndFloorsItems(
|
||||||
|
states,
|
||||||
|
haFloors,
|
||||||
|
haAreas,
|
||||||
|
haDevices,
|
||||||
|
haEntities,
|
||||||
|
formatId,
|
||||||
|
includeDomains,
|
||||||
|
excludeDomains,
|
||||||
|
includeDeviceClasses,
|
||||||
|
deviceFilter,
|
||||||
|
entityFilter,
|
||||||
|
excludeAreas,
|
||||||
|
excludeFloors,
|
||||||
|
includeEmptyFloors
|
||||||
|
) as FloorComboBoxItem[];
|
||||||
|
|
||||||
|
const getAreasAndFloorsItems = (
|
||||||
|
states: HomeAssistant["states"],
|
||||||
|
haFloors: HomeAssistant["floors"],
|
||||||
|
haAreas: HomeAssistant["areas"],
|
||||||
|
haDevices: HomeAssistant["devices"],
|
||||||
|
haEntities: HomeAssistant["entities"],
|
||||||
|
formatId: (value: AreaFloorValue) => string,
|
||||||
|
includeDomains?: string[],
|
||||||
|
excludeDomains?: string[],
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
|
excludeAreas?: string[],
|
||||||
|
excludeFloors?: string[],
|
||||||
|
includeEmptyFloors = false,
|
||||||
|
nested = false
|
||||||
|
): (
|
||||||
|
| FloorComboBoxItem
|
||||||
|
| FloorNestedComboBoxItem
|
||||||
|
| UnassignedAreasFloorComboBoxItem
|
||||||
|
)[] => {
|
||||||
const floors = Object.values(haFloors);
|
const floors = Object.values(haFloors);
|
||||||
const areas = Object.values(haAreas);
|
const areas = Object.values(haAreas);
|
||||||
const devices = Object.values(haDevices);
|
const devices = Object.values(haDevices);
|
||||||
@@ -189,6 +271,14 @@ export const getAreasAndFloors = (
|
|||||||
|
|
||||||
const compare = floorCompare(haFloors);
|
const compare = floorCompare(haFloors);
|
||||||
|
|
||||||
|
if (includeEmptyFloors) {
|
||||||
|
Object.values(haFloors).forEach((floor) => {
|
||||||
|
if (!floorAreaLookup[floor.floor_id]) {
|
||||||
|
floorAreaLookup[floor.floor_id] = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const floorAreaEntries: [
|
const floorAreaEntries: [
|
||||||
FloorRegistryEntry | undefined,
|
FloorRegistryEntry | undefined,
|
||||||
@@ -200,9 +290,15 @@ export const getAreasAndFloors = (
|
|||||||
})
|
})
|
||||||
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
|
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
|
||||||
|
|
||||||
const items: FloorComboBoxItem[] = [];
|
const items: (
|
||||||
|
| FloorComboBoxItem
|
||||||
|
| FloorNestedComboBoxItem
|
||||||
|
| UnassignedAreasFloorComboBoxItem
|
||||||
|
)[] = [];
|
||||||
|
|
||||||
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||||
|
let floorItem: FloorComboBoxItem | FloorNestedComboBoxItem;
|
||||||
|
|
||||||
if (floor) {
|
if (floor) {
|
||||||
const floorName = computeFloorName(floor);
|
const floorName = computeFloorName(floor);
|
||||||
|
|
||||||
@@ -213,7 +309,7 @@ export const getAreasAndFloors = (
|
|||||||
})
|
})
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
items.push({
|
floorItem = {
|
||||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||||
type: "floor",
|
type: "floor",
|
||||||
primary: floorName,
|
primary: floorName,
|
||||||
@@ -225,25 +321,9 @@ export const getAreasAndFloors = (
|
|||||||
...floor.aliases,
|
...floor.aliases,
|
||||||
...areaSearchLabels,
|
...areaSearchLabels,
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
items.push(
|
const floorAreasItems = floorAreas.map((area) => {
|
||||||
...floorAreas.map((area) => {
|
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
|
||||||
return {
|
|
||||||
id: formatId({ id: area.area_id, type: "area" }),
|
|
||||||
type: "area" as const,
|
|
||||||
primary: areaName,
|
|
||||||
area: area,
|
|
||||||
icon: area.icon || undefined,
|
|
||||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
items.push(
|
|
||||||
...unassignedAreas.map((area) => {
|
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
return {
|
return {
|
||||||
id: formatId({ id: area.area_id, type: "area" }),
|
id: formatId({ id: area.area_id, type: "area" }),
|
||||||
@@ -253,8 +333,38 @@ export const getAreasAndFloors = (
|
|||||||
icon: area.icon || undefined,
|
icon: area.icon || undefined,
|
||||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
if (floor) {
|
||||||
|
items.push(floorItem!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nested && floor) {
|
||||||
|
(floorItem! as FloorNestedComboBoxItem).areas = floorAreasItems;
|
||||||
|
} else {
|
||||||
|
items.push(...floorAreasItems);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unassignedAreaItems = unassignedAreas.map((area) => {
|
||||||
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
|
return {
|
||||||
|
id: formatId({ id: area.area_id, type: "area" }),
|
||||||
|
type: "area" as const,
|
||||||
|
primary: areaName,
|
||||||
|
area: area,
|
||||||
|
icon: area.icon || undefined,
|
||||||
|
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nested && unassignedAreaItems.length) {
|
||||||
|
items.push({
|
||||||
|
areas: unassignedAreaItems,
|
||||||
|
} as UnassignedAreasFloorComboBoxItem);
|
||||||
|
} else {
|
||||||
|
items.push(...unassignedAreaItems);
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,11 +60,12 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getAreaEntityLookup = (
|
export const getAreaEntityLookup = (
|
||||||
entities: EntityRegistryEntry[]
|
entities: EntityRegistryEntry[],
|
||||||
|
filterHidden = false
|
||||||
): AreaEntityLookup => {
|
): AreaEntityLookup => {
|
||||||
const areaEntityLookup: AreaEntityLookup = {};
|
const areaEntityLookup: AreaEntityLookup = {};
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (!entity.area_id) {
|
if (!entity.area_id || (filterHidden && entity.hidden_by)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!(entity.area_id in areaEntityLookup)) {
|
if (!(entity.area_id in areaEntityLookup)) {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
HassEntityBase,
|
HassEntityBase,
|
||||||
HassServiceTarget,
|
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
import { ensureArray } from "../common/array/ensure-array";
|
import { ensureArray } from "../common/array/ensure-array";
|
||||||
import type { WeekdayShort } from "../common/datetime/weekday";
|
|
||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
import type { LocalizeKeys } from "../common/translations/localize";
|
import type { LocalizeKeys } from "../common/translations/localize";
|
||||||
import { createSearchParam } from "../common/url/search-params";
|
import { createSearchParam } from "../common/url/search-params";
|
||||||
@@ -14,19 +12,11 @@ import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
|||||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||||
import type { Action, Field, MODES } from "./script";
|
import type { Action, Field, MODES } from "./script";
|
||||||
import { migrateAutomationAction } from "./script";
|
import { migrateAutomationAction } from "./script";
|
||||||
import type { TriggerDescription } from "./trigger";
|
import type { WeekdayShort } from "../common/datetime/weekday";
|
||||||
|
|
||||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||||
|
|
||||||
export const DYNAMIC_PREFIX = "__DYNAMIC__";
|
|
||||||
|
|
||||||
export const isDynamic = (key: string | undefined): boolean | undefined =>
|
|
||||||
key?.startsWith(DYNAMIC_PREFIX);
|
|
||||||
|
|
||||||
export const getValueFromDynamic = (key: string): string =>
|
|
||||||
key.substring(DYNAMIC_PREFIX.length);
|
|
||||||
|
|
||||||
export interface AutomationEntity extends HassEntityBase {
|
export interface AutomationEntity extends HassEntityBase {
|
||||||
attributes: HassEntityAttributeBase & {
|
attributes: HassEntityAttributeBase & {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -96,12 +86,6 @@ export interface BaseTrigger {
|
|||||||
id?: string;
|
id?: string;
|
||||||
variables?: Record<string, unknown>;
|
variables?: Record<string, unknown>;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
options?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlatformTrigger extends BaseTrigger {
|
|
||||||
trigger: Exclude<string, LegacyTrigger["trigger"]>;
|
|
||||||
target?: HassServiceTarget;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StateTrigger extends BaseTrigger {
|
export interface StateTrigger extends BaseTrigger {
|
||||||
@@ -211,7 +195,7 @@ export interface CalendarTrigger extends BaseTrigger {
|
|||||||
offset: string;
|
offset: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LegacyTrigger =
|
export type Trigger =
|
||||||
| StateTrigger
|
| StateTrigger
|
||||||
| MqttTrigger
|
| MqttTrigger
|
||||||
| GeoLocationTrigger
|
| GeoLocationTrigger
|
||||||
@@ -228,9 +212,8 @@ export type LegacyTrigger =
|
|||||||
| TemplateTrigger
|
| TemplateTrigger
|
||||||
| EventTrigger
|
| EventTrigger
|
||||||
| DeviceTrigger
|
| DeviceTrigger
|
||||||
| CalendarTrigger;
|
| CalendarTrigger
|
||||||
|
| TriggerList;
|
||||||
export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
|
|
||||||
|
|
||||||
interface BaseCondition {
|
interface BaseCondition {
|
||||||
condition: string;
|
condition: string;
|
||||||
@@ -592,7 +575,6 @@ export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
|||||||
insertAfter: (value: Trigger | Trigger[]) => boolean;
|
insertAfter: (value: Trigger | Trigger[]) => boolean;
|
||||||
toggleYamlMode: () => void;
|
toggleYamlMode: () => void;
|
||||||
config: Trigger;
|
config: Trigger;
|
||||||
description?: TriggerDescription;
|
|
||||||
yamlMode: boolean;
|
yamlMode: boolean;
|
||||||
uiSupported: boolean;
|
uiSupported: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ import {
|
|||||||
formatListWithAnds,
|
formatListWithAnds,
|
||||||
formatListWithOrs,
|
formatListWithOrs,
|
||||||
} from "../common/string/format-list";
|
} from "../common/string/format-list";
|
||||||
import { hasTemplate } from "../common/string/has-template";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import type { Condition, ForDict, LegacyTrigger, Trigger } from "./automation";
|
import type { Condition, ForDict, Trigger } from "./automation";
|
||||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||||
import {
|
import {
|
||||||
localizeDeviceAutomationCondition,
|
localizeDeviceAutomationCondition,
|
||||||
@@ -26,7 +25,8 @@ import {
|
|||||||
} from "./device_automation";
|
} from "./device_automation";
|
||||||
import type { EntityRegistryEntry } from "./entity_registry";
|
import type { EntityRegistryEntry } from "./entity_registry";
|
||||||
import type { FrontendLocaleData } from "./translation";
|
import type { FrontendLocaleData } from "./translation";
|
||||||
import { getTriggerDomain, getTriggerObjectId, isTriggerList } from "./trigger";
|
import { isTriggerList } from "./trigger";
|
||||||
|
import { hasTemplate } from "../common/string/has-template";
|
||||||
|
|
||||||
const triggerTranslationBaseKey =
|
const triggerTranslationBaseKey =
|
||||||
"ui.panel.config.automation.editor.triggers.type";
|
"ui.panel.config.automation.editor.triggers.type";
|
||||||
@@ -121,37 +121,6 @@ const tryDescribeTrigger = (
|
|||||||
return trigger.alias;
|
return trigger.alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = describeLegacyTrigger(
|
|
||||||
trigger as LegacyTrigger,
|
|
||||||
hass,
|
|
||||||
entityRegistry
|
|
||||||
);
|
|
||||||
|
|
||||||
if (description) {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerType = trigger.trigger;
|
|
||||||
|
|
||||||
const domain = getTriggerDomain(trigger.trigger);
|
|
||||||
const type = getTriggerObjectId(trigger.trigger);
|
|
||||||
|
|
||||||
return (
|
|
||||||
hass.localize(
|
|
||||||
`component.${domain}.triggers.${type}.description_configured`
|
|
||||||
) ||
|
|
||||||
hass.localize(
|
|
||||||
`ui.panel.config.automation.editor.triggers.type.${triggerType as LegacyTrigger["trigger"]}.label`
|
|
||||||
) ||
|
|
||||||
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const describeLegacyTrigger = (
|
|
||||||
trigger: LegacyTrigger,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entityRegistry: EntityRegistryEntry[]
|
|
||||||
) => {
|
|
||||||
// Event Trigger
|
// Event Trigger
|
||||||
if (trigger.trigger === "event" && trigger.event_type) {
|
if (trigger.trigger === "event" && trigger.event_type) {
|
||||||
const eventTypes: string[] = [];
|
const eventTypes: string[] = [];
|
||||||
@@ -833,7 +802,13 @@ const describeLegacyTrigger = (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return undefined;
|
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
|
||||||
|
) ||
|
||||||
|
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const describeCondition = (
|
export const describeCondition = (
|
||||||
|
|||||||
@@ -107,11 +107,12 @@ export const sortDeviceRegistryByName = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getDeviceEntityLookup = (
|
export const getDeviceEntityLookup = (
|
||||||
entities: EntityRegistryEntry[]
|
entities: EntityRegistryEntry[],
|
||||||
|
filterHidden = false
|
||||||
): DeviceEntityLookup => {
|
): DeviceEntityLookup => {
|
||||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (!entity.device_id) {
|
if (!entity.device_id || (filterHidden && entity.hidden_by)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!(entity.device_id in deviceEntityLookup)) {
|
if (!(entity.device_id in deviceEntityLookup)) {
|
||||||
|
|||||||
@@ -360,35 +360,6 @@ export const getReferencedStatisticIds = (
|
|||||||
return statIDs;
|
return statIDs;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getReferencedStatisticIdsPower = (
|
|
||||||
prefs: EnergyPreferences
|
|
||||||
): string[] => {
|
|
||||||
const statIDs: (string | undefined)[] = [];
|
|
||||||
|
|
||||||
for (const source of prefs.energy_sources) {
|
|
||||||
if (source.type === "gas" || source.type === "water") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.type === "solar") {
|
|
||||||
statIDs.push(source.stat_rate);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.type === "battery") {
|
|
||||||
statIDs.push(source.stat_rate);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.power) {
|
|
||||||
statIDs.push(...source.power.map((p) => p.stat_rate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
statIDs.push(...prefs.device_consumption.map((d) => d.stat_rate));
|
|
||||||
|
|
||||||
return statIDs.filter(Boolean) as string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const enum CompareMode {
|
export const enum CompareMode {
|
||||||
NONE = "",
|
NONE = "",
|
||||||
PREVIOUS = "previous",
|
PREVIOUS = "previous",
|
||||||
@@ -436,10 +407,9 @@ const getEnergyData = async (
|
|||||||
"gas",
|
"gas",
|
||||||
"device",
|
"device",
|
||||||
]);
|
]);
|
||||||
const powerStatIds = getReferencedStatisticIdsPower(prefs);
|
|
||||||
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
|
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
|
||||||
|
|
||||||
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
|
const allStatIDs = [...energyStatIds, ...waterStatIds];
|
||||||
|
|
||||||
const dayDifference = differenceInDays(end || new Date(), start);
|
const dayDifference = differenceInDays(end || new Date(), start);
|
||||||
const period =
|
const period =
|
||||||
@@ -450,8 +420,6 @@ const getEnergyData = async (
|
|||||||
: dayDifference > 2
|
: dayDifference > 2
|
||||||
? "day"
|
? "day"
|
||||||
: "hour";
|
: "hour";
|
||||||
const finePeriod =
|
|
||||||
dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
|
|
||||||
|
|
||||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||||
const statsMetadataArray = allStatIDs.length
|
const statsMetadataArray = allStatIDs.length
|
||||||
@@ -473,9 +441,6 @@ const getEnergyData = async (
|
|||||||
? (gasUnit as (typeof VOLUME_UNITS)[number])
|
? (gasUnit as (typeof VOLUME_UNITS)[number])
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
const powerUnits: StatisticsUnitConfiguration = {
|
|
||||||
power: "kW",
|
|
||||||
};
|
|
||||||
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
|
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
|
||||||
const waterUnits: StatisticsUnitConfiguration = {
|
const waterUnits: StatisticsUnitConfiguration = {
|
||||||
volume: waterUnit,
|
volume: waterUnit,
|
||||||
@@ -486,12 +451,6 @@ const getEnergyData = async (
|
|||||||
"change",
|
"change",
|
||||||
])
|
])
|
||||||
: {};
|
: {};
|
||||||
const _powerStats: Statistics | Promise<Statistics> = powerStatIds.length
|
|
||||||
? fetchStatistics(hass!, start, end, powerStatIds, finePeriod, powerUnits, [
|
|
||||||
"mean",
|
|
||||||
])
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
|
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
|
||||||
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
|
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
|
||||||
"change",
|
"change",
|
||||||
@@ -598,7 +557,6 @@ const getEnergyData = async (
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
energyStats,
|
energyStats,
|
||||||
powerStats,
|
|
||||||
waterStats,
|
waterStats,
|
||||||
energyStatsCompare,
|
energyStatsCompare,
|
||||||
waterStatsCompare,
|
waterStatsCompare,
|
||||||
@@ -606,14 +564,13 @@ const getEnergyData = async (
|
|||||||
fossilEnergyConsumptionCompare,
|
fossilEnergyConsumptionCompare,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
_energyStats,
|
_energyStats,
|
||||||
_powerStats,
|
|
||||||
_waterStats,
|
_waterStats,
|
||||||
_energyStatsCompare,
|
_energyStatsCompare,
|
||||||
_waterStatsCompare,
|
_waterStatsCompare,
|
||||||
_fossilEnergyConsumption,
|
_fossilEnergyConsumption,
|
||||||
_fossilEnergyConsumptionCompare,
|
_fossilEnergyConsumptionCompare,
|
||||||
]);
|
]);
|
||||||
const stats = { ...energyStats, ...waterStats, ...powerStats };
|
const stats = { ...energyStats, ...waterStats };
|
||||||
if (compare) {
|
if (compare) {
|
||||||
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import type {
|
|||||||
} from "./entity_registry";
|
} from "./entity_registry";
|
||||||
|
|
||||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||||
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
|
|
||||||
|
|
||||||
/** Icon to use when no icon specified for service. */
|
/** Icon to use when no icon specified for service. */
|
||||||
export const DEFAULT_SERVICE_ICON = mdiRoomService;
|
export const DEFAULT_SERVICE_ICON = mdiRoomService;
|
||||||
@@ -134,19 +133,14 @@ const resources: {
|
|||||||
all?: Promise<Record<string, ServiceIcons>>;
|
all?: Promise<Record<string, ServiceIcons>>;
|
||||||
domains: Record<string, ServiceIcons | Promise<ServiceIcons>>;
|
domains: Record<string, ServiceIcons | Promise<ServiceIcons>>;
|
||||||
};
|
};
|
||||||
triggers: {
|
|
||||||
all?: Promise<Record<string, TriggerIcons>>;
|
|
||||||
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
|
|
||||||
};
|
|
||||||
} = {
|
} = {
|
||||||
entity: {},
|
entity: {},
|
||||||
entity_component: {},
|
entity_component: {},
|
||||||
services: { domains: {} },
|
services: { domains: {} },
|
||||||
triggers: { domains: {} },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IconResources<
|
interface IconResources<
|
||||||
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
|
T extends ComponentIcons | PlatformIcons | ServiceIcons,
|
||||||
> {
|
> {
|
||||||
resources: Record<string, T>;
|
resources: Record<string, T>;
|
||||||
}
|
}
|
||||||
@@ -190,22 +184,12 @@ type ServiceIcons = Record<
|
|||||||
{ service: string; sections?: Record<string, string> }
|
{ service: string; sections?: Record<string, string> }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type TriggerIcons = Record<
|
export type IconCategory = "entity" | "entity_component" | "services";
|
||||||
string,
|
|
||||||
{ trigger: string; sections?: Record<string, string> }
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type IconCategory =
|
|
||||||
| "entity"
|
|
||||||
| "entity_component"
|
|
||||||
| "services"
|
|
||||||
| "triggers";
|
|
||||||
|
|
||||||
interface CategoryType {
|
interface CategoryType {
|
||||||
entity: PlatformIcons;
|
entity: PlatformIcons;
|
||||||
entity_component: ComponentIcons;
|
entity_component: ComponentIcons;
|
||||||
services: ServiceIcons;
|
services: ServiceIcons;
|
||||||
triggers: TriggerIcons;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getHassIcons = async <T extends IconCategory>(
|
export const getHassIcons = async <T extends IconCategory>(
|
||||||
@@ -274,59 +258,42 @@ export const getComponentIcons = async (
|
|||||||
return resources.entity_component.resources.then((res) => res[domain]);
|
return resources.entity_component.resources.then((res) => res[domain]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategoryIcons = async <
|
export const getServiceIcons = async (
|
||||||
T extends Exclude<IconCategory, "entity" | "entity_component">,
|
|
||||||
>(
|
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
category: T,
|
|
||||||
domain?: string,
|
domain?: string,
|
||||||
force = false
|
force = false
|
||||||
): Promise<CategoryType[T] | Record<string, CategoryType[T]> | undefined> => {
|
): Promise<ServiceIcons | Record<string, ServiceIcons> | undefined> => {
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
if (!force && resources[category].all) {
|
if (!force && resources.services.all) {
|
||||||
return resources[category].all as Promise<
|
return resources.services.all;
|
||||||
Record<string, CategoryType[T]>
|
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
resources[category].all = getHassIcons(hass, category).then((res) => {
|
resources.services.all = getHassIcons(hass, "services", domain).then(
|
||||||
resources[category].domains = res.resources as any;
|
(res) => {
|
||||||
return res?.resources as Record<string, CategoryType[T]>;
|
resources.services.domains = res.resources;
|
||||||
}) as any;
|
return res?.resources;
|
||||||
return resources[category].all as Promise<Record<string, CategoryType[T]>>;
|
}
|
||||||
|
);
|
||||||
|
return resources.services.all;
|
||||||
}
|
}
|
||||||
if (!force && domain in resources[category].domains) {
|
if (!force && domain in resources.services.domains) {
|
||||||
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
return resources.services.domains[domain];
|
||||||
}
|
}
|
||||||
if (resources[category].all && !force) {
|
if (resources.services.all && !force) {
|
||||||
await resources[category].all;
|
await resources.services.all;
|
||||||
if (domain in resources[category].domains) {
|
if (domain in resources.services.domains) {
|
||||||
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
return resources.services.domains[domain];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isComponentLoaded(hass, domain)) {
|
if (!isComponentLoaded(hass, domain)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const result = getHassIcons(hass, category, domain);
|
const result = getHassIcons(hass, "services", domain);
|
||||||
resources[category].domains[domain] = result.then(
|
resources.services.domains[domain] = result.then(
|
||||||
(res) => res?.resources[domain]
|
(res) => res?.resources[domain]
|
||||||
) as any;
|
);
|
||||||
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
return resources.services.domains[domain];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServiceIcons = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
domain?: string,
|
|
||||||
force = false
|
|
||||||
): Promise<ServiceIcons | Record<string, ServiceIcons> | undefined> =>
|
|
||||||
getCategoryIcons(hass, "services", domain, force);
|
|
||||||
|
|
||||||
export const getTriggerIcons = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
domain?: string,
|
|
||||||
force = false
|
|
||||||
): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> =>
|
|
||||||
getCategoryIcons(hass, "triggers", domain, force);
|
|
||||||
|
|
||||||
// Cache for sorted range keys
|
// Cache for sorted range keys
|
||||||
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
|
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
|
||||||
|
|
||||||
@@ -506,26 +473,6 @@ export const attributeIcon = async (
|
|||||||
return icon;
|
return icon;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const triggerIcon = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
trigger: string
|
|
||||||
): Promise<string | undefined> => {
|
|
||||||
let icon: string | undefined;
|
|
||||||
|
|
||||||
const domain = getTriggerDomain(trigger);
|
|
||||||
const triggerName = getTriggerObjectId(trigger);
|
|
||||||
|
|
||||||
const triggerIcons = await getTriggerIcons(hass, domain);
|
|
||||||
if (triggerIcons) {
|
|
||||||
const trgrIcon = triggerIcons[triggerName] as TriggerIcons[string];
|
|
||||||
icon = trgrIcon?.trigger;
|
|
||||||
}
|
|
||||||
if (!icon) {
|
|
||||||
icon = await domainIcon(hass, domain);
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serviceIcon = async (
|
export const serviceIcon = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
service: string
|
service: string
|
||||||
|
|||||||
@@ -101,7 +101,10 @@ export const deleteLabelRegistryEntry = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getLabels = (
|
export const getLabels = (
|
||||||
hass: HomeAssistant,
|
hassStates: HomeAssistant["states"],
|
||||||
|
hassAreas: HomeAssistant["areas"],
|
||||||
|
hassDevices: HomeAssistant["devices"],
|
||||||
|
hassEntities: HomeAssistant["entities"],
|
||||||
labels?: LabelRegistryEntry[],
|
labels?: LabelRegistryEntry[],
|
||||||
includeDomains?: string[],
|
includeDomains?: string[],
|
||||||
excludeDomains?: string[],
|
excludeDomains?: string[],
|
||||||
@@ -115,8 +118,8 @@ export const getLabels = (
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices = Object.values(hass.devices);
|
const devices = Object.values(hassDevices);
|
||||||
const entities = Object.values(hass.entities);
|
const entities = Object.values(hassEntities);
|
||||||
|
|
||||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||||
@@ -170,7 +173,7 @@ export const getLabels = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return deviceEntityLookup[device.id].some((entity) => {
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
const stateObj = hass.states[entity.entity_id];
|
const stateObj = hassStates[entity.entity_id];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -181,7 +184,7 @@ export const getLabels = (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
inputEntities = inputEntities!.filter((entity) => {
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
const stateObj = hass.states[entity.entity_id];
|
const stateObj = hassStates[entity.entity_id];
|
||||||
return (
|
return (
|
||||||
stateObj.attributes.device_class &&
|
stateObj.attributes.device_class &&
|
||||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||||
@@ -200,7 +203,7 @@ export const getLabels = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return deviceEntityLookup[device.id].some((entity) => {
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
const stateObj = hass.states[entity.entity_id];
|
const stateObj = hassStates[entity.entity_id];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -208,7 +211,7 @@ export const getLabels = (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
inputEntities = inputEntities!.filter((entity) => {
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
const stateObj = hass.states[entity.entity_id];
|
const stateObj = hassStates[entity.entity_id];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -245,7 +248,7 @@ export const getLabels = (
|
|||||||
|
|
||||||
if (areaIds) {
|
if (areaIds) {
|
||||||
areaIds.forEach((areaId) => {
|
areaIds.forEach((areaId) => {
|
||||||
const area = hass.areas[areaId];
|
const area = hassAreas[areaId];
|
||||||
area.labels.forEach((label) => usedLabels.add(label));
|
area.labels.forEach((label) => usedLabels.add(label));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export interface TodoItem {
|
|||||||
status: TodoItemStatus | null;
|
status: TodoItemStatus | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
due?: string | null;
|
due?: string | null;
|
||||||
completed?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum TodoListEntityFeature {
|
export const enum TodoListEntityFeature {
|
||||||
|
|||||||
@@ -73,8 +73,7 @@ export type TranslationCategory =
|
|||||||
| "application_credentials"
|
| "application_credentials"
|
||||||
| "issues"
|
| "issues"
|
||||||
| "selector"
|
| "selector"
|
||||||
| "services"
|
| "services";
|
||||||
| "triggers";
|
|
||||||
|
|
||||||
export const subscribeTranslationPreferences = (
|
export const subscribeTranslationPreferences = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|||||||
@@ -1,20 +1,57 @@
|
|||||||
import { mdiMapClock, mdiShape } from "@mdi/js";
|
import {
|
||||||
|
mdiAvTimer,
|
||||||
|
mdiCalendar,
|
||||||
|
mdiClockOutline,
|
||||||
|
mdiCodeBraces,
|
||||||
|
mdiDevices,
|
||||||
|
mdiFormatListBulleted,
|
||||||
|
mdiGestureDoubleTap,
|
||||||
|
mdiMapClock,
|
||||||
|
mdiMapMarker,
|
||||||
|
mdiMapMarkerRadius,
|
||||||
|
mdiMessageAlert,
|
||||||
|
mdiMicrophoneMessage,
|
||||||
|
mdiNfcVariant,
|
||||||
|
mdiNumeric,
|
||||||
|
mdiShape,
|
||||||
|
mdiStateMachine,
|
||||||
|
mdiSwapHorizontal,
|
||||||
|
mdiWeatherSunny,
|
||||||
|
mdiWebhook,
|
||||||
|
} from "@mdi/js";
|
||||||
|
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import type {
|
import type {
|
||||||
AutomationElementGroupCollection,
|
AutomationElementGroupCollection,
|
||||||
Trigger,
|
Trigger,
|
||||||
TriggerList,
|
TriggerList,
|
||||||
} from "./automation";
|
} from "./automation";
|
||||||
import type { Selector, TargetSelector } from "./selector";
|
|
||||||
|
export const TRIGGER_ICONS = {
|
||||||
|
calendar: mdiCalendar,
|
||||||
|
device: mdiDevices,
|
||||||
|
event: mdiGestureDoubleTap,
|
||||||
|
state: mdiStateMachine,
|
||||||
|
geo_location: mdiMapMarker,
|
||||||
|
homeassistant: mdiHomeAssistant,
|
||||||
|
mqtt: mdiSwapHorizontal,
|
||||||
|
numeric_state: mdiNumeric,
|
||||||
|
sun: mdiWeatherSunny,
|
||||||
|
conversation: mdiMicrophoneMessage,
|
||||||
|
tag: mdiNfcVariant,
|
||||||
|
template: mdiCodeBraces,
|
||||||
|
time: mdiClockOutline,
|
||||||
|
time_pattern: mdiAvTimer,
|
||||||
|
webhook: mdiWebhook,
|
||||||
|
persistent_notification: mdiMessageAlert,
|
||||||
|
zone: mdiMapMarkerRadius,
|
||||||
|
list: mdiFormatListBulleted,
|
||||||
|
};
|
||||||
|
|
||||||
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||||
{
|
{
|
||||||
groups: {
|
groups: {
|
||||||
device: {},
|
device: {},
|
||||||
dynamicGroups: {},
|
|
||||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||||
time_location: {
|
time_location: {
|
||||||
icon: mdiMapClock,
|
icon: mdiMapClock,
|
||||||
@@ -46,33 +83,3 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
|||||||
|
|
||||||
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
|
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
|
||||||
"triggers" in trigger;
|
"triggers" in trigger;
|
||||||
|
|
||||||
export interface TriggerDescription {
|
|
||||||
target?: TargetSelector["target"];
|
|
||||||
fields: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
example?: string | boolean | number;
|
|
||||||
default?: unknown;
|
|
||||||
required?: boolean;
|
|
||||||
selector?: Selector;
|
|
||||||
context?: Record<string, string>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TriggerDescriptions = Record<string, TriggerDescription>;
|
|
||||||
|
|
||||||
export const subscribeTriggers = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
callback: (triggers: TriggerDescriptions) => void
|
|
||||||
) =>
|
|
||||||
hass.connection.subscribeMessage<TriggerDescriptions>(callback, {
|
|
||||||
type: "trigger_platforms/subscribe",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getTriggerDomain = (trigger: string) =>
|
|
||||||
trigger.includes(".") ? computeDomain(trigger) : trigger;
|
|
||||||
|
|
||||||
export const getTriggerObjectId = (trigger: string) =>
|
|
||||||
trigger.includes(".") ? computeObjectId(trigger) : "_";
|
|
||||||
|
|||||||
@@ -40,6 +40,12 @@
|
|||||||
color: var(--primary-text-color, #212121);
|
color: var(--primary-text-color, #212121);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
html {
|
||||||
|
background-color: var(--primary-background-color, #111111);
|
||||||
|
color: var(--primary-text-color, #e1e1e1);
|
||||||
|
}
|
||||||
|
}
|
||||||
#ha-launch-screen {
|
#ha-launch-screen {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -56,6 +62,12 @@
|
|||||||
background-color: var(--primary-background-color, #fafafa);
|
background-color: var(--primary-background-color, #fafafa);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
/* body selector to avoid minification causing bad jinja2 */
|
||||||
|
body #ha-launch-screen {
|
||||||
|
background-color: var(--primary-background-color, #111111);
|
||||||
|
}
|
||||||
|
}
|
||||||
#ha-launch-screen.removing {
|
#ha-launch-screen.removing {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -80,14 +92,6 @@
|
|||||||
opacity: .66;
|
opacity: .66;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
html {
|
|
||||||
background-color: var(--primary-background-color, #111111);
|
|
||||||
color: var(--primary-text-color, #e1e1e1);
|
|
||||||
}
|
|
||||||
/* body selector to avoid minification causing bad jinja2 */
|
|
||||||
body #ha-launch-screen {
|
|
||||||
background-color: var(--primary-background-color, #111111);
|
|
||||||
}
|
|
||||||
.ohf-logo {
|
.ohf-logo {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ import "../../../../components/ha-sortable";
|
|||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import {
|
import {
|
||||||
ACTION_BUILDING_BLOCKS,
|
ACTION_BUILDING_BLOCKS,
|
||||||
|
getService,
|
||||||
|
isService,
|
||||||
VIRTUAL_ACTIONS,
|
VIRTUAL_ACTIONS,
|
||||||
} from "../../../../data/action";
|
} from "../../../../data/action";
|
||||||
import {
|
import type { AutomationClipboard } from "../../../../data/automation";
|
||||||
getValueFromDynamic,
|
|
||||||
isDynamic,
|
|
||||||
type AutomationClipboard,
|
|
||||||
} from "../../../../data/automation";
|
|
||||||
import type { Action } from "../../../../data/script";
|
import type { Action } from "../../../../data/script";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import {
|
import {
|
||||||
@@ -219,9 +217,9 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
actions = this.actions.concat(deepClone(this._clipboard!.action));
|
actions = this.actions.concat(deepClone(this._clipboard!.action));
|
||||||
} else if (action in VIRTUAL_ACTIONS) {
|
} else if (action in VIRTUAL_ACTIONS) {
|
||||||
actions = this.actions.concat(VIRTUAL_ACTIONS[action]);
|
actions = this.actions.concat(VIRTUAL_ACTIONS[action]);
|
||||||
} else if (isDynamic(action)) {
|
} else if (isService(action)) {
|
||||||
actions = this.actions.concat({
|
actions = this.actions.concat({
|
||||||
action: getValueFromDynamic(action),
|
action: getService(action),
|
||||||
metadata: {},
|
metadata: {},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,810 @@
|
|||||||
|
import "@home-assistant/webawesome/dist/components/tree-item/tree-item";
|
||||||
|
import "@home-assistant/webawesome/dist/components/tree/tree";
|
||||||
|
import type { WaSelectionChangeEvent } from "@home-assistant/webawesome/dist/events/selection-change";
|
||||||
|
import { consume } from "@lit/context";
|
||||||
|
import { mdiSelectionMarker, mdiTextureBox } from "@mdi/js";
|
||||||
|
import type { SingleHassServiceTarget } from "home-assistant-js-websocket";
|
||||||
|
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||||
|
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||||
|
import "../../../../components/entity/state-badge";
|
||||||
|
import "../../../../components/ha-floor-icon";
|
||||||
|
import "../../../../components/ha-icon";
|
||||||
|
import "../../../../components/ha-icon-next";
|
||||||
|
import "../../../../components/ha-md-list";
|
||||||
|
import "../../../../components/ha-md-list-item";
|
||||||
|
import "../../../../components/ha-section-title";
|
||||||
|
import "../../../../components/ha-svg-icon";
|
||||||
|
import {
|
||||||
|
getAreasNestedInFloors,
|
||||||
|
type AreaFloorValue,
|
||||||
|
type FloorComboBoxItem,
|
||||||
|
type FloorNestedComboBoxItem,
|
||||||
|
type UnassignedAreasFloorComboBoxItem,
|
||||||
|
} from "../../../../data/area_floor";
|
||||||
|
import {
|
||||||
|
getConfigEntries,
|
||||||
|
type ConfigEntry,
|
||||||
|
} from "../../../../data/config_entries";
|
||||||
|
import {
|
||||||
|
areasContext,
|
||||||
|
devicesContext,
|
||||||
|
entitiesContext,
|
||||||
|
floorsContext,
|
||||||
|
labelsContext,
|
||||||
|
localizeContext,
|
||||||
|
statesContext,
|
||||||
|
} from "../../../../data/context";
|
||||||
|
import {
|
||||||
|
getLabels,
|
||||||
|
type LabelRegistryEntry,
|
||||||
|
} from "../../../../data/label_registry";
|
||||||
|
import { extractFromTarget } from "../../../../data/target";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { brandsUrl } from "../../../../util/brands-url";
|
||||||
|
|
||||||
|
const SEPARATOR = "________";
|
||||||
|
|
||||||
|
interface DeviceEntries {
|
||||||
|
open: boolean;
|
||||||
|
entities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AreaEntries {
|
||||||
|
open: boolean;
|
||||||
|
devices: Record<string, DeviceEntries>;
|
||||||
|
entities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-automation-add-from-target")
|
||||||
|
export default class HaAutomationAddFromTarget extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public value?: SingleHassServiceTarget;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
// #region context
|
||||||
|
@state()
|
||||||
|
@consume({ context: localizeContext, subscribe: true })
|
||||||
|
private localize!: HomeAssistant["localize"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: statesContext, subscribe: true })
|
||||||
|
private states!: HomeAssistant["states"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: floorsContext, subscribe: true })
|
||||||
|
private floors!: HomeAssistant["floors"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: areasContext, subscribe: true })
|
||||||
|
private areas!: HomeAssistant["areas"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: devicesContext, subscribe: true })
|
||||||
|
private devices!: HomeAssistant["devices"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: entitiesContext, subscribe: true })
|
||||||
|
private entities!: HomeAssistant["entities"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: labelsContext, subscribe: true })
|
||||||
|
private _labelRegistry!: LabelRegistryEntry[];
|
||||||
|
// #endregion context
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _floorAreas: (
|
||||||
|
| FloorNestedComboBoxItem
|
||||||
|
| UnassignedAreasFloorComboBoxItem
|
||||||
|
)[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _areaEntries: Record<string, AreaEntries> = {};
|
||||||
|
|
||||||
|
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||||
|
|
||||||
|
private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
this._loadConfigEntries();
|
||||||
|
this._getTreeData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
${!this.narrow || !this.value ? this._renderFloors() : nothing}
|
||||||
|
${this.narrow && this.value ? this._renderNarrow(this.value) : nothing}
|
||||||
|
${!this.narrow || !this.value ? this._renderLabels() : nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderNarrow(value: SingleHassServiceTarget) {
|
||||||
|
const [valueTypeId, valueId] = Object.entries(value)[0];
|
||||||
|
const valueType = valueTypeId.replace("_id", "");
|
||||||
|
|
||||||
|
if (!valueType || valueType === "label") {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "floor") {
|
||||||
|
return this._renderAreas(
|
||||||
|
this._floorAreas.find(
|
||||||
|
(floor) =>
|
||||||
|
(valueId && floor.id === `${valueType}${SEPARATOR}${valueId}`) ||
|
||||||
|
(!valueId && !floor.id)
|
||||||
|
)?.areas
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "area") {
|
||||||
|
const { devices, entities } =
|
||||||
|
this._areaEntries[`area${SEPARATOR}${valueId}`];
|
||||||
|
const numberOfDevices = Object.keys(devices).length;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${numberOfDevices ? this._renderDevices(devices) : nothing}
|
||||||
|
${entities.length ? this._renderEntities(entities) : nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "device" && this.devices[valueId]) {
|
||||||
|
const deviceArea = this.devices[valueId].area_id!;
|
||||||
|
return this._renderEntities(
|
||||||
|
this._areaEntries[`area${SEPARATOR}${deviceArea}`]?.devices[valueId]
|
||||||
|
?.entities
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderFloors() {
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize(
|
||||||
|
"ui.panel.config.automation.editor.home"
|
||||||
|
)}</ha-section-title
|
||||||
|
>
|
||||||
|
${!this._floorAreas.length ||
|
||||||
|
(!this._floorAreas[0].id && !this._floorAreas[0].areas.length)
|
||||||
|
? html`<ha-md-list>
|
||||||
|
<ha-md-list-item type="text">
|
||||||
|
<div slot="headline">
|
||||||
|
${this.localize("ui.components.area-picker.no_areas")}
|
||||||
|
</div>
|
||||||
|
</ha-md-list-item>
|
||||||
|
</ha-md-list>`
|
||||||
|
: this.narrow
|
||||||
|
? html`<ha-md-list>
|
||||||
|
${this._floorAreas.map((floor, index) =>
|
||||||
|
index === 0 && !floor.id
|
||||||
|
? this._renderAreas(floor.areas)
|
||||||
|
: html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${floor.id || `floor${SEPARATOR}`}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
${floor.id && (floor as FloorNestedComboBoxItem).floor
|
||||||
|
? html`<ha-floor-icon
|
||||||
|
slot="start"
|
||||||
|
.floor=${(floor as FloorNestedComboBoxItem).floor}
|
||||||
|
></ha-floor-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${mdiSelectionMarker}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
|
||||||
|
<div slot="headline">
|
||||||
|
${!floor.id
|
||||||
|
? this.localize(
|
||||||
|
"ui.components.area-picker.unassigned_areas"
|
||||||
|
)
|
||||||
|
: floor.primary}
|
||||||
|
</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`
|
||||||
|
)}
|
||||||
|
</ha-md-list>`
|
||||||
|
: html`<wa-tree @wa-selection-change=${this._handleSelectionChange}>
|
||||||
|
${this._floorAreas.map((floor, index) =>
|
||||||
|
index === 0 && !floor.id
|
||||||
|
? this._renderAreas(floor.areas)
|
||||||
|
: html`<wa-tree-item
|
||||||
|
.disabledSelection=${!floor.id}
|
||||||
|
.target=${floor.id}
|
||||||
|
.selected=${!!floor.id &&
|
||||||
|
this._getSelectedTargetId(this.value) === floor.id}
|
||||||
|
>
|
||||||
|
${floor.id && (floor as FloorNestedComboBoxItem).floor
|
||||||
|
? html`<ha-floor-icon
|
||||||
|
.floor=${(floor as FloorNestedComboBoxItem).floor}
|
||||||
|
></ha-floor-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
.path=${mdiSelectionMarker}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
${!floor.id
|
||||||
|
? this.localize(
|
||||||
|
"ui.components.area-picker.unassigned_areas"
|
||||||
|
)
|
||||||
|
: floor.primary}
|
||||||
|
${this._renderAreas(floor.areas)}
|
||||||
|
</wa-tree-item>`
|
||||||
|
)}
|
||||||
|
</wa-tree>`} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderLabels() {
|
||||||
|
const labels = this._getLabelsMemoized(
|
||||||
|
this.states,
|
||||||
|
this.areas,
|
||||||
|
this.devices,
|
||||||
|
this.entities,
|
||||||
|
this._labelRegistry,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
`label${SEPARATOR}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!labels.length) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize("ui.components.label-picker.labels")}</ha-section-title
|
||||||
|
>
|
||||||
|
<ha-md-list>
|
||||||
|
${labels.map(
|
||||||
|
(label) =>
|
||||||
|
html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${label.id}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
class=${this._getSelectedTargetId(this.value) === label.id
|
||||||
|
? "selected"
|
||||||
|
: ""}
|
||||||
|
>${label.icon
|
||||||
|
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
|
||||||
|
: label.icon_path
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${label.icon_path}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: nothing}
|
||||||
|
<div slot="headline">${label.primary}</div>
|
||||||
|
${this.narrow
|
||||||
|
? html`<ha-icon-next slot="end"></ha-icon-next> `
|
||||||
|
: nothing}
|
||||||
|
</ha-md-list-item>`
|
||||||
|
)}
|
||||||
|
</ha-md-list>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderAreas(areas: FloorComboBoxItem[] = []) {
|
||||||
|
if (!areas.length) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.narrow) {
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize(
|
||||||
|
"ui.components.target-picker.type.areas"
|
||||||
|
)}</ha-section-title
|
||||||
|
>
|
||||||
|
<ha-md-list>
|
||||||
|
${areas.map(({ id, primary, icon, icon_path }) => {
|
||||||
|
if (!this._areaEntries[id]) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${id}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
${icon
|
||||||
|
? html`<ha-icon slot="start" .icon=${icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${icon_path || mdiTextureBox}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
|
||||||
|
<div slot="headline">${primary}</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`;
|
||||||
|
})}
|
||||||
|
</ha-md-list>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return areas.map(({ id, primary, icon, icon_path }) => {
|
||||||
|
if (!this._areaEntries[id]) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { open, devices, entities } = this._areaEntries[id];
|
||||||
|
const numberOfDevices = Object.keys(devices).length;
|
||||||
|
const numberOfItems = numberOfDevices + entities.length;
|
||||||
|
|
||||||
|
return html`<wa-tree-item
|
||||||
|
.target=${id}
|
||||||
|
.selected=${this._getSelectedTargetId(this.value) === id}
|
||||||
|
.lazy=${!open && !!numberOfItems}
|
||||||
|
@wa-lazy-load=${this._expandItem}
|
||||||
|
@wa-collapse=${this._collapseItem}
|
||||||
|
>
|
||||||
|
${icon
|
||||||
|
? html`<ha-icon .icon=${icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
.path=${icon_path || mdiTextureBox}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
${primary}
|
||||||
|
${open
|
||||||
|
? html`
|
||||||
|
${numberOfDevices ? this._renderDevices(devices) : nothing}
|
||||||
|
${entities.length ? this._renderEntities(entities) : nothing}
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</wa-tree-item>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderDevices(devices: Record<string, DeviceEntries>) {
|
||||||
|
const renderedDevices = Object.keys(devices).map((deviceId) => {
|
||||||
|
if (!this.devices[deviceId]) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = this.devices[deviceId];
|
||||||
|
const configEntry = device.primary_config_entry
|
||||||
|
? this._configEntryLookup?.[device.primary_config_entry]
|
||||||
|
: undefined;
|
||||||
|
const domain = configEntry?.domain;
|
||||||
|
|
||||||
|
const deviceName = computeDeviceName(device) || deviceId;
|
||||||
|
|
||||||
|
if (this.narrow) {
|
||||||
|
return html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${`device${SEPARATOR}${deviceId}`}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
${domain
|
||||||
|
? html`
|
||||||
|
<img
|
||||||
|
slot="start"
|
||||||
|
alt=""
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<div slot="headline">${deviceName}</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { open, entities } = devices[deviceId];
|
||||||
|
|
||||||
|
return html`<wa-tree-item
|
||||||
|
.target=${`device${SEPARATOR}${deviceId}`}
|
||||||
|
.selected=${this._getSelectedTargetId(this.value) ===
|
||||||
|
`device${SEPARATOR}${deviceId}`}
|
||||||
|
.lazy=${!open && !!entities.length}
|
||||||
|
@wa-lazy-load=${this._expandItem}
|
||||||
|
@wa-collapse=${this._collapseItem}
|
||||||
|
.title=${deviceName}
|
||||||
|
>
|
||||||
|
${domain
|
||||||
|
? html`
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<span class="item-label">${deviceName}</span>
|
||||||
|
${open ? this._renderEntities(entities) : nothing}
|
||||||
|
</wa-tree-item>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.narrow) {
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize(
|
||||||
|
"ui.components.target-picker.type.devices"
|
||||||
|
)}</ha-section-title
|
||||||
|
>
|
||||||
|
<ha-md-list> ${renderedDevices} </ha-md-list>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderedDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderEntities(entities: string[] = []) {
|
||||||
|
if (!entities.length) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedEntites = entities.map((entityId) => {
|
||||||
|
const stateObj = this.hass.states[entityId];
|
||||||
|
|
||||||
|
const [entityName, deviceName] = computeEntityNameList(
|
||||||
|
stateObj,
|
||||||
|
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||||
|
this.entities,
|
||||||
|
this.devices,
|
||||||
|
this.areas,
|
||||||
|
this.floors
|
||||||
|
);
|
||||||
|
|
||||||
|
const label = entityName || deviceName || entityId;
|
||||||
|
|
||||||
|
if (this.narrow) {
|
||||||
|
return html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${`entity${SEPARATOR}${entityId}`}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
<state-badge
|
||||||
|
slot="start"
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
.hass=${this.hass}
|
||||||
|
></state-badge>
|
||||||
|
<div slot="headline" class="item-label">${label}</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<wa-tree-item
|
||||||
|
.target=${`entity${SEPARATOR}${entityId}`}
|
||||||
|
.selected=${this._getSelectedTargetId(this.value) ===
|
||||||
|
`entity${SEPARATOR}${entityId}`}
|
||||||
|
.title=${label}
|
||||||
|
>
|
||||||
|
<state-badge .stateObj=${stateObj} .hass=${this.hass}></state-badge>
|
||||||
|
<span class="item-label">${label}</span>
|
||||||
|
</wa-tree-item>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.narrow) {
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize(
|
||||||
|
"ui.components.target-picker.type.entities"
|
||||||
|
)}</ha-section-title
|
||||||
|
>
|
||||||
|
<ha-md-list>${renderedEntites}</ha-md-list>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderedEntites;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getSelectedTargetId = memoizeOne(
|
||||||
|
(value: SingleHassServiceTarget | undefined) =>
|
||||||
|
value && Object.keys(value).length
|
||||||
|
? `${Object.keys(value)[0].replace("_id", "")}${SEPARATOR}${Object.values(value)[0]}`
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getTreeData() {
|
||||||
|
this._floorAreas = getAreasNestedInFloors(
|
||||||
|
this.states,
|
||||||
|
this.floors,
|
||||||
|
this.areas,
|
||||||
|
this.devices,
|
||||||
|
this.entities,
|
||||||
|
this._formatId,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const floor of this._floorAreas) {
|
||||||
|
for (const area of floor.areas) {
|
||||||
|
this._loadArea(area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _formatId = memoizeOne((value: AreaFloorValue): string =>
|
||||||
|
[value.type, value.id].join(SEPARATOR)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _handleSelectionChange(ev: WaSelectionChangeEvent) {
|
||||||
|
const treeItem = ev.detail.selection[0] as unknown as
|
||||||
|
| { target?: string }
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (treeItem?.target) {
|
||||||
|
this._valueChanged(treeItem.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _selectItem(ev: CustomEvent) {
|
||||||
|
const target = (ev.currentTarget as any).target;
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
this._valueChanged(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(itemId: string) {
|
||||||
|
const [type, id] = itemId.split(SEPARATOR, 2);
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { [`${type}_id`]: id || undefined },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadArea(area: FloorComboBoxItem) {
|
||||||
|
try {
|
||||||
|
const [, id] = area.id.split(SEPARATOR, 2);
|
||||||
|
const targetEntries = await extractFromTarget(this.hass, {
|
||||||
|
area_id: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const devices: Record<string, DeviceEntries> = {};
|
||||||
|
|
||||||
|
targetEntries.referenced_devices.forEach((device_id) => {
|
||||||
|
devices[device_id] = {
|
||||||
|
open: false,
|
||||||
|
entities: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const entities: string[] = [];
|
||||||
|
|
||||||
|
targetEntries.referenced_entities.forEach((entity_id) => {
|
||||||
|
const entity = this.hass.entities[entity_id];
|
||||||
|
if (entity.device_id && devices[entity.device_id]) {
|
||||||
|
devices[entity.device_id].entities.push(entity_id);
|
||||||
|
} else {
|
||||||
|
entities.push(entity_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
[area.id]: {
|
||||||
|
open: false,
|
||||||
|
devices,
|
||||||
|
entities,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Failed to extract target", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandItem(ev) {
|
||||||
|
const targetId = ev.target.target;
|
||||||
|
const [type, id] = targetId.split(SEPARATOR, 2);
|
||||||
|
|
||||||
|
if (type === "area") {
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
[targetId]: {
|
||||||
|
...this._areaEntries[targetId],
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (type === "device") {
|
||||||
|
const areaEntry = Object.values(this._areaEntries).find((area) =>
|
||||||
|
Object.keys(area.devices).includes(id)
|
||||||
|
);
|
||||||
|
if (areaEntry) {
|
||||||
|
areaEntry.devices[id].open = true;
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _collapseItem(ev) {
|
||||||
|
const targetId = ev.target.target;
|
||||||
|
const [type, id] = targetId.split(SEPARATOR, 2);
|
||||||
|
|
||||||
|
if (type === "area") {
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
[targetId]: {
|
||||||
|
...this._areaEntries[targetId],
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (type === "device") {
|
||||||
|
const areaEntry = Object.values(this._areaEntries).find((area) =>
|
||||||
|
Object.keys(area.devices).includes(id)
|
||||||
|
);
|
||||||
|
if (areaEntry) {
|
||||||
|
areaEntry.devices[id].open = false;
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadConfigEntries() {
|
||||||
|
const configEntries = await getConfigEntries(this.hass);
|
||||||
|
this._configEntryLookup = Object.fromEntries(
|
||||||
|
configEntries.map((entry) => [entry.entry_id, entry])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public navigateBack() {
|
||||||
|
if (!this.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueType = Object.keys(this.value)[0].replace("_id", "");
|
||||||
|
const valueId = this.value[`${valueType}_id`];
|
||||||
|
|
||||||
|
if (valueType === "floor" || valueType === "label") {
|
||||||
|
fireEvent(this, "value-changed", { value: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "area") {
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { floor_id: this.areas[valueId].floor_id },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "device") {
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { area_id: this.devices[valueId].area_id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "entity") {
|
||||||
|
for (const [areaId, areaEntry] of Object.entries(this._areaEntries)) {
|
||||||
|
const entityDeviceId = this.entities[valueId].device_id;
|
||||||
|
if (entityDeviceId && areaEntry.devices[entityDeviceId]) {
|
||||||
|
// Device is also in area -> go back to device
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areaEntry.entities.includes(valueId)) {
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { area_id: areaId.split(SEPARATOR, 2)[1] },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { device_id: this.entities[valueId].device_id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
--wa-color-neutral-fill-quiet: var(--ha-color-fill-primary-normal-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-section-title {
|
||||||
|
top: 0;
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tree-item::part(item) {
|
||||||
|
height: var(--ha-space-10);
|
||||||
|
padding: var(--ha-space-1) var(--ha-space-3);
|
||||||
|
cursor: pointer;
|
||||||
|
border-inline-start: 0;
|
||||||
|
}
|
||||||
|
wa-tree-item::part(label) {
|
||||||
|
gap: var(--ha-space-3);
|
||||||
|
font-family: var(--ha-font-family-heading);
|
||||||
|
font-weight: var(--ha-font-weight-medium);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.item-label {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-svg-icon,
|
||||||
|
ha-icon,
|
||||||
|
ha-floor-icon {
|
||||||
|
padding: var(--ha-space-1);
|
||||||
|
color: var(--ha-color-on-neutral-quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tree-item::part(item):hover {
|
||||||
|
background-color: var(--ha-color-fill-neutral-quiet-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: var(--ha-space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
state-badge {
|
||||||
|
min-width: 32px;
|
||||||
|
max-width: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
max-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tree-item[selected],
|
||||||
|
wa-tree-item[selected] > ha-svg-icon,
|
||||||
|
wa-tree-item[selected] > ha-icon,
|
||||||
|
wa-tree-item[selected] > ha-floor-icon {
|
||||||
|
color: var(--ha-color-on-primary-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tree-item[selected]::part(item):hover {
|
||||||
|
background-color: var(--ha-color-fill-primary-normal-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tree-item::part(base).tree-item-selected .item {
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list {
|
||||||
|
padding: 0;
|
||||||
|
--md-list-item-leading-space: var(--ha-space-3);
|
||||||
|
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||||
|
--md-list-item-bottom-space: var(--ha-space-1);
|
||||||
|
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||||
|
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||||
|
--md-list-item-one-line-container-height: var(--ha-space-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list-item.selected {
|
||||||
|
background-color: var(--ha-color-fill-primary-normal-active);
|
||||||
|
--md-list-item-label-text-color: var(--ha-color-on-primary-normal);
|
||||||
|
--icon-primary-color: var(--ha-color-on-primary-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list-item.selected ha-icon,
|
||||||
|
ha-md-list-item.selected ha-svg-icon {
|
||||||
|
color: var(--ha-color-on-primary-normal);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-automation-add-from-target": HaAutomationAddFromTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import type HaAutomationConditionEditor from "../action/ha-automation-action-edi
|
|||||||
import { getAutomationActionType } from "../action/ha-automation-action-row";
|
import { getAutomationActionType } from "../action/ha-automation-action-row";
|
||||||
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
|
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
|
||||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||||
|
import "../trigger/ha-automation-trigger-editor";
|
||||||
import "./ha-automation-sidebar-card";
|
import "./ha-automation-sidebar-card";
|
||||||
|
|
||||||
@customElement("ha-automation-sidebar-action")
|
@customElement("ha-automation-sidebar-action")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import "../../../../components/ha-dialog-header";
|
|||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
import "../../../../components/ha-md-button-menu";
|
import "../../../../components/ha-md-button-menu";
|
||||||
import "../../../../components/ha-md-divider";
|
import "../../../../components/ha-md-divider";
|
||||||
|
import "../../../../components/ha-md-menu-item";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import "../ha-automation-editor-warning";
|
import "../ha-automation-editor-warning";
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import {
|
|||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import "../../../../components/ha-md-divider";
|
|
||||||
import "../../../../components/ha-md-menu-item";
|
|
||||||
import "../../../../components/ha-svg-icon";
|
|
||||||
import type { OptionSidebarConfig } from "../../../../data/automation";
|
import type { OptionSidebarConfig } from "../../../../data/automation";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { isMac } from "../../../../util/is_mac";
|
import { isMac } from "../../../../util/is_mac";
|
||||||
|
|||||||
@@ -15,15 +15,8 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||||||
import { keyed } from "lit/directives/keyed";
|
import { keyed } from "lit/directives/keyed";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||||
import type {
|
import type { TriggerSidebarConfig } from "../../../../data/automation";
|
||||||
LegacyTrigger,
|
import { isTriggerList } from "../../../../data/trigger";
|
||||||
TriggerSidebarConfig,
|
|
||||||
} from "../../../../data/automation";
|
|
||||||
import {
|
|
||||||
getTriggerDomain,
|
|
||||||
getTriggerObjectId,
|
|
||||||
isTriggerList,
|
|
||||||
} from "../../../../data/trigger";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { isMac } from "../../../../util/is_mac";
|
import { isMac } from "../../../../util/is_mac";
|
||||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||||
@@ -70,7 +63,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const rowDisabled =
|
const rowDisabled =
|
||||||
"enabled" in this.config.config && this.config.config.enabled === false;
|
this.disabled ||
|
||||||
|
("enabled" in this.config.config && this.config.config.enabled === false);
|
||||||
const type = isTriggerList(this.config.config)
|
const type = isTriggerList(this.config.config)
|
||||||
? "list"
|
? "list"
|
||||||
: this.config.config.trigger;
|
: this.config.config.trigger;
|
||||||
@@ -79,18 +73,9 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
|||||||
"ui.panel.config.automation.editor.triggers.trigger"
|
"ui.panel.config.automation.editor.triggers.trigger"
|
||||||
);
|
);
|
||||||
|
|
||||||
const domain =
|
const title = this.hass.localize(
|
||||||
"trigger" in this.config.config &&
|
`ui.panel.config.automation.editor.triggers.type.${type}.label`
|
||||||
getTriggerDomain(this.config.config.trigger);
|
);
|
||||||
const triggerName =
|
|
||||||
"trigger" in this.config.config &&
|
|
||||||
getTriggerObjectId(this.config.config.trigger);
|
|
||||||
|
|
||||||
const title =
|
|
||||||
this.hass.localize(
|
|
||||||
`ui.panel.config.automation.editor.triggers.type.${type as LegacyTrigger["trigger"]}.label`
|
|
||||||
) ||
|
|
||||||
this.hass.localize(`component.${domain}.triggers.${triggerName}.name`);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-automation-sidebar-card
|
<ha-automation-sidebar-card
|
||||||
@@ -284,7 +269,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
|||||||
class="sidebar-editor"
|
class="sidebar-editor"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.trigger=${this.config.config}
|
.trigger=${this.config.config}
|
||||||
.description=${this.config.description}
|
|
||||||
@value-changed=${this._valueChangedSidebar}
|
@value-changed=${this._valueChangedSidebar}
|
||||||
@yaml-changed=${this._yamlChangedSidebar}
|
@yaml-changed=${this._yamlChangedSidebar}
|
||||||
.uiSupported=${this.config.uiSupported}
|
.uiSupported=${this.config.uiSupported}
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ import "../../../../components/ha-yaml-editor";
|
|||||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||||
import type { Trigger } from "../../../../data/automation";
|
import type { Trigger } from "../../../../data/automation";
|
||||||
import { migrateAutomationTrigger } from "../../../../data/automation";
|
import { migrateAutomationTrigger } from "../../../../data/automation";
|
||||||
import type { TriggerDescription } from "../../../../data/trigger";
|
|
||||||
import { isTriggerList } from "../../../../data/trigger";
|
import { isTriggerList } from "../../../../data/trigger";
|
||||||
import { haStyle } from "../../../../resources/styles";
|
import { haStyle } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import "../ha-automation-editor-warning";
|
import "../ha-automation-editor-warning";
|
||||||
import "./types/ha-automation-trigger-platform";
|
|
||||||
|
|
||||||
@customElement("ha-automation-trigger-editor")
|
@customElement("ha-automation-trigger-editor")
|
||||||
export default class HaAutomationTriggerEditor extends LitElement {
|
export default class HaAutomationTriggerEditor extends LitElement {
|
||||||
@@ -33,8 +31,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
|
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public description?: TriggerDescription;
|
|
||||||
|
|
||||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@@ -91,18 +87,11 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
|||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<div @value-changed=${this._onUiChanged}>
|
<div @value-changed=${this._onUiChanged}>
|
||||||
${this.description
|
${dynamicElement(`ha-automation-trigger-${type}`, {
|
||||||
? html`<ha-automation-trigger-platform
|
hass: this.hass,
|
||||||
.hass=${this.hass}
|
trigger: this.trigger,
|
||||||
.trigger=${this.trigger}
|
disabled: this.disabled,
|
||||||
.description=${this.description}
|
})}
|
||||||
.disabled=${this.disabled}
|
|
||||||
></ha-automation-trigger-platform>`
|
|
||||||
: dynamicElement(`ha-automation-trigger-${type}`, {
|
|
||||||
hass: this.hass,
|
|
||||||
trigger: this.trigger,
|
|
||||||
disabled: this.disabled,
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,11 +40,9 @@ import "../../../../components/ha-md-button-menu";
|
|||||||
import "../../../../components/ha-md-divider";
|
import "../../../../components/ha-md-divider";
|
||||||
import "../../../../components/ha-md-menu-item";
|
import "../../../../components/ha-md-menu-item";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
|
|
||||||
import type {
|
import type {
|
||||||
AutomationClipboard,
|
AutomationClipboard,
|
||||||
Trigger,
|
Trigger,
|
||||||
TriggerList,
|
|
||||||
TriggerSidebarConfig,
|
TriggerSidebarConfig,
|
||||||
} from "../../../../data/automation";
|
} from "../../../../data/automation";
|
||||||
import { isTrigger, subscribeTrigger } from "../../../../data/automation";
|
import { isTrigger, subscribeTrigger } from "../../../../data/automation";
|
||||||
@@ -52,8 +50,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
|
|||||||
import { validateConfig } from "../../../../data/config";
|
import { validateConfig } from "../../../../data/config";
|
||||||
import { fullEntitiesContext } from "../../../../data/context";
|
import { fullEntitiesContext } from "../../../../data/context";
|
||||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
|
||||||
import { isTriggerList } from "../../../../data/trigger";
|
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showPromptDialog,
|
showPromptDialog,
|
||||||
@@ -75,7 +72,6 @@ import "./types/ha-automation-trigger-list";
|
|||||||
import "./types/ha-automation-trigger-mqtt";
|
import "./types/ha-automation-trigger-mqtt";
|
||||||
import "./types/ha-automation-trigger-numeric_state";
|
import "./types/ha-automation-trigger-numeric_state";
|
||||||
import "./types/ha-automation-trigger-persistent_notification";
|
import "./types/ha-automation-trigger-persistent_notification";
|
||||||
import "./types/ha-automation-trigger-platform";
|
|
||||||
import "./types/ha-automation-trigger-state";
|
import "./types/ha-automation-trigger-state";
|
||||||
import "./types/ha-automation-trigger-sun";
|
import "./types/ha-automation-trigger-sun";
|
||||||
import "./types/ha-automation-trigger-tag";
|
import "./types/ha-automation-trigger-tag";
|
||||||
@@ -141,9 +137,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
|
|
||||||
@state() private _warnings?: string[];
|
@state() private _warnings?: string[];
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public triggerDescriptions: TriggerDescriptions = {};
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@query("ha-automation-trigger-editor")
|
@query("ha-automation-trigger-editor")
|
||||||
@@ -185,24 +178,18 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderRow() {
|
private _renderRow() {
|
||||||
const type = this._getType(this.trigger, this.triggerDescriptions);
|
const type = this._getType(this.trigger);
|
||||||
|
|
||||||
const supported = this._uiSupported(type);
|
const supported = this._uiSupported(type);
|
||||||
|
|
||||||
const yamlMode = this._yamlMode || !supported;
|
const yamlMode = this._yamlMode || !supported;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${type === "list"
|
<ha-svg-icon
|
||||||
? html`<ha-svg-icon
|
slot="leading-icon"
|
||||||
slot="leading-icon"
|
class="trigger-icon"
|
||||||
class="trigger-icon"
|
.path=${TRIGGER_ICONS[type]}
|
||||||
.path=${TRIGGER_ICONS[type]}
|
></ha-svg-icon>
|
||||||
></ha-svg-icon>`
|
|
||||||
: html`<ha-trigger-icon
|
|
||||||
slot="leading-icon"
|
|
||||||
.hass=${this.hass}
|
|
||||||
.trigger=${(this.trigger as Exclude<Trigger, TriggerList>).trigger}
|
|
||||||
></ha-trigger-icon>`}
|
|
||||||
<h3 slot="header">
|
<h3 slot="header">
|
||||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -406,9 +393,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
<ha-automation-trigger-editor
|
<ha-automation-trigger-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.trigger=${this.trigger}
|
.trigger=${this.trigger}
|
||||||
.description=${"trigger" in this.trigger
|
|
||||||
? this.triggerDescriptions[this.trigger.trigger]
|
|
||||||
: undefined}
|
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.yamlMode=${this._yamlMode}
|
.yamlMode=${this._yamlMode}
|
||||||
.uiSupported=${supported}
|
.uiSupported=${supported}
|
||||||
@@ -568,7 +552,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public openSidebar(trigger?: Trigger): void {
|
public openSidebar(trigger?: Trigger): void {
|
||||||
trigger = trigger || this.trigger;
|
|
||||||
fireEvent(this, "open-sidebar", {
|
fireEvent(this, "open-sidebar", {
|
||||||
save: (value) => {
|
save: (value) => {
|
||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
@@ -593,14 +576,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
duplicate: this._duplicateTrigger,
|
duplicate: this._duplicateTrigger,
|
||||||
cut: this._cutTrigger,
|
cut: this._cutTrigger,
|
||||||
insertAfter: this._insertAfter,
|
insertAfter: this._insertAfter,
|
||||||
config: trigger,
|
config: trigger || this.trigger,
|
||||||
uiSupported: this._uiSupported(
|
uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
|
||||||
this._getType(trigger, this.triggerDescriptions)
|
|
||||||
),
|
|
||||||
description:
|
|
||||||
"trigger" in trigger
|
|
||||||
? this.triggerDescriptions[trigger.trigger]
|
|
||||||
: undefined,
|
|
||||||
yamlMode: this._yamlMode,
|
yamlMode: this._yamlMode,
|
||||||
} satisfies TriggerSidebarConfig);
|
} satisfies TriggerSidebarConfig);
|
||||||
this._selected = true;
|
this._selected = true;
|
||||||
@@ -782,18 +759,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getType = memoizeOne(
|
private _getType = memoizeOne((trigger: Trigger) =>
|
||||||
(trigger: Trigger, triggerDescriptions: TriggerDescriptions) => {
|
isTriggerList(trigger) ? "list" : trigger.trigger
|
||||||
if (isTriggerList(trigger)) {
|
|
||||||
return "list";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trigger.trigger in triggerDescriptions) {
|
|
||||||
return "platform";
|
|
||||||
}
|
|
||||||
|
|
||||||
return trigger.trigger;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private _uiSupported = memoizeOne(
|
private _uiSupported = memoizeOne(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { PropertyValues } from "lit";
|
|||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
|
||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
@@ -13,16 +12,12 @@ import "../../../../components/ha-button";
|
|||||||
import "../../../../components/ha-button-menu";
|
import "../../../../components/ha-button-menu";
|
||||||
import "../../../../components/ha-sortable";
|
import "../../../../components/ha-sortable";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import {
|
import type {
|
||||||
getValueFromDynamic,
|
AutomationClipboard,
|
||||||
isDynamic,
|
Trigger,
|
||||||
type AutomationClipboard,
|
TriggerList,
|
||||||
type Trigger,
|
|
||||||
type TriggerList,
|
|
||||||
} from "../../../../data/automation";
|
} from "../../../../data/automation";
|
||||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
import { isTriggerList } from "../../../../data/trigger";
|
||||||
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
|
|
||||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import {
|
import {
|
||||||
PASTE_VALUE,
|
PASTE_VALUE,
|
||||||
@@ -31,9 +26,10 @@ import {
|
|||||||
import { automationRowsStyles } from "../styles";
|
import { automationRowsStyles } from "../styles";
|
||||||
import "./ha-automation-trigger-row";
|
import "./ha-automation-trigger-row";
|
||||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||||
|
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||||
|
|
||||||
@customElement("ha-automation-trigger")
|
@customElement("ha-automation-trigger")
|
||||||
export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
export default class HaAutomationTrigger extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public triggers!: Trigger[];
|
@property({ attribute: false }) public triggers!: Trigger[];
|
||||||
@@ -66,23 +62,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private _triggerKeys = new WeakMap<Trigger, string>();
|
private _triggerKeys = new WeakMap<Trigger, string>();
|
||||||
|
|
||||||
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
|
||||||
|
|
||||||
protected hassSubscribe() {
|
|
||||||
return [
|
|
||||||
subscribeTriggers(this.hass, (triggers) => this._addTriggers(triggers)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _addTriggers(triggers: TriggerDescriptions) {
|
|
||||||
this._triggerDescriptions = { ...this._triggerDescriptions, ...triggers };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
|
||||||
super.firstUpdated(changedProps);
|
|
||||||
this.hass.loadBackendTranslation("triggers");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-sortable
|
<ha-sortable
|
||||||
@@ -106,7 +85,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
|||||||
.first=${idx === 0}
|
.first=${idx === 0}
|
||||||
.last=${idx === this.triggers.length - 1}
|
.last=${idx === this.triggers.length - 1}
|
||||||
.trigger=${trg}
|
.trigger=${trg}
|
||||||
.triggerDescriptions=${this._triggerDescriptions}
|
|
||||||
@duplicate=${this._duplicateTrigger}
|
@duplicate=${this._duplicateTrigger}
|
||||||
@insert-after=${this._insertAfter}
|
@insert-after=${this._insertAfter}
|
||||||
@move-down=${this._moveDown}
|
@move-down=${this._moveDown}
|
||||||
@@ -178,10 +156,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
|||||||
let triggers: Trigger[];
|
let triggers: Trigger[];
|
||||||
if (value === PASTE_VALUE) {
|
if (value === PASTE_VALUE) {
|
||||||
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
|
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
|
||||||
} else if (isDynamic(value)) {
|
|
||||||
triggers = this.triggers.concat({
|
|
||||||
trigger: getValueFromDynamic(value),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
|
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
|
||||||
const elClass = customElements.get(
|
const elClass = customElements.get(
|
||||||
|
|||||||
@@ -1,416 +0,0 @@
|
|||||||
import { mdiHelpCircle } from "@mdi/js";
|
|
||||||
import type { PropertyValues } from "lit";
|
|
||||||
import { css, html, LitElement, nothing } 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 "../../../../../components/ha-checkbox";
|
|
||||||
import "../../../../../components/ha-selector/ha-selector";
|
|
||||||
import "../../../../../components/ha-settings-row";
|
|
||||||
import type { PlatformTrigger } from "../../../../../data/automation";
|
|
||||||
import type { IntegrationManifest } from "../../../../../data/integration";
|
|
||||||
import { fetchIntegrationManifest } from "../../../../../data/integration";
|
|
||||||
import type { TargetSelector } from "../../../../../data/selector";
|
|
||||||
import {
|
|
||||||
getTriggerDomain,
|
|
||||||
getTriggerObjectId,
|
|
||||||
type TriggerDescription,
|
|
||||||
} from "../../../../../data/trigger";
|
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
|
||||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
|
||||||
|
|
||||||
const showOptionalToggle = (field: TriggerDescription["fields"][string]) =>
|
|
||||||
field.selector &&
|
|
||||||
!field.required &&
|
|
||||||
!("boolean" in field.selector && field.default);
|
|
||||||
|
|
||||||
@customElement("ha-automation-trigger-platform")
|
|
||||||
export class HaPlatformTrigger extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public trigger!: PlatformTrigger;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public description?: TriggerDescription;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
|
||||||
|
|
||||||
@state() private _checkedKeys = new Set();
|
|
||||||
|
|
||||||
@state() private _manifest?: IntegrationManifest;
|
|
||||||
|
|
||||||
public static get defaultConfig(): PlatformTrigger {
|
|
||||||
return { trigger: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
|
||||||
super.willUpdate(changedProperties);
|
|
||||||
if (!this.hasUpdated) {
|
|
||||||
this.hass.loadBackendTranslation("triggers");
|
|
||||||
this.hass.loadBackendTranslation("selector");
|
|
||||||
}
|
|
||||||
if (!changedProperties.has("trigger")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const oldValue = changedProperties.get("trigger") as
|
|
||||||
| undefined
|
|
||||||
| this["trigger"];
|
|
||||||
|
|
||||||
// Fetch the manifest if we have a trigger selected and the trigger domain changed.
|
|
||||||
// If no trigger is selected, clear the manifest.
|
|
||||||
if (this.trigger?.trigger) {
|
|
||||||
const domain = getTriggerDomain(this.trigger.trigger);
|
|
||||||
|
|
||||||
const oldDomain = getTriggerDomain(oldValue?.trigger || "");
|
|
||||||
|
|
||||||
if (domain !== oldDomain) {
|
|
||||||
this._fetchManifest(domain);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._manifest = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
const domain = getTriggerDomain(this.trigger.trigger);
|
|
||||||
const triggerName = getTriggerObjectId(this.trigger.trigger);
|
|
||||||
|
|
||||||
const description = this.hass.localize(
|
|
||||||
`component.${domain}.triggers.${triggerName}.description`
|
|
||||||
);
|
|
||||||
|
|
||||||
const triggerDesc = this.description;
|
|
||||||
|
|
||||||
const shouldRenderDataYaml = !triggerDesc?.fields;
|
|
||||||
|
|
||||||
const hasOptional = Boolean(
|
|
||||||
triggerDesc?.fields &&
|
|
||||||
Object.values(triggerDesc.fields).some((field) =>
|
|
||||||
showOptionalToggle(field)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="description">
|
|
||||||
${description ? html`<p>${description}</p>` : nothing}
|
|
||||||
${this._manifest
|
|
||||||
? html`<a
|
|
||||||
href=${this._manifest.is_built_in
|
|
||||||
? documentationUrl(
|
|
||||||
this.hass,
|
|
||||||
`/integrations/${this._manifest.domain}`
|
|
||||||
)
|
|
||||||
: this._manifest.documentation}
|
|
||||||
title=${this.hass.localize(
|
|
||||||
"ui.components.service-control.integration_doc"
|
|
||||||
)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiHelpCircle}
|
|
||||||
class="help-icon"
|
|
||||||
></ha-icon-button>
|
|
||||||
</a>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
${triggerDesc && "target" in triggerDesc
|
|
||||||
? html`<ha-settings-row narrow>
|
|
||||||
${hasOptional
|
|
||||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
|
||||||
: nothing}
|
|
||||||
<span slot="heading"
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.components.service-control.target"
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
<span slot="description"
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.components.service-control.target_secondary"
|
|
||||||
)}</span
|
|
||||||
><ha-selector
|
|
||||||
.hass=${this.hass}
|
|
||||||
.selector=${this._targetSelector(triggerDesc.target)}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
@value-changed=${this._targetChanged}
|
|
||||||
.value=${this.trigger?.target}
|
|
||||||
></ha-selector
|
|
||||||
></ha-settings-row>`
|
|
||||||
: nothing}
|
|
||||||
${shouldRenderDataYaml
|
|
||||||
? html`<ha-yaml-editor
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.service-control.action_data"
|
|
||||||
)}
|
|
||||||
.name=${"data"}
|
|
||||||
.readOnly=${this.disabled}
|
|
||||||
.defaultValue=${this.trigger?.options}
|
|
||||||
@value-changed=${this._dataChanged}
|
|
||||||
></ha-yaml-editor>`
|
|
||||||
: Object.entries(triggerDesc.fields).map(([fieldName, dataField]) =>
|
|
||||||
this._renderField(
|
|
||||||
fieldName,
|
|
||||||
dataField,
|
|
||||||
hasOptional,
|
|
||||||
domain,
|
|
||||||
triggerName
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _targetSelector = memoizeOne(
|
|
||||||
(targetSelector: TargetSelector["target"] | null | undefined) =>
|
|
||||||
targetSelector ? { target: { ...targetSelector } } : { target: {} }
|
|
||||||
);
|
|
||||||
|
|
||||||
private _renderField = (
|
|
||||||
fieldName: string,
|
|
||||||
dataField: TriggerDescription["fields"][string],
|
|
||||||
hasOptional: boolean,
|
|
||||||
domain: string | undefined,
|
|
||||||
triggerName: string | undefined
|
|
||||||
) => {
|
|
||||||
const selector = dataField?.selector ?? { text: null };
|
|
||||||
|
|
||||||
const showOptional = showOptionalToggle(dataField);
|
|
||||||
|
|
||||||
return dataField.selector
|
|
||||||
? html`<ha-settings-row narrow>
|
|
||||||
${!showOptional
|
|
||||||
? hasOptional
|
|
||||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
|
||||||
: nothing
|
|
||||||
: html`<ha-checkbox
|
|
||||||
.key=${fieldName}
|
|
||||||
.checked=${this._checkedKeys.has(fieldName) ||
|
|
||||||
(this.trigger?.options &&
|
|
||||||
this.trigger.options[fieldName] !== undefined)}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
@change=${this._checkboxChanged}
|
|
||||||
slot="prefix"
|
|
||||||
></ha-checkbox>`}
|
|
||||||
<span slot="heading"
|
|
||||||
>${this.hass.localize(
|
|
||||||
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.name`
|
|
||||||
) || triggerName}</span
|
|
||||||
>
|
|
||||||
<span slot="description"
|
|
||||||
>${this.hass.localize(
|
|
||||||
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.description`
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
<ha-selector
|
|
||||||
.disabled=${this.disabled ||
|
|
||||||
(showOptional &&
|
|
||||||
!this._checkedKeys.has(fieldName) &&
|
|
||||||
(!this.trigger?.options ||
|
|
||||||
this.trigger.options[fieldName] === undefined))}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.selector=${selector}
|
|
||||||
.context=${this._generateContext(dataField)}
|
|
||||||
.key=${fieldName}
|
|
||||||
@value-changed=${this._dataChanged}
|
|
||||||
.value=${this.trigger?.options
|
|
||||||
? this.trigger.options[fieldName]
|
|
||||||
: undefined}
|
|
||||||
.placeholder=${dataField.default}
|
|
||||||
.localizeValue=${this._localizeValueCallback}
|
|
||||||
></ha-selector>
|
|
||||||
</ha-settings-row>`
|
|
||||||
: nothing;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _generateContext(
|
|
||||||
field: TriggerDescription["fields"][string]
|
|
||||||
): Record<string, any> | undefined {
|
|
||||||
if (!field.context) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = {};
|
|
||||||
for (const [context_key, data_key] of Object.entries(field.context)) {
|
|
||||||
context[context_key] =
|
|
||||||
data_key === "target"
|
|
||||||
? this.trigger.target
|
|
||||||
: this.trigger.options?.[data_key];
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dataChanged(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
if (ev.detail.isValid === false) {
|
|
||||||
// Don't clear an object selector that returns invalid YAML
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const key = (ev.currentTarget as any).key;
|
|
||||||
const value = ev.detail.value;
|
|
||||||
if (
|
|
||||||
this.trigger?.options?.[key] === value ||
|
|
||||||
((!this.trigger?.options || !(key in this.trigger.options)) &&
|
|
||||||
(value === "" || value === undefined))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = { ...this.trigger?.options, [key]: value };
|
|
||||||
|
|
||||||
if (
|
|
||||||
value === "" ||
|
|
||||||
value === undefined ||
|
|
||||||
(typeof value === "object" && !Object.keys(value).length)
|
|
||||||
) {
|
|
||||||
delete options[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent(this, "value-changed", {
|
|
||||||
value: {
|
|
||||||
...this.trigger,
|
|
||||||
options,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _targetChanged(ev: CustomEvent): void {
|
|
||||||
ev.stopPropagation();
|
|
||||||
fireEvent(this, "value-changed", {
|
|
||||||
value: {
|
|
||||||
...this.trigger,
|
|
||||||
target: ev.detail.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _checkboxChanged(ev) {
|
|
||||||
const checked = ev.currentTarget.checked;
|
|
||||||
const key = ev.currentTarget.key;
|
|
||||||
let options;
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
this._checkedKeys.add(key);
|
|
||||||
const field =
|
|
||||||
this.description &&
|
|
||||||
Object.entries(this.description).find(([k, _value]) => k === key)?.[1];
|
|
||||||
let defaultValue = field?.default;
|
|
||||||
|
|
||||||
if (
|
|
||||||
defaultValue == null &&
|
|
||||||
field?.selector &&
|
|
||||||
"constant" in field.selector
|
|
||||||
) {
|
|
||||||
defaultValue = field.selector.constant?.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
defaultValue == null &&
|
|
||||||
field?.selector &&
|
|
||||||
"boolean" in field.selector
|
|
||||||
) {
|
|
||||||
defaultValue = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultValue != null) {
|
|
||||||
options = {
|
|
||||||
...this.trigger?.options,
|
|
||||||
[key]: defaultValue,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._checkedKeys.delete(key);
|
|
||||||
options = { ...this.trigger?.options };
|
|
||||||
delete options[key];
|
|
||||||
}
|
|
||||||
if (options) {
|
|
||||||
fireEvent(this, "value-changed", {
|
|
||||||
value: {
|
|
||||||
...this.trigger,
|
|
||||||
options,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.requestUpdate("_checkedKeys");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _localizeValueCallback = (key: string) => {
|
|
||||||
if (!this.trigger?.trigger) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return this.hass.localize(
|
|
||||||
`component.${computeDomain(this.trigger.trigger)}.selector.${key}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private async _fetchManifest(integration: string) {
|
|
||||||
this._manifest = undefined;
|
|
||||||
try {
|
|
||||||
this._manifest = await fetchIntegrationManifest(this.hass, integration);
|
|
||||||
} catch (_err: any) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(`Unable to fetch integration manifest for ${integration}`);
|
|
||||||
// Ignore if loading manifest fails. Probably bad JSON in manifest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
ha-settings-row {
|
|
||||||
padding: 0 var(--ha-space-4);
|
|
||||||
}
|
|
||||||
ha-settings-row[narrow] {
|
|
||||||
padding-bottom: var(--ha-space-2);
|
|
||||||
}
|
|
||||||
ha-settings-row {
|
|
||||||
--settings-row-content-width: 100%;
|
|
||||||
--settings-row-prefix-display: contents;
|
|
||||||
border-top: var(
|
|
||||||
--service-control-items-border-top,
|
|
||||||
1px solid var(--divider-color)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ha-service-picker,
|
|
||||||
ha-entity-picker,
|
|
||||||
ha-yaml-editor {
|
|
||||||
display: block;
|
|
||||||
margin: 0 var(--ha-space-4);
|
|
||||||
}
|
|
||||||
ha-yaml-editor {
|
|
||||||
padding: var(--ha-space-4) 0;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0 var(--ha-space-4);
|
|
||||||
padding: var(--ha-space-4) 0;
|
|
||||||
}
|
|
||||||
:host([hide-picker]) p {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
.checkbox-spacer {
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
ha-checkbox {
|
|
||||||
margin-left: calc(var(--ha-space-4) * -1);
|
|
||||||
margin-inline-start: calc(var(--ha-space-4) * -1);
|
|
||||||
margin-inline-end: initial;
|
|
||||||
}
|
|
||||||
.help-icon {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
.description {
|
|
||||||
justify-content: space-between;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-right: 2px;
|
|
||||||
padding-inline-end: 2px;
|
|
||||||
padding-inline-start: initial;
|
|
||||||
}
|
|
||||||
.description p {
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-automation-trigger-platform": HaPlatformTrigger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,10 +16,8 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
BarSeriesOption,
|
BarSeriesOption,
|
||||||
CallbackDataParams,
|
CallbackDataParams,
|
||||||
LineSeriesOption,
|
|
||||||
TopLevelFormatterParams,
|
TopLevelFormatterParams,
|
||||||
} from "echarts/types/dist/shared";
|
} from "echarts/types/dist/shared";
|
||||||
import type { LineDataItemOption } from "echarts/types/src/chart/line/LineSeries";
|
|
||||||
import type { FrontendLocaleData } from "../../../../../data/translation";
|
import type { FrontendLocaleData } from "../../../../../data/translation";
|
||||||
import { formatNumber } from "../../../../../common/number/format_number";
|
import { formatNumber } from "../../../../../common/number/format_number";
|
||||||
import {
|
import {
|
||||||
@@ -172,10 +170,11 @@ function formatTooltip(
|
|||||||
compare
|
compare
|
||||||
? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `
|
? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `
|
||||||
: ""
|
: ""
|
||||||
}${formatTime(date, locale, config)}`;
|
}${formatTime(date, locale, config)} – ${formatTime(
|
||||||
if (params[0].componentSubType === "bar") {
|
addHours(date, 1),
|
||||||
period += ` – ${formatTime(addHours(date, 1), locale, config)}`;
|
locale,
|
||||||
}
|
config
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
|
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
|
||||||
|
|
||||||
@@ -282,35 +281,6 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fillLineGaps(datasets: LineSeriesOption[]) {
|
|
||||||
const buckets = Array.from(
|
|
||||||
new Set(
|
|
||||||
datasets
|
|
||||||
.map((dataset) =>
|
|
||||||
dataset.data!.map((datapoint) => Number(datapoint![0]))
|
|
||||||
)
|
|
||||||
.flat()
|
|
||||||
)
|
|
||||||
).sort((a, b) => a - b);
|
|
||||||
buckets.forEach((bucket, index) => {
|
|
||||||
for (let i = datasets.length - 1; i >= 0; i--) {
|
|
||||||
const dataPoint = datasets[i].data![index];
|
|
||||||
const item: LineDataItemOption =
|
|
||||||
dataPoint && typeof dataPoint === "object" && "value" in dataPoint
|
|
||||||
? dataPoint
|
|
||||||
: ({ value: dataPoint } as LineDataItemOption);
|
|
||||||
const x = item.value?.[0];
|
|
||||||
if (x === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (Number(x) !== bucket) {
|
|
||||||
datasets[i].data?.splice(index, 0, [bucket, 0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return datasets;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCompareTransform(start: Date, compareStart?: Date) {
|
export function getCompareTransform(start: Date, compareStart?: Date) {
|
||||||
if (!compareStart) {
|
if (!compareStart) {
|
||||||
return (ts: Date) => ts;
|
return (ts: Date) => ts;
|
||||||
|
|||||||
@@ -1,335 +0,0 @@
|
|||||||
import { endOfToday, isToday, startOfToday } from "date-fns";
|
|
||||||
import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import type { PropertyValues } from "lit";
|
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { classMap } from "lit/directives/class-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import type { LineSeriesOption } from "echarts/charts";
|
|
||||||
import { graphic } from "echarts";
|
|
||||||
import "../../../../components/chart/ha-chart-base";
|
|
||||||
import "../../../../components/ha-card";
|
|
||||||
import type { EnergyData } from "../../../../data/energy";
|
|
||||||
import { getEnergyDataCollection } from "../../../../data/energy";
|
|
||||||
import type { StatisticValue } from "../../../../data/recorder";
|
|
||||||
import type { FrontendLocaleData } from "../../../../data/translation";
|
|
||||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
import type { LovelaceCard } from "../../types";
|
|
||||||
import type { PowerSourcesGraphCardConfig } from "../types";
|
|
||||||
import { hasConfigChanged } from "../../common/has-changed";
|
|
||||||
import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options";
|
|
||||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
|
||||||
import { hex2rgb } from "../../../../common/color/convert-color";
|
|
||||||
|
|
||||||
@customElement("hui-power-sources-graph-card")
|
|
||||||
export class HuiPowerSourcesGraphCard
|
|
||||||
extends SubscribeMixin(LitElement)
|
|
||||||
implements LovelaceCard
|
|
||||||
{
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@state() private _config?: PowerSourcesGraphCardConfig;
|
|
||||||
|
|
||||||
@state() private _chartData: LineSeriesOption[] = [];
|
|
||||||
|
|
||||||
@state() private _start = startOfToday();
|
|
||||||
|
|
||||||
@state() private _end = endOfToday();
|
|
||||||
|
|
||||||
@state() private _compareStart?: Date;
|
|
||||||
|
|
||||||
@state() private _compareEnd?: Date;
|
|
||||||
|
|
||||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
|
||||||
return [
|
|
||||||
getEnergyDataCollection(this.hass, {
|
|
||||||
key: this._config?.collection_key,
|
|
||||||
}).subscribe((data) => this._getStatistics(data)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCardSize(): Promise<number> | number {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setConfig(config: PowerSourcesGraphCardConfig): void {
|
|
||||||
this._config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
|
||||||
return (
|
|
||||||
hasConfigChanged(this, changedProps) ||
|
|
||||||
changedProps.size > 1 ||
|
|
||||||
!changedProps.has("hass")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this.hass || !this._config) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-card>
|
|
||||||
${this._config.title
|
|
||||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
|
||||||
: ""}
|
|
||||||
<div
|
|
||||||
class="content ${classMap({
|
|
||||||
"has-header": !!this._config.title,
|
|
||||||
})}"
|
|
||||||
>
|
|
||||||
<ha-chart-base
|
|
||||||
.hass=${this.hass}
|
|
||||||
.data=${this._chartData}
|
|
||||||
.options=${this._createOptions(
|
|
||||||
this._start,
|
|
||||||
this._end,
|
|
||||||
this.hass.locale,
|
|
||||||
this.hass.config,
|
|
||||||
this._compareStart,
|
|
||||||
this._compareEnd
|
|
||||||
)}
|
|
||||||
></ha-chart-base>
|
|
||||||
${!this._chartData.some((dataset) => dataset.data!.length)
|
|
||||||
? html`<div class="no-data">
|
|
||||||
${isToday(this._start)
|
|
||||||
? this.hass.localize("ui.panel.lovelace.cards.energy.no_data")
|
|
||||||
: this.hass.localize(
|
|
||||||
"ui.panel.lovelace.cards.energy.no_data_period"
|
|
||||||
)}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createOptions = memoizeOne(
|
|
||||||
(
|
|
||||||
start: Date,
|
|
||||||
end: Date,
|
|
||||||
locale: FrontendLocaleData,
|
|
||||||
config: HassConfig,
|
|
||||||
compareStart?: Date,
|
|
||||||
compareEnd?: Date
|
|
||||||
): ECOption =>
|
|
||||||
getCommonOptions(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
locale,
|
|
||||||
config,
|
|
||||||
"kW",
|
|
||||||
compareStart,
|
|
||||||
compareEnd
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
|
||||||
const datasets: LineSeriesOption[] = [];
|
|
||||||
|
|
||||||
const statIds = {
|
|
||||||
solar: {
|
|
||||||
stats: [] as string[],
|
|
||||||
color: "--energy-solar-color",
|
|
||||||
name: this.hass.localize(
|
|
||||||
"ui.panel.lovelace.cards.energy.power_graph.solar"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
stats: [] as string[],
|
|
||||||
color: "--energy-grid-consumption-color",
|
|
||||||
name: this.hass.localize(
|
|
||||||
"ui.panel.lovelace.cards.energy.power_graph.grid"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
battery: {
|
|
||||||
stats: [] as string[],
|
|
||||||
color: "--energy-battery-out-color",
|
|
||||||
name: this.hass.localize(
|
|
||||||
"ui.panel.lovelace.cards.energy.power_graph.battery"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const computedStyles = getComputedStyle(this);
|
|
||||||
|
|
||||||
for (const source of energyData.prefs.energy_sources) {
|
|
||||||
if (source.type === "solar") {
|
|
||||||
if (source.stat_rate) {
|
|
||||||
statIds.solar.stats.push(source.stat_rate);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.type === "battery") {
|
|
||||||
if (source.stat_rate) {
|
|
||||||
statIds.battery.stats.push(source.stat_rate);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.type === "grid" && source.power) {
|
|
||||||
statIds.grid.stats.push(...source.power.map((p) => p.stat_rate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const commonSeriesOptions: LineSeriesOption = {
|
|
||||||
type: "line",
|
|
||||||
smooth: 0.4,
|
|
||||||
smoothMonotone: "x",
|
|
||||||
lineStyle: {
|
|
||||||
width: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(statIds).forEach((key, keyIndex) => {
|
|
||||||
if (statIds[key].stats.length) {
|
|
||||||
const colorHex = computedStyles.getPropertyValue(statIds[key].color);
|
|
||||||
const rgb = hex2rgb(colorHex);
|
|
||||||
// Echarts is supposed to handle that but it is bugged when you use it together with stacking.
|
|
||||||
// The interpolation breaks the stacking, so this positive/negative is a workaround
|
|
||||||
const { positive, negative } = this._processData(
|
|
||||||
statIds[key].stats.map((id: string) => energyData.stats[id] ?? [])
|
|
||||||
);
|
|
||||||
datasets.push({
|
|
||||||
...commonSeriesOptions,
|
|
||||||
id: key,
|
|
||||||
name: statIds[key].name,
|
|
||||||
color: colorHex,
|
|
||||||
stack: "positive",
|
|
||||||
areaStyle: {
|
|
||||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
data: positive,
|
|
||||||
z: 3 - keyIndex, // draw in reverse order so 0 value lines are overwritten
|
|
||||||
});
|
|
||||||
if (key !== "solar") {
|
|
||||||
datasets.push({
|
|
||||||
...commonSeriesOptions,
|
|
||||||
id: `${key}-negative`,
|
|
||||||
name: statIds[key].name,
|
|
||||||
color: colorHex,
|
|
||||||
stack: "negative",
|
|
||||||
areaStyle: {
|
|
||||||
color: new graphic.LinearGradient(0, 1, 0, 0, [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
data: negative,
|
|
||||||
z: 4 - keyIndex, // draw in reverse order but above positive series
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._start = energyData.start;
|
|
||||||
this._end = energyData.end || endOfToday();
|
|
||||||
|
|
||||||
this._chartData = fillLineGaps(datasets);
|
|
||||||
|
|
||||||
const usageData: NonNullable<LineSeriesOption["data"]> = [];
|
|
||||||
this._chartData[0]?.data!.forEach((item, i) => {
|
|
||||||
// fillLineGaps ensures all datasets have the same x values
|
|
||||||
const x =
|
|
||||||
typeof item === "object" && "value" in item!
|
|
||||||
? item.value![0]
|
|
||||||
: item![0];
|
|
||||||
usageData[i] = [x, 0];
|
|
||||||
this._chartData.forEach((dataset) => {
|
|
||||||
const y =
|
|
||||||
typeof dataset.data![i] === "object" && "value" in dataset.data![i]!
|
|
||||||
? dataset.data![i].value![1]
|
|
||||||
: dataset.data![i]![1];
|
|
||||||
usageData[i]![1] += y as number;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this._chartData.push({
|
|
||||||
...commonSeriesOptions,
|
|
||||||
id: "usage",
|
|
||||||
name: this.hass.localize(
|
|
||||||
"ui.panel.lovelace.cards.energy.power_graph.usage"
|
|
||||||
),
|
|
||||||
color: computedStyles.getPropertyValue("--primary-color"),
|
|
||||||
lineStyle: { width: 2 },
|
|
||||||
data: usageData,
|
|
||||||
z: 5,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _processData(stats: StatisticValue[][]) {
|
|
||||||
const data: Record<number, number[]> = {};
|
|
||||||
stats.forEach((statSet) => {
|
|
||||||
statSet.forEach((point) => {
|
|
||||||
if (point.mean == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const x = (point.start + point.end) / 2;
|
|
||||||
data[x] = [...(data[x] ?? []), point.mean];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const positive: [number, number][] = [];
|
|
||||||
const negative: [number, number][] = [];
|
|
||||||
Object.entries(data).forEach(([x, y]) => {
|
|
||||||
const ts = Number(x);
|
|
||||||
const meanY = y.reduce((a, b) => a + b, 0) / y.length;
|
|
||||||
positive.push([ts, Math.max(0, meanY)]);
|
|
||||||
negative.push([ts, Math.min(0, meanY)]);
|
|
||||||
});
|
|
||||||
return { positive, negative };
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
ha-card {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.card-header {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
padding: var(--ha-space-4);
|
|
||||||
}
|
|
||||||
.has-header {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
.no-data {
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20%;
|
|
||||||
margin-left: var(--ha-space-8);
|
|
||||||
margin-inline-start: var(--ha-space-8);
|
|
||||||
margin-inline-end: initial;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"hui-power-sources-graph-card": HuiPowerSourcesGraphCard;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -230,11 +230,6 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
|
|||||||
group_by_area?: boolean;
|
group_by_area?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
|
|
||||||
type: "power-sources-graph";
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
||||||
type: "entity-filter";
|
type: "entity-filter";
|
||||||
entities: (EntityFilterEntityConfig | string)[];
|
entities: (EntityFilterEntityConfig | string)[];
|
||||||
|
|||||||
@@ -66,8 +66,6 @@ const LAZY_LOAD_TYPES = {
|
|||||||
"energy-usage-graph": () =>
|
"energy-usage-graph": () =>
|
||||||
import("../cards/energy/hui-energy-usage-graph-card"),
|
import("../cards/energy/hui-energy-usage-graph-card"),
|
||||||
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
||||||
"power-sources-graph": () =>
|
|
||||||
import("../cards/energy/hui-power-sources-graph-card"),
|
|
||||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||||
error: () => import("../cards/hui-error-card"),
|
error: () => import("../cards/hui-error-card"),
|
||||||
"home-summary": () => import("../cards/hui-home-summary-card"),
|
"home-summary": () => import("../cards/hui-home-summary-card"),
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const NON_STANDARD_URLS = {
|
|||||||
"energy-devices-graph": "energy/#devices-energy-graph",
|
"energy-devices-graph": "energy/#devices-energy-graph",
|
||||||
"energy-devices-detail-graph": "energy/#detail-devices-energy-graph",
|
"energy-devices-detail-graph": "energy/#detail-devices-energy-graph",
|
||||||
"energy-sankey": "energy/#sankey-energy-graph",
|
"energy-sankey": "energy/#sankey-energy-graph",
|
||||||
"power-sources-graph": "energy/#power-sources-graph",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCardDocumentationURL = (
|
export const getCardDocumentationURL = (
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ export class HUIViewBackground extends LitElement {
|
|||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||||
super.willUpdate(changedProperties);
|
super.willUpdate(changedProperties);
|
||||||
let applyTheme = false;
|
|
||||||
if (changedProperties.has("hass") && this.hass) {
|
if (changedProperties.has("hass") && this.hass) {
|
||||||
const oldHass = changedProperties.get("hass");
|
const oldHass = changedProperties.get("hass");
|
||||||
if (
|
if (
|
||||||
@@ -117,18 +116,16 @@ export class HUIViewBackground extends LitElement {
|
|||||||
this.hass.themes !== oldHass.themes ||
|
this.hass.themes !== oldHass.themes ||
|
||||||
this.hass.selectedTheme !== oldHass.selectedTheme
|
this.hass.selectedTheme !== oldHass.selectedTheme
|
||||||
) {
|
) {
|
||||||
applyTheme = true;
|
this._applyTheme();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperties.has("background")) {
|
if (changedProperties.has("background")) {
|
||||||
applyTheme = true;
|
this._applyTheme();
|
||||||
this._fetchMedia();
|
this._fetchMedia();
|
||||||
}
|
}
|
||||||
if (changedProperties.has("resolvedImage")) {
|
if (changedProperties.has("resolvedImage")) {
|
||||||
applyTheme = true;
|
|
||||||
}
|
|
||||||
if (applyTheme) {
|
|
||||||
this._applyTheme();
|
this._applyTheme();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { haStyleDialog } from "../../resources/styles";
|
|||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import type { TodoItemEditDialogParams } from "./show-dialog-todo-item-editor";
|
import type { TodoItemEditDialogParams } from "./show-dialog-todo-item-editor";
|
||||||
import { supportsMarkdownHelper } from "../../common/translations/markdown_support";
|
import { supportsMarkdownHelper } from "../../common/translations/markdown_support";
|
||||||
import { formatShortDateTimeWithConditionalYear } from "../../common/datetime/format_date_time";
|
|
||||||
|
|
||||||
@customElement("dialog-todo-item-editor")
|
@customElement("dialog-todo-item-editor")
|
||||||
class DialogTodoItemEditor extends LitElement {
|
class DialogTodoItemEditor extends LitElement {
|
||||||
@@ -42,8 +41,6 @@ class DialogTodoItemEditor extends LitElement {
|
|||||||
|
|
||||||
@state() private _due?: Date;
|
@state() private _due?: Date;
|
||||||
|
|
||||||
@state() private _completedTime?: Date;
|
|
||||||
|
|
||||||
@state() private _checked = false;
|
@state() private _checked = false;
|
||||||
|
|
||||||
@state() private _hasTime = false;
|
@state() private _hasTime = false;
|
||||||
@@ -68,9 +65,6 @@ class DialogTodoItemEditor extends LitElement {
|
|||||||
this._checked = entry.status === TodoItemStatus.Completed;
|
this._checked = entry.status === TodoItemStatus.Completed;
|
||||||
this._summary = entry.summary;
|
this._summary = entry.summary;
|
||||||
this._description = entry.description || "";
|
this._description = entry.description || "";
|
||||||
this._completedTime = entry.completed
|
|
||||||
? new Date(entry.completed)
|
|
||||||
: undefined;
|
|
||||||
this._hasTime = entry.due?.includes("T") || false;
|
this._hasTime = entry.due?.includes("T") || false;
|
||||||
this._due = entry.due
|
this._due = entry.due
|
||||||
? new Date(this._hasTime ? entry.due : `${entry.due}T00:00:00`)
|
? new Date(this._hasTime ? entry.due : `${entry.due}T00:00:00`)
|
||||||
@@ -144,17 +138,6 @@ class DialogTodoItemEditor extends LitElement {
|
|||||||
.disabled=${!canUpdate}
|
.disabled=${!canUpdate}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
</div>
|
</div>
|
||||||
${this._completedTime
|
|
||||||
? html`<div class="italic">
|
|
||||||
${this.hass.localize("ui.components.todo.item.completed_time", {
|
|
||||||
datetime: formatShortDateTimeWithConditionalYear(
|
|
||||||
this._completedTime,
|
|
||||||
this.hass.locale,
|
|
||||||
this.hass.config
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
${this._todoListSupportsFeature(
|
${this._todoListSupportsFeature(
|
||||||
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||||
)
|
)
|
||||||
@@ -472,9 +455,6 @@ class DialogTodoItemEditor extends LitElement {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,3 +199,23 @@ export const baseEntrypointStyles = css`
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const baseAnimationStyles = css`
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { css } from "lit";
|
|
||||||
|
|
||||||
export const animationStyles = css`
|
|
||||||
@keyframes fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-out {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scale {
|
|
||||||
from {
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -52,6 +52,8 @@ export const waColorStyles = css`
|
|||||||
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
|
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
|
||||||
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
|
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
|
||||||
|
|
||||||
|
--wa-color-text-quiet: var(--ha-color-text-secondary);
|
||||||
|
|
||||||
--wa-color-text-normal: var(--ha-color-text-primary);
|
--wa-color-text-normal: var(--ha-color-text-primary);
|
||||||
--wa-color-surface-default: var(--card-background-color);
|
--wa-color-surface-default: var(--card-background-color);
|
||||||
--wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
|
--wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
|
||||||
@@ -62,5 +64,7 @@ export const waColorStyles = css`
|
|||||||
|
|
||||||
--wa-focus-ring-color: var(--ha-color-neutral-60);
|
--wa-focus-ring-color: var(--ha-color-neutral-60);
|
||||||
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
|
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
--wa-color-text-normal: var(--ha-color-text-primary);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export const coreStyles = css`
|
|||||||
--ha-shadow-spread-sm: 0;
|
--ha-shadow-spread-sm: 0;
|
||||||
--ha-shadow-spread-md: 0;
|
--ha-shadow-spread-md: 0;
|
||||||
--ha-shadow-spread-lg: 0;
|
--ha-shadow-spread-lg: 0;
|
||||||
|
|
||||||
--ha-animation-base-duration: 350ms;
|
--ha-animation-base-duration: 350ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { fontStyles } from "../roboto";
|
import { fontStyles } from "../roboto";
|
||||||
import { animationStyles } from "./animations.globals";
|
|
||||||
import { colorDerivedVariables, colorStylesCollection } from "./color";
|
import { colorDerivedVariables, colorStylesCollection } from "./color";
|
||||||
import { coreDerivedVariables, coreStyles } from "./core.globals";
|
import { coreDerivedVariables, coreStyles } from "./core.globals";
|
||||||
import { mainDerivedVariables, mainStyles } from "./main.globals";
|
import { mainDerivedVariables, mainStyles } from "./main.globals";
|
||||||
@@ -18,7 +17,6 @@ export const themeStyles = [
|
|||||||
...colorStylesCollection,
|
...colorStylesCollection,
|
||||||
fontStyles.toString(),
|
fontStyles.toString(),
|
||||||
waMainStyles.toString(),
|
waMainStyles.toString(),
|
||||||
animationStyles.toString(),
|
|
||||||
].join("");
|
].join("");
|
||||||
|
|
||||||
export const derivedStyles = {
|
export const derivedStyles = {
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ export const waMainStyles = css`
|
|||||||
--wa-focus-ring-offset: 2px;
|
--wa-focus-ring-offset: 2px;
|
||||||
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
|
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
|
||||||
|
|
||||||
|
--wa-space-xs: var(--ha-space-2);
|
||||||
|
--wa-space-m: var(--ha-space-4);
|
||||||
--wa-space-l: var(--ha-space-6);
|
--wa-space-l: var(--ha-space-6);
|
||||||
--wa-space-xl: var(--ha-space-8);
|
--wa-space-xl: var(--ha-space-8);
|
||||||
|
|
||||||
--wa-form-control-padding-block: 0.75em;
|
--wa-form-control-padding-block: 0.75em;
|
||||||
|
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
|
||||||
|
|
||||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||||
|
--wa-transition-normal: 150ms;
|
||||||
--wa-transition-fast: 75ms;
|
--wa-transition-fast: 75ms;
|
||||||
--wa-transition-easing: ease;
|
--wa-transition-easing: ease;
|
||||||
|
|
||||||
@@ -28,6 +32,7 @@ export const waMainStyles = css`
|
|||||||
|
|
||||||
--wa-line-height-condensed: var(--ha-line-height-condensed);
|
--wa-line-height-condensed: var(--ha-line-height-condensed);
|
||||||
|
|
||||||
|
--wa-font-size-m: var(--ha-font-size-m);
|
||||||
--wa-shadow-s: var(--ha-box-shadow-s);
|
--wa-shadow-s: var(--ha-box-shadow-s);
|
||||||
--wa-shadow-m: var(--ha-box-shadow-m);
|
--wa-shadow-m: var(--ha-box-shadow-m);
|
||||||
--wa-shadow-l: var(--ha-box-shadow-l);
|
--wa-shadow-l: var(--ha-box-shadow-l);
|
||||||
|
|||||||
@@ -1130,7 +1130,6 @@
|
|||||||
"edit": "Edit item",
|
"edit": "Edit item",
|
||||||
"save": "Save item",
|
"save": "Save item",
|
||||||
"due": "Due date",
|
"due": "Due date",
|
||||||
"completed_time": "Completed { datetime }",
|
|
||||||
"not_all_required_fields": "Not all required fields are filled in",
|
"not_all_required_fields": "Not all required fields are filled in",
|
||||||
"confirm_delete": {
|
"confirm_delete": {
|
||||||
"delete": "Delete item",
|
"delete": "Delete item",
|
||||||
@@ -3990,6 +3989,9 @@
|
|||||||
"item_pasted": "{item} pasted",
|
"item_pasted": "{item} pasted",
|
||||||
"ctrl": "Ctrl",
|
"ctrl": "Ctrl",
|
||||||
"del": "Del",
|
"del": "Del",
|
||||||
|
"targets": "Targets",
|
||||||
|
"select_target": "Select a target",
|
||||||
|
"home": "Home",
|
||||||
"blocks": "Blocks",
|
"blocks": "Blocks",
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"name": "Triggers",
|
"name": "Triggers",
|
||||||
@@ -7162,12 +7164,6 @@
|
|||||||
"low_carbon_energy_consumed": "Low-carbon electricity consumed",
|
"low_carbon_energy_consumed": "Low-carbon electricity consumed",
|
||||||
"low_carbon_energy_not_calculated": "Consumed low-carbon electricity couldn't be calculated"
|
"low_carbon_energy_not_calculated": "Consumed low-carbon electricity couldn't be calculated"
|
||||||
},
|
},
|
||||||
"power_graph": {
|
|
||||||
"grid": "Grid",
|
|
||||||
"solar": "Solar",
|
|
||||||
"battery": "Battery",
|
|
||||||
"usage": "Used"
|
|
||||||
},
|
|
||||||
"energy_compare": {
|
"energy_compare": {
|
||||||
"info": "You are comparing the period {start} with the period {end}",
|
"info": "You are comparing the period {start} with the period {end}",
|
||||||
"compare_previous_year": "Compare previous year",
|
"compare_previous_year": "Compare previous year",
|
||||||
|
|||||||
@@ -1,7 +1,26 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { render } from "lit";
|
import { render } from "lit";
|
||||||
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
|
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
|
||||||
import { withViewTransition } from "../common/util/view-transition";
|
|
||||||
|
const removeElement = (
|
||||||
|
launchScreenElement: HTMLElement,
|
||||||
|
skipAnimation: boolean
|
||||||
|
) => {
|
||||||
|
if (skipAnimation) {
|
||||||
|
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
launchScreenElement.classList.add("removing");
|
||||||
|
|
||||||
|
const durationFromCss = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue("--ha-animation-base-duration")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
||||||
|
}, parseAnimationDuration(durationFromCss));
|
||||||
|
};
|
||||||
|
|
||||||
export const removeLaunchScreen = () => {
|
export const removeLaunchScreen = () => {
|
||||||
const launchScreenElement = document.getElementById("ha-launch-screen");
|
const launchScreenElement = document.getElementById("ha-launch-screen");
|
||||||
@@ -9,22 +28,14 @@ export const removeLaunchScreen = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
withViewTransition((viewTransitionAvailable: boolean) => {
|
if (document.startViewTransition) {
|
||||||
if (!viewTransitionAvailable) {
|
document.startViewTransition(() => {
|
||||||
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
removeElement(launchScreenElement, false);
|
||||||
return;
|
});
|
||||||
}
|
} else {
|
||||||
|
// Fallback: Direct removal without transition
|
||||||
launchScreenElement.classList.add("removing");
|
removeElement(launchScreenElement, true);
|
||||||
|
}
|
||||||
const durationFromCss = getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue("--ha-animation-base-duration")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
|
||||||
}, parseAnimationDuration(durationFromCss));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
|
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user