mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-14 21:40:27 +00:00
Compare commits
10 Commits
sec_pypi_p
...
renovate/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8df40edb7a | ||
|
|
332694549c | ||
|
|
396ddef722 | ||
|
|
d02804449a | ||
|
|
4ab24cdc72 | ||
|
|
81c27090d2 | ||
|
|
09bdfd3ad7 | ||
|
|
97e49f751c | ||
|
|
e0d241a2db | ||
|
|
83e065ae98 |
13
.github/workflows/release.yaml
vendored
13
.github/workflows/release.yaml
vendored
@@ -19,11 +19,8 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: pypi
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
id-token: write # For "Trusted Publisher" to PyPi
|
|
||||||
if: github.repository_owner == 'home-assistant'
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -49,18 +46,14 @@ jobs:
|
|||||||
run: ./script/translations_download
|
run: ./script/translations_download
|
||||||
env:
|
env:
|
||||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||||
|
|
||||||
- name: Build and release package
|
- name: Build and release package
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install build
|
python3 -m pip install twine build
|
||||||
|
export TWINE_USERNAME="__token__"
|
||||||
|
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||||
script/release
|
script/release
|
||||||
|
|
||||||
- name: Publish to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
|
||||||
with:
|
|
||||||
skip-existing: true
|
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -260,7 +260,6 @@ 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 { Trigger } from "../../../../src/data/automation";
|
import type { LegacyTrigger } 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: Trigger = {
|
const initialTrigger: LegacyTrigger = {
|
||||||
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": "22.21.1"
|
"node": "24.11.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# Pushes a new version to PyPi.
|
||||||
|
|
||||||
# Stop on errors
|
# Stop on errors
|
||||||
set -e
|
set -e
|
||||||
@@ -11,4 +12,5 @@ yarn install
|
|||||||
script/build_frontend
|
script/build_frontend
|
||||||
|
|
||||||
rm -rf dist home_assistant_frontend.egg-info
|
rm -rf dist home_assistant_frontend.egg-info
|
||||||
python3 -m build -q
|
python3 -m build
|
||||||
|
python3 -m twine upload dist/*.whl --skip-existing
|
||||||
|
|||||||
30
src/common/util/view-transition.ts
Normal file
30
src/common/util/view-transition.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, state } 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";
|
||||||
@@ -25,15 +27,29 @@ 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.selector.state?.entity_id ||
|
.entityId=${this._entityIds}
|
||||||
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}
|
||||||
@@ -50,8 +66,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
|||||||
return html`
|
return html`
|
||||||
<ha-entity-state-picker
|
<ha-entity-state-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entityId=${this.selector.state?.entity_id ||
|
.entityId=${this._entityIds}
|
||||||
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}
|
||||||
@@ -65,6 +80,24 @@ 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 {
|
||||||
|
|||||||
97
src/components/ha-trigger-icon.ts
Normal file
97
src/components/ha-trigger-icon.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
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: {},
|
||||||
serviceGroups: {},
|
dynamicGroups: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -117,14 +117,6 @@ 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",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
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";
|
||||||
@@ -12,11 +14,19 @@ 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 { WeekdayShort } from "../common/datetime/weekday";
|
import type { TriggerDescription } from "./trigger";
|
||||||
|
|
||||||
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;
|
||||||
@@ -86,6 +96,12 @@ 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 {
|
||||||
@@ -195,7 +211,7 @@ export interface CalendarTrigger extends BaseTrigger {
|
|||||||
offset: string;
|
offset: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Trigger =
|
export type LegacyTrigger =
|
||||||
| StateTrigger
|
| StateTrigger
|
||||||
| MqttTrigger
|
| MqttTrigger
|
||||||
| GeoLocationTrigger
|
| GeoLocationTrigger
|
||||||
@@ -212,8 +228,9 @@ export type Trigger =
|
|||||||
| TemplateTrigger
|
| TemplateTrigger
|
||||||
| EventTrigger
|
| EventTrigger
|
||||||
| DeviceTrigger
|
| DeviceTrigger
|
||||||
| CalendarTrigger
|
| CalendarTrigger;
|
||||||
| TriggerList;
|
|
||||||
|
export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
|
||||||
|
|
||||||
interface BaseCondition {
|
interface BaseCondition {
|
||||||
condition: string;
|
condition: string;
|
||||||
@@ -575,6 +592,7 @@ 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,8 +16,9 @@ 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, Trigger } from "./automation";
|
import type { Condition, ForDict, LegacyTrigger, Trigger } from "./automation";
|
||||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||||
import {
|
import {
|
||||||
localizeDeviceAutomationCondition,
|
localizeDeviceAutomationCondition,
|
||||||
@@ -25,8 +26,7 @@ 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 { isTriggerList } from "./trigger";
|
import { getTriggerDomain, getTriggerObjectId, 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,6 +121,37 @@ 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[] = [];
|
||||||
@@ -802,13 +833,7 @@ const tryDescribeTrigger = (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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 = (
|
||||||
|
|||||||
@@ -360,6 +360,35 @@ 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",
|
||||||
@@ -407,9 +436,10 @@ 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];
|
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
|
||||||
|
|
||||||
const dayDifference = differenceInDays(end || new Date(), start);
|
const dayDifference = differenceInDays(end || new Date(), start);
|
||||||
const period =
|
const period =
|
||||||
@@ -420,6 +450,8 @@ 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
|
||||||
@@ -441,6 +473,9 @@ 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,
|
||||||
@@ -451,6 +486,12 @@ 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",
|
||||||
@@ -557,6 +598,7 @@ const getEnergyData = async (
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
energyStats,
|
energyStats,
|
||||||
|
powerStats,
|
||||||
waterStats,
|
waterStats,
|
||||||
energyStatsCompare,
|
energyStatsCompare,
|
||||||
waterStatsCompare,
|
waterStatsCompare,
|
||||||
@@ -564,13 +606,14 @@ 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 };
|
const stats = { ...energyStats, ...waterStats, ...powerStats };
|
||||||
if (compare) {
|
if (compare) {
|
||||||
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ 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;
|
||||||
@@ -133,14 +134,19 @@ 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,
|
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
|
||||||
> {
|
> {
|
||||||
resources: Record<string, T>;
|
resources: Record<string, T>;
|
||||||
}
|
}
|
||||||
@@ -184,12 +190,22 @@ type ServiceIcons = Record<
|
|||||||
{ service: string; sections?: Record<string, string> }
|
{ service: string; sections?: Record<string, string> }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type IconCategory = "entity" | "entity_component" | "services";
|
type TriggerIcons = Record<
|
||||||
|
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>(
|
||||||
@@ -258,42 +274,59 @@ export const getComponentIcons = async (
|
|||||||
return resources.entity_component.resources.then((res) => res[domain]);
|
return resources.entity_component.resources.then((res) => res[domain]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServiceIcons = async (
|
export const getCategoryIcons = async <
|
||||||
|
T extends Exclude<IconCategory, "entity" | "entity_component">,
|
||||||
|
>(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
category: T,
|
||||||
domain?: string,
|
domain?: string,
|
||||||
force = false
|
force = false
|
||||||
): Promise<ServiceIcons | Record<string, ServiceIcons> | undefined> => {
|
): Promise<CategoryType[T] | Record<string, CategoryType[T]> | undefined> => {
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
if (!force && resources.services.all) {
|
if (!force && resources[category].all) {
|
||||||
return resources.services.all;
|
return resources[category].all as Promise<
|
||||||
|
Record<string, CategoryType[T]>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
resources.services.all = getHassIcons(hass, "services", domain).then(
|
resources[category].all = getHassIcons(hass, category).then((res) => {
|
||||||
(res) => {
|
resources[category].domains = res.resources as any;
|
||||||
resources.services.domains = res.resources;
|
return res?.resources as Record<string, CategoryType[T]>;
|
||||||
return res?.resources;
|
}) as any;
|
||||||
|
return resources[category].all as Promise<Record<string, CategoryType[T]>>;
|
||||||
}
|
}
|
||||||
);
|
if (!force && domain in resources[category].domains) {
|
||||||
return resources.services.all;
|
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
||||||
}
|
}
|
||||||
if (!force && domain in resources.services.domains) {
|
if (resources[category].all && !force) {
|
||||||
return resources.services.domains[domain];
|
await resources[category].all;
|
||||||
}
|
if (domain in resources[category].domains) {
|
||||||
if (resources.services.all && !force) {
|
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
||||||
await resources.services.all;
|
|
||||||
if (domain in resources.services.domains) {
|
|
||||||
return resources.services.domains[domain];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isComponentLoaded(hass, domain)) {
|
if (!isComponentLoaded(hass, domain)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const result = getHassIcons(hass, "services", domain);
|
const result = getHassIcons(hass, category, domain);
|
||||||
resources.services.domains[domain] = result.then(
|
resources[category].domains[domain] = result.then(
|
||||||
(res) => res?.resources[domain]
|
(res) => res?.resources[domain]
|
||||||
);
|
) as any;
|
||||||
return resources.services.domains[domain];
|
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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[]>();
|
||||||
|
|
||||||
@@ -473,6 +506,26 @@ 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
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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,7 +73,8 @@ 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,57 +1,20 @@
|
|||||||
import {
|
import { mdiMapClock, mdiShape } from "@mdi/js";
|
||||||
mdiAvTimer,
|
|
||||||
mdiCalendar,
|
|
||||||
mdiClockOutline,
|
|
||||||
mdiCodeBraces,
|
|
||||||
mdiDevices,
|
|
||||||
mdiFormatListBulleted,
|
|
||||||
mdiGestureDoubleTap,
|
|
||||||
mdiMapClock,
|
|
||||||
mdiMapMarker,
|
|
||||||
mdiMapMarkerRadius,
|
|
||||||
mdiMessageAlert,
|
|
||||||
mdiMicrophoneMessage,
|
|
||||||
mdiNfcVariant,
|
|
||||||
mdiNumeric,
|
|
||||||
mdiShape,
|
|
||||||
mdiStateMachine,
|
|
||||||
mdiSwapHorizontal,
|
|
||||||
mdiWeatherSunny,
|
|
||||||
mdiWebhook,
|
|
||||||
} from "@mdi/js";
|
|
||||||
|
|
||||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
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,
|
||||||
@@ -83,3 +46,33 @@ 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,12 +40,6 @@
|
|||||||
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;
|
||||||
@@ -62,12 +56,6 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@@ -92,6 +80,14 @@
|
|||||||
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,11 +14,13 @@ 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 type { AutomationClipboard } from "../../../../data/automation";
|
import {
|
||||||
|
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 {
|
||||||
@@ -217,9 +219,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 (isService(action)) {
|
} else if (isDynamic(action)) {
|
||||||
actions = this.actions.concat({
|
actions = this.actions.concat({
|
||||||
action: getService(action),
|
action: getValueFromDynamic(action),
|
||||||
metadata: {},
|
metadata: {},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
mdiPlus,
|
mdiPlus,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import {
|
import {
|
||||||
@@ -40,32 +41,39 @@ import "../../../components/ha-md-list";
|
|||||||
import type { HaMdList } from "../../../components/ha-md-list";
|
import type { HaMdList } from "../../../components/ha-md-list";
|
||||||
import "../../../components/ha-md-list-item";
|
import "../../../components/ha-md-list-item";
|
||||||
import "../../../components/ha-service-icon";
|
import "../../../components/ha-service-icon";
|
||||||
|
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
|
||||||
import "../../../components/ha-wa-dialog";
|
import "../../../components/ha-wa-dialog";
|
||||||
import "../../../components/search-input";
|
import "../../../components/search-input";
|
||||||
import {
|
import {
|
||||||
ACTION_BUILDING_BLOCKS_GROUP,
|
ACTION_BUILDING_BLOCKS_GROUP,
|
||||||
ACTION_COLLECTIONS,
|
ACTION_COLLECTIONS,
|
||||||
ACTION_ICONS,
|
ACTION_ICONS,
|
||||||
SERVICE_PREFIX,
|
|
||||||
getService,
|
|
||||||
isService,
|
|
||||||
} from "../../../data/action";
|
} from "../../../data/action";
|
||||||
import type {
|
import {
|
||||||
AutomationElementGroup,
|
DYNAMIC_PREFIX,
|
||||||
AutomationElementGroupCollection,
|
getValueFromDynamic,
|
||||||
|
isDynamic,
|
||||||
|
type AutomationElementGroup,
|
||||||
|
type AutomationElementGroupCollection,
|
||||||
} from "../../../data/automation";
|
} from "../../../data/automation";
|
||||||
import {
|
import {
|
||||||
CONDITION_BUILDING_BLOCKS_GROUP,
|
CONDITION_BUILDING_BLOCKS_GROUP,
|
||||||
CONDITION_COLLECTIONS,
|
CONDITION_COLLECTIONS,
|
||||||
CONDITION_ICONS,
|
CONDITION_ICONS,
|
||||||
} from "../../../data/condition";
|
} from "../../../data/condition";
|
||||||
import { getServiceIcons } from "../../../data/icons";
|
import { getServiceIcons, getTriggerIcons } from "../../../data/icons";
|
||||||
import type { IntegrationManifest } from "../../../data/integration";
|
import type { IntegrationManifest } from "../../../data/integration";
|
||||||
import {
|
import {
|
||||||
domainToName,
|
domainToName,
|
||||||
fetchIntegrationManifests,
|
fetchIntegrationManifests,
|
||||||
} from "../../../data/integration";
|
} from "../../../data/integration";
|
||||||
import { TRIGGER_COLLECTIONS, TRIGGER_ICONS } from "../../../data/trigger";
|
import type { TriggerDescriptions } from "../../../data/trigger";
|
||||||
|
import {
|
||||||
|
TRIGGER_COLLECTIONS,
|
||||||
|
getTriggerDomain,
|
||||||
|
getTriggerObjectId,
|
||||||
|
subscribeTriggers,
|
||||||
|
} from "../../../data/trigger";
|
||||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||||
import { HaFuse } from "../../../resources/fuse";
|
import { HaFuse } from "../../../resources/fuse";
|
||||||
@@ -111,7 +119,7 @@ const ENTITY_DOMAINS_OTHER = new Set([
|
|||||||
|
|
||||||
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
|
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
|
||||||
|
|
||||||
const ACTION_SERVICE_KEYWORDS = ["serviceGroups", "helpers", "other"];
|
const ACTION_SERVICE_KEYWORDS = ["dynamicGroups", "helpers", "other"];
|
||||||
|
|
||||||
@customElement("add-automation-element-dialog")
|
@customElement("add-automation-element-dialog")
|
||||||
class DialogAddAutomationElement
|
class DialogAddAutomationElement
|
||||||
@@ -142,6 +150,8 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
@state() private _narrow = false;
|
@state() private _narrow = false;
|
||||||
|
|
||||||
|
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
||||||
|
|
||||||
@query(".items ha-md-list ha-md-list-item")
|
@query(".items ha-md-list ha-md-list-item")
|
||||||
private _itemsListFirstElement?: HaMdList;
|
private _itemsListFirstElement?: HaMdList;
|
||||||
|
|
||||||
@@ -152,6 +162,8 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
private _removeKeyboardShortcuts?: () => void;
|
private _removeKeyboardShortcuts?: () => void;
|
||||||
|
|
||||||
|
private _unsub?: Promise<UnsubscribeFunc>;
|
||||||
|
|
||||||
public showDialog(params): void {
|
public showDialog(params): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
|
|
||||||
@@ -163,6 +175,17 @@ class DialogAddAutomationElement
|
|||||||
this._calculateUsedDomains();
|
this._calculateUsedDomains();
|
||||||
getServiceIcons(this.hass);
|
getServiceIcons(this.hass);
|
||||||
}
|
}
|
||||||
|
if (this._params?.type === "trigger") {
|
||||||
|
this.hass.loadBackendTranslation("triggers");
|
||||||
|
this._fetchManifests();
|
||||||
|
getTriggerIcons(this.hass);
|
||||||
|
this._unsub = subscribeTriggers(this.hass, (triggers) => {
|
||||||
|
this._triggerDescriptions = {
|
||||||
|
...this._triggerDescriptions,
|
||||||
|
...triggers,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
this._fullScreen = matchMedia(
|
this._fullScreen = matchMedia(
|
||||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||||
).matches;
|
).matches;
|
||||||
@@ -176,6 +199,10 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this.removeKeyboardShortcuts();
|
this.removeKeyboardShortcuts();
|
||||||
|
if (this._unsub) {
|
||||||
|
this._unsub.then((unsub) => unsub());
|
||||||
|
this._unsub = undefined;
|
||||||
|
}
|
||||||
if (this._params) {
|
if (this._params) {
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
@@ -317,6 +344,11 @@ class DialogAddAutomationElement
|
|||||||
);
|
);
|
||||||
|
|
||||||
const items = flattenGroups(groups).flat();
|
const items = flattenGroups(groups).flat();
|
||||||
|
if (type === "trigger") {
|
||||||
|
items.push(
|
||||||
|
...this._triggers(localize, this._triggerDescriptions, manifests)
|
||||||
|
);
|
||||||
|
}
|
||||||
if (type === "action") {
|
if (type === "action") {
|
||||||
items.push(...this._services(localize, services, manifests));
|
items.push(...this._services(localize, services, manifests));
|
||||||
}
|
}
|
||||||
@@ -339,6 +371,7 @@ class DialogAddAutomationElement
|
|||||||
domains: Set<string> | undefined,
|
domains: Set<string> | undefined,
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
services: HomeAssistant["services"],
|
services: HomeAssistant["services"],
|
||||||
|
triggerDescriptions: TriggerDescriptions,
|
||||||
manifests?: DomainManifestLookup
|
manifests?: DomainManifestLookup
|
||||||
): {
|
): {
|
||||||
titleKey?: LocalizeKeys;
|
titleKey?: LocalizeKeys;
|
||||||
@@ -362,7 +395,32 @@ class DialogAddAutomationElement
|
|||||||
services,
|
services,
|
||||||
manifests,
|
manifests,
|
||||||
domains,
|
domains,
|
||||||
collection.groups.serviceGroups
|
collection.groups.dynamicGroups
|
||||||
|
? undefined
|
||||||
|
: collection.groups.helpers
|
||||||
|
? "helper"
|
||||||
|
: "other"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
collectionGroups = collectionGroups.filter(
|
||||||
|
([key]) => !ACTION_SERVICE_KEYWORDS.includes(key)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
type === "trigger" &&
|
||||||
|
Object.keys(collection.groups).some((item) =>
|
||||||
|
ACTION_SERVICE_KEYWORDS.includes(item)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
groups.push(
|
||||||
|
...this._triggerGroups(
|
||||||
|
localize,
|
||||||
|
triggerDescriptions,
|
||||||
|
manifests,
|
||||||
|
domains,
|
||||||
|
collection.groups.dynamicGroups
|
||||||
? undefined
|
? undefined
|
||||||
: collection.groups.helpers
|
: collection.groups.helpers
|
||||||
? "helper"
|
? "helper"
|
||||||
@@ -429,10 +487,19 @@ class DialogAddAutomationElement
|
|||||||
services: HomeAssistant["services"],
|
services: HomeAssistant["services"],
|
||||||
manifests?: DomainManifestLookup
|
manifests?: DomainManifestLookup
|
||||||
): ListItem[] => {
|
): ListItem[] => {
|
||||||
if (type === "action" && isService(group)) {
|
if (type === "action" && isDynamic(group)) {
|
||||||
return this._services(localize, services, manifests, group);
|
return this._services(localize, services, manifests, group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "trigger" && isDynamic(group)) {
|
||||||
|
return this._triggers(
|
||||||
|
localize,
|
||||||
|
this._triggerDescriptions,
|
||||||
|
manifests,
|
||||||
|
group
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const groups = this._getGroups(type, group, collectionIndex);
|
const groups = this._getGroups(type, group, collectionIndex);
|
||||||
|
|
||||||
const result = Object.entries(groups).map(([key, options]) =>
|
const result = Object.entries(groups).map(([key, options]) =>
|
||||||
@@ -514,7 +581,7 @@ class DialogAddAutomationElement
|
|||||||
brand-fallback
|
brand-fallback
|
||||||
></ha-domain-icon>
|
></ha-domain-icon>
|
||||||
`,
|
`,
|
||||||
key: `${SERVICE_PREFIX}${domain}`,
|
key: `${DYNAMIC_PREFIX}${domain}`,
|
||||||
name: domainToName(localize, domain, manifest),
|
name: domainToName(localize, domain, manifest),
|
||||||
description: "",
|
description: "",
|
||||||
});
|
});
|
||||||
@@ -525,6 +592,102 @@ class DialogAddAutomationElement
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _triggerGroups = (
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
triggers: TriggerDescriptions,
|
||||||
|
manifests: DomainManifestLookup | undefined,
|
||||||
|
domains: Set<string> | undefined,
|
||||||
|
type: "helper" | "other" | undefined
|
||||||
|
): ListItem[] => {
|
||||||
|
if (!triggers || !manifests) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result: ListItem[] = [];
|
||||||
|
const addedDomains = new Set<string>();
|
||||||
|
Object.keys(triggers).forEach((trigger) => {
|
||||||
|
const domain = getTriggerDomain(trigger);
|
||||||
|
|
||||||
|
if (addedDomains.has(domain)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addedDomains.add(domain);
|
||||||
|
|
||||||
|
const manifest = manifests[domain];
|
||||||
|
const domainUsed = !domains ? true : domains.has(domain);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(type === undefined &&
|
||||||
|
(ENTITY_DOMAINS_MAIN.has(domain) ||
|
||||||
|
(manifest?.integration_type === "entity" &&
|
||||||
|
domainUsed &&
|
||||||
|
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
|
||||||
|
(type === "helper" && manifest?.integration_type === "helper") ||
|
||||||
|
(type === "other" &&
|
||||||
|
!ENTITY_DOMAINS_MAIN.has(domain) &&
|
||||||
|
(ENTITY_DOMAINS_OTHER.has(domain) ||
|
||||||
|
(!domainUsed && manifest?.integration_type === "entity") ||
|
||||||
|
!["helper", "entity"].includes(manifest?.integration_type || "")))
|
||||||
|
) {
|
||||||
|
result.push({
|
||||||
|
icon: html`
|
||||||
|
<ha-domain-icon
|
||||||
|
.hass=${this.hass}
|
||||||
|
.domain=${domain}
|
||||||
|
brand-fallback
|
||||||
|
></ha-domain-icon>
|
||||||
|
`,
|
||||||
|
key: `${DYNAMIC_PREFIX}${domain}`,
|
||||||
|
name: domainToName(localize, domain, manifest),
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result.sort((a, b) =>
|
||||||
|
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _triggers = memoizeOne(
|
||||||
|
(
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
triggers: TriggerDescriptions,
|
||||||
|
_manifests: DomainManifestLookup | undefined,
|
||||||
|
group?: string
|
||||||
|
): ListItem[] => {
|
||||||
|
if (!triggers) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result: ListItem[] = [];
|
||||||
|
|
||||||
|
for (const trigger of Object.keys(triggers)) {
|
||||||
|
const domain = getTriggerDomain(trigger);
|
||||||
|
const triggerName = getTriggerObjectId(trigger);
|
||||||
|
|
||||||
|
if (group && group !== `${DYNAMIC_PREFIX}${domain}`) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
icon: html`
|
||||||
|
<ha-trigger-icon
|
||||||
|
.hass=${this.hass}
|
||||||
|
.trigger=${trigger}
|
||||||
|
></ha-trigger-icon>
|
||||||
|
`,
|
||||||
|
key: `${DYNAMIC_PREFIX}${trigger}`,
|
||||||
|
name:
|
||||||
|
localize(`component.${domain}.triggers.${triggerName}.name`) ||
|
||||||
|
trigger,
|
||||||
|
description:
|
||||||
|
localize(
|
||||||
|
`component.${domain}.triggers.${triggerName}.description`
|
||||||
|
) || trigger,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private _services = memoizeOne(
|
private _services = memoizeOne(
|
||||||
(
|
(
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
@@ -539,8 +702,8 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
let domain: string | undefined;
|
let domain: string | undefined;
|
||||||
|
|
||||||
if (isService(group)) {
|
if (isDynamic(group)) {
|
||||||
domain = getService(group!);
|
domain = getValueFromDynamic(group!);
|
||||||
}
|
}
|
||||||
|
|
||||||
const addDomain = (dmn: string) => {
|
const addDomain = (dmn: string) => {
|
||||||
@@ -554,7 +717,7 @@ class DialogAddAutomationElement
|
|||||||
.service=${`${dmn}.${service}`}
|
.service=${`${dmn}.${service}`}
|
||||||
></ha-service-icon>
|
></ha-service-icon>
|
||||||
`,
|
`,
|
||||||
key: `${SERVICE_PREFIX}${dmn}.${service}`,
|
key: `${DYNAMIC_PREFIX}${dmn}.${service}`,
|
||||||
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
|
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
|
||||||
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
|
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
|
||||||
services[dmn][service]?.name ||
|
services[dmn][service]?.name ||
|
||||||
@@ -668,14 +831,15 @@ class DialogAddAutomationElement
|
|||||||
this._domains,
|
this._domains,
|
||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this.hass.services,
|
this.hass.services,
|
||||||
|
this._triggerDescriptions,
|
||||||
this._manifests
|
this._manifests
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupName = isService(this._selectedGroup)
|
const groupName = isDynamic(this._selectedGroup)
|
||||||
? domainToName(
|
? domainToName(
|
||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
getService(this._selectedGroup!),
|
getValueFromDynamic(this._selectedGroup!),
|
||||||
this._manifests?.[getService(this._selectedGroup!)]
|
this._manifests?.[getValueFromDynamic(this._selectedGroup!)]
|
||||||
)
|
)
|
||||||
: this.hass.localize(
|
: this.hass.localize(
|
||||||
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
|
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ 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,7 +17,6 @@ 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,6 +6,9 @@ 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,8 +15,15 @@ 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 { TriggerSidebarConfig } from "../../../../data/automation";
|
import type {
|
||||||
import { isTriggerList } from "../../../../data/trigger";
|
LegacyTrigger,
|
||||||
|
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";
|
||||||
@@ -63,8 +70,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const rowDisabled =
|
const rowDisabled =
|
||||||
this.disabled ||
|
"enabled" in this.config.config && this.config.config.enabled === false;
|
||||||
("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;
|
||||||
@@ -73,9 +79,18 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
|||||||
"ui.panel.config.automation.editor.triggers.trigger"
|
"ui.panel.config.automation.editor.triggers.trigger"
|
||||||
);
|
);
|
||||||
|
|
||||||
const title = this.hass.localize(
|
const domain =
|
||||||
`ui.panel.config.automation.editor.triggers.type.${type}.label`
|
"trigger" in this.config.config &&
|
||||||
);
|
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
|
||||||
@@ -269,6 +284,7 @@ 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,10 +9,12 @@ 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 {
|
||||||
@@ -31,6 +33,8 @@ 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() {
|
||||||
@@ -87,7 +91,14 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
|||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<div @value-changed=${this._onUiChanged}>
|
<div @value-changed=${this._onUiChanged}>
|
||||||
${dynamicElement(`ha-automation-trigger-${type}`, {
|
${this.description
|
||||||
|
? html`<ha-automation-trigger-platform
|
||||||
|
.hass=${this.hass}
|
||||||
|
.trigger=${this.trigger}
|
||||||
|
.description=${this.description}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
></ha-automation-trigger-platform>`
|
||||||
|
: dynamicElement(`ha-automation-trigger-${type}`, {
|
||||||
hass: this.hass,
|
hass: this.hass,
|
||||||
trigger: this.trigger,
|
trigger: this.trigger,
|
||||||
disabled: this.disabled,
|
disabled: this.disabled,
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ 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";
|
||||||
@@ -50,7 +52,8 @@ 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 { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
|
import type { TriggerDescriptions } from "../../../../data/trigger";
|
||||||
|
import { isTriggerList } from "../../../../data/trigger";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showPromptDialog,
|
showPromptDialog,
|
||||||
@@ -72,6 +75,7 @@ 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";
|
||||||
@@ -137,6 +141,9 @@ 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")
|
||||||
@@ -178,18 +185,24 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderRow() {
|
private _renderRow() {
|
||||||
const type = this._getType(this.trigger);
|
const type = this._getType(this.trigger, this.triggerDescriptions);
|
||||||
|
|
||||||
const supported = this._uiSupported(type);
|
const supported = this._uiSupported(type);
|
||||||
|
|
||||||
const yamlMode = this._yamlMode || !supported;
|
const yamlMode = this._yamlMode || !supported;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-svg-icon
|
${type === "list"
|
||||||
|
? 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>
|
||||||
@@ -393,6 +406,9 @@ 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}
|
||||||
@@ -552,6 +568,7 @@ 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 });
|
||||||
@@ -576,8 +593,14 @@ 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 || this.trigger,
|
config: trigger,
|
||||||
uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
|
uiSupported: this._uiSupported(
|
||||||
|
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;
|
||||||
@@ -759,8 +782,18 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getType = memoizeOne((trigger: Trigger) =>
|
private _getType = memoizeOne(
|
||||||
isTriggerList(trigger) ? "list" : trigger.trigger
|
(trigger: Trigger, triggerDescriptions: TriggerDescriptions) => {
|
||||||
|
if (isTriggerList(trigger)) {
|
||||||
|
return "list";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trigger.trigger in triggerDescriptions) {
|
||||||
|
return "platform";
|
||||||
|
}
|
||||||
|
|
||||||
|
return trigger.trigger;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _uiSupported = memoizeOne(
|
private _uiSupported = memoizeOne(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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";
|
||||||
@@ -12,12 +13,16 @@ 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 type {
|
import {
|
||||||
AutomationClipboard,
|
getValueFromDynamic,
|
||||||
Trigger,
|
isDynamic,
|
||||||
TriggerList,
|
type AutomationClipboard,
|
||||||
|
type Trigger,
|
||||||
|
type TriggerList,
|
||||||
} from "../../../../data/automation";
|
} from "../../../../data/automation";
|
||||||
import { isTriggerList } from "../../../../data/trigger";
|
import type { TriggerDescriptions } 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,
|
||||||
@@ -26,10 +31,9 @@ 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 LitElement {
|
export default class HaAutomationTrigger extends SubscribeMixin(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[];
|
||||||
@@ -62,6 +66,23 @@ export default class HaAutomationTrigger extends 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
|
||||||
@@ -85,6 +106,7 @@ export default class HaAutomationTrigger extends 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}
|
||||||
@@ -156,6 +178,10 @@ export default class HaAutomationTrigger extends 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(
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
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,8 +16,10 @@ 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 {
|
||||||
@@ -170,11 +172,10 @@ function formatTooltip(
|
|||||||
compare
|
compare
|
||||||
? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `
|
? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `
|
||||||
: ""
|
: ""
|
||||||
}${formatTime(date, locale, config)} – ${formatTime(
|
}${formatTime(date, locale, config)}`;
|
||||||
addHours(date, 1),
|
if (params[0].componentSubType === "bar") {
|
||||||
locale,
|
period += ` – ${formatTime(addHours(date, 1), locale, config)}`;
|
||||||
config
|
}
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
|
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
|
||||||
|
|
||||||
@@ -281,6 +282,35 @@ 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;
|
||||||
|
|||||||
335
src/panels/lovelace/cards/energy/hui-power-sources-graph-card.ts
Normal file
335
src/panels/lovelace/cards/energy/hui-power-sources-graph-card.ts
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
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,6 +230,11 @@ 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,6 +66,8 @@ 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,6 +22,7 @@ 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,6 +109,7 @@ 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 (
|
||||||
@@ -116,16 +117,18 @@ 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
|
||||||
) {
|
) {
|
||||||
this._applyTheme();
|
applyTheme = true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperties.has("background")) {
|
if (changedProperties.has("background")) {
|
||||||
this._applyTheme();
|
applyTheme = true;
|
||||||
this._fetchMedia();
|
this._fetchMedia();
|
||||||
}
|
}
|
||||||
if (changedProperties.has("resolvedImage")) {
|
if (changedProperties.has("resolvedImage")) {
|
||||||
|
applyTheme = true;
|
||||||
|
}
|
||||||
|
if (applyTheme) {
|
||||||
this._applyTheme();
|
this._applyTheme();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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 {
|
||||||
@@ -41,6 +42,8 @@ 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;
|
||||||
@@ -65,6 +68,9 @@ 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`)
|
||||||
@@ -138,6 +144,17 @@ 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
|
||||||
)
|
)
|
||||||
@@ -455,6 +472,9 @@ class DialogTodoItemEditor extends LitElement {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,23 +199,3 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
30
src/resources/theme/animations.globals.ts
Normal file
30
src/resources/theme/animations.globals.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -55,6 +55,7 @@ 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,4 +1,5 @@
|
|||||||
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";
|
||||||
@@ -17,6 +18,7 @@ export const themeStyles = [
|
|||||||
...colorStylesCollection,
|
...colorStylesCollection,
|
||||||
fontStyles.toString(),
|
fontStyles.toString(),
|
||||||
waMainStyles.toString(),
|
waMainStyles.toString(),
|
||||||
|
animationStyles.toString(),
|
||||||
].join("");
|
].join("");
|
||||||
|
|
||||||
export const derivedStyles = {
|
export const derivedStyles = {
|
||||||
|
|||||||
@@ -1130,6 +1130,7 @@
|
|||||||
"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",
|
||||||
@@ -7161,6 +7162,12 @@
|
|||||||
"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,12 +1,16 @@
|
|||||||
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 = (
|
export const removeLaunchScreen = () => {
|
||||||
launchScreenElement: HTMLElement,
|
const launchScreenElement = document.getElementById("ha-launch-screen");
|
||||||
skipAnimation: boolean
|
if (!launchScreenElement?.parentElement) {
|
||||||
) => {
|
return;
|
||||||
if (skipAnimation) {
|
}
|
||||||
|
|
||||||
|
withViewTransition((viewTransitionAvailable: boolean) => {
|
||||||
|
if (!viewTransitionAvailable) {
|
||||||
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -20,22 +24,7 @@ const removeElement = (
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
||||||
}, parseAnimationDuration(durationFromCss));
|
}, parseAnimationDuration(durationFromCss));
|
||||||
};
|
|
||||||
|
|
||||||
export const removeLaunchScreen = () => {
|
|
||||||
const launchScreenElement = document.getElementById("ha-launch-screen");
|
|
||||||
if (!launchScreenElement?.parentElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.startViewTransition) {
|
|
||||||
document.startViewTransition(() => {
|
|
||||||
removeElement(launchScreenElement, false);
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// Fallback: Direct removal without transition
|
|
||||||
removeElement(launchScreenElement, true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
|
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user