mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-16 14:30:36 +00:00
Compare commits
13 Commits
renovate/l
...
add-device
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df7a36e743 | ||
|
|
5786fe4b8d | ||
|
|
6fa274e4bf | ||
|
|
1bd1e015ff | ||
|
|
7588490419 | ||
|
|
2e80a3ddab | ||
|
|
332694549c | ||
|
|
396ddef722 | ||
|
|
d02804449a | ||
|
|
4ab24cdc72 | ||
|
|
81c27090d2 | ||
|
|
09bdfd3ad7 | ||
|
|
97e49f751c |
@@ -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",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -153,7 +153,7 @@
|
|||||||
"@babel/plugin-transform-runtime": "7.28.5",
|
"@babel/plugin-transform-runtime": "7.28.5",
|
||||||
"@babel/preset-env": "7.28.5",
|
"@babel/preset-env": "7.28.5",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.21.6",
|
"@bundle-stats/plugin-webpack-filter": "4.21.6",
|
||||||
"@lokalise/node-api": "15.4.0",
|
"@lokalise/node-api": "15.3.1",
|
||||||
"@octokit/auth-oauth-device": "8.0.3",
|
"@octokit/auth-oauth-device": "8.0.3",
|
||||||
"@octokit/plugin-retry": "8.0.3",
|
"@octokit/plugin-retry": "8.0.3",
|
||||||
"@octokit/rest": "22.0.1",
|
"@octokit/rest": "22.0.1",
|
||||||
|
|||||||
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",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface AnalyticsPreferences {
|
|||||||
diagnostics?: boolean;
|
diagnostics?: boolean;
|
||||||
usage?: boolean;
|
usage?: boolean;
|
||||||
statistics?: boolean;
|
statistics?: boolean;
|
||||||
|
snapshots?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Analytics {
|
export interface Analytics {
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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]>>;
|
||||||
);
|
|
||||||
return resources.services.all;
|
|
||||||
}
|
}
|
||||||
if (!force && domain in resources.services.domains) {
|
if (!force && domain in resources[category].domains) {
|
||||||
return resources.services.domains[domain];
|
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
||||||
}
|
}
|
||||||
if (resources.services.all && !force) {
|
if (resources[category].all && !force) {
|
||||||
await resources.services.all;
|
await resources[category].all;
|
||||||
if (domain in resources.services.domains) {
|
if (domain in resources[category].domains) {
|
||||||
return resources.services.domains[domain];
|
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) : "_";
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import type { CSSResultGroup } from "lit";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { createCloseHeading } from "../../components/ha-dialog";
|
import "../../components/ha-wa-dialog";
|
||||||
|
import "../../components/ha-dialog-footer";
|
||||||
import "../../components/ha-formfield";
|
import "../../components/ha-formfield";
|
||||||
import "../../components/ha-switch";
|
import "../../components/ha-switch";
|
||||||
import "../../components/ha-button";
|
import "../../components/ha-button";
|
||||||
@@ -28,6 +29,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
|
|
||||||
@state() private _submitting = false;
|
@state() private _submitting = false;
|
||||||
|
|
||||||
|
@state() private _open = false;
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
params: ConfigEntrySystemOptionsDialogParams
|
params: ConfigEntrySystemOptionsDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -35,9 +38,14 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._disableNewEntities = params.entry.pref_disable_new_entities;
|
this._disableNewEntities = params.entry.pref_disable_new_entities;
|
||||||
this._disablePolling = params.entry.pref_disable_polling;
|
this._disablePolling = params.entry.pref_disable_polling;
|
||||||
|
this._open = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
|
this._open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dialogClosed(): void {
|
||||||
this._error = "";
|
this._error = "";
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
@@ -49,18 +57,19 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-wa-dialog
|
||||||
open
|
.hass=${this.hass}
|
||||||
@closed=${this.closeDialog}
|
.open=${this._open}
|
||||||
.heading=${createCloseHeading(
|
header-title=${this.hass.localize(
|
||||||
this.hass,
|
"ui.dialogs.config_entry_system_options.title",
|
||||||
this.hass.localize("ui.dialogs.config_entry_system_options.title", {
|
{
|
||||||
integration:
|
integration:
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
`component.${this._params.entry.domain}.title`
|
`component.${this._params.entry.domain}.title`
|
||||||
) || this._params.entry.domain,
|
) || this._params.entry.domain,
|
||||||
})
|
}
|
||||||
)}
|
)}
|
||||||
|
@closed=${this._dialogClosed}
|
||||||
>
|
>
|
||||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
@@ -82,10 +91,10 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
</p>`}
|
</p>`}
|
||||||
>
|
>
|
||||||
<ha-switch
|
<ha-switch
|
||||||
|
autofocus
|
||||||
.checked=${!this._disableNewEntities}
|
.checked=${!this._disableNewEntities}
|
||||||
@change=${this._disableNewEntitiesChanged}
|
@change=${this._disableNewEntitiesChanged}
|
||||||
.disabled=${this._submitting}
|
.disabled=${this._submitting}
|
||||||
dialogInitialFocus
|
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
|
|
||||||
@@ -113,22 +122,27 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
.disabled=${this._submitting}
|
.disabled=${this._submitting}
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
<ha-button
|
|
||||||
appearance="plain"
|
<ha-dialog-footer slot="footer">
|
||||||
slot="primaryAction"
|
<ha-button
|
||||||
@click=${this.closeDialog}
|
appearance="plain"
|
||||||
.disabled=${this._submitting}
|
slot="secondaryAction"
|
||||||
>
|
@click=${this.closeDialog}
|
||||||
${this.hass.localize("ui.common.cancel")}
|
.disabled=${this._submitting}
|
||||||
</ha-button>
|
>
|
||||||
<ha-button
|
${this.hass.localize("ui.common.cancel")}
|
||||||
slot="primaryAction"
|
</ha-button>
|
||||||
@click=${this._updateEntry}
|
<ha-button
|
||||||
.disabled=${this._submitting}
|
slot="primaryAction"
|
||||||
>
|
@click=${this._updateEntry}
|
||||||
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
|
.disabled=${this._submitting}
|
||||||
</ha-button>
|
>
|
||||||
</ha-dialog>
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.config_entry_system_options.update"
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</ha-dialog-footer>
|
||||||
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,11 +91,18 @@ 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
|
||||||
hass: this.hass,
|
? html`<ha-automation-trigger-platform
|
||||||
trigger: this.trigger,
|
.hass=${this.hass}
|
||||||
disabled: this.disabled,
|
.trigger=${this.trigger}
|
||||||
})}
|
.description=${this.description}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
></ha-automation-trigger-platform>`
|
||||||
|
: dynamicElement(`ha-automation-trigger-${type}`, {
|
||||||
|
hass: this.hass,
|
||||||
|
trigger: this.trigger,
|
||||||
|
disabled: this.disabled,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,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"
|
||||||
slot="leading-icon"
|
? html`<ha-svg-icon
|
||||||
class="trigger-icon"
|
slot="leading-icon"
|
||||||
.path=${TRIGGER_ICONS[type]}
|
class="trigger-icon"
|
||||||
></ha-svg-icon>
|
.path=${TRIGGER_ICONS[type]}
|
||||||
|
></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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { mdiOpenInNew } from "@mdi/js";
|
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import "../../../components/ha-analytics";
|
import "../../../components/ha-analytics";
|
||||||
@@ -17,6 +16,8 @@ import {
|
|||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { documentationUrl } from "../../../util/documentation-url";
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
|
import { isDevVersion } from "../../../common/config/version";
|
||||||
|
import type { HaSwitch } from "../../../components/ha-switch";
|
||||||
|
|
||||||
@customElement("ha-config-analytics")
|
@customElement("ha-config-analytics")
|
||||||
class ConfigAnalytics extends LitElement {
|
class ConfigAnalytics extends LitElement {
|
||||||
@@ -34,10 +35,22 @@ class ConfigAnalytics extends LitElement {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card outlined>
|
<ha-card
|
||||||
|
outlined
|
||||||
|
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
|
||||||
|
"Home Assistant analytics"}
|
||||||
|
>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${error ? html`<div class="error">${error}</div>` : ""}
|
${error ? html`<div class="error">${error}</div>` : nothing}
|
||||||
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p>
|
<p>
|
||||||
|
${this.hass.localize("ui.panel.config.analytics.intro")}
|
||||||
|
<a
|
||||||
|
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>${this.hass.localize("ui.panel.config.analytics.learn_more")} </a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
<ha-analytics
|
<ha-analytics
|
||||||
translation_key_panel="config"
|
translation_key_panel="config"
|
||||||
@analytics-preferences-changed=${this._preferencesChanged}
|
@analytics-preferences-changed=${this._preferencesChanged}
|
||||||
@@ -45,26 +58,50 @@ class ConfigAnalytics extends LitElement {
|
|||||||
.analytics=${this._analyticsDetails}
|
.analytics=${this._analyticsDetails}
|
||||||
></ha-analytics>
|
></ha-analytics>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
|
||||||
<ha-button @click=${this._save}>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.core.section.core.core_config.save_button"
|
|
||||||
)}
|
|
||||||
</ha-button>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
</ha-card>
|
||||||
<div class="footer">
|
${isDevVersion(this.hass.config.version)
|
||||||
<ha-button
|
? html`<ha-card
|
||||||
size="small"
|
outlined
|
||||||
appearance="plain"
|
.header=${this.hass.localize(
|
||||||
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
"ui.panel.config.analytics.preferences.snapshots.header"
|
||||||
target="_blank"
|
)}
|
||||||
rel="noreferrer"
|
>
|
||||||
>
|
<div class="card-content">
|
||||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
<p>
|
||||||
${this.hass.localize("ui.panel.config.analytics.learn_more")}
|
${this.hass.localize(
|
||||||
</ha-button>
|
"ui.panel.config.analytics.preferences.snapshots.info"
|
||||||
</div>
|
)}
|
||||||
|
<a
|
||||||
|
href=${documentationUrl(this.hass, "/device-database/")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.analytics.preferences.snapshots.learn_more"
|
||||||
|
)} </a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading" data-for="snapshots">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.analytics.preferences.snapshots.title`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="description" data-for="snapshots">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.analytics.preferences.snapshots.description`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._handleDeviceRowClick}
|
||||||
|
.checked=${!!this._analyticsDetails?.preferences.snapshots}
|
||||||
|
.disabled=${this._analyticsDetails === undefined}
|
||||||
|
name="snapshots"
|
||||||
|
>
|
||||||
|
</ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
</div>
|
||||||
|
</ha-card>`
|
||||||
|
: nothing}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,11 +133,25 @@ class ConfigAnalytics extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleDeviceRowClick(ev: Event) {
|
||||||
|
const target = ev.target as HaSwitch;
|
||||||
|
|
||||||
|
this._analyticsDetails = {
|
||||||
|
...this._analyticsDetails!,
|
||||||
|
preferences: {
|
||||||
|
...this._analyticsDetails!.preferences,
|
||||||
|
snapshots: target.checked,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this._save();
|
||||||
|
}
|
||||||
|
|
||||||
private _preferencesChanged(event: CustomEvent): void {
|
private _preferencesChanged(event: CustomEvent): void {
|
||||||
this._analyticsDetails = {
|
this._analyticsDetails = {
|
||||||
...this._analyticsDetails!,
|
...this._analyticsDetails!,
|
||||||
preferences: event.detail.preferences,
|
preferences: event.detail.preferences,
|
||||||
};
|
};
|
||||||
|
this._save();
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -127,6 +178,9 @@ class ConfigAnalytics extends LitElement {
|
|||||||
padding: 32px 0 16px;
|
padding: 32px 0 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
ha-card:not(:first-of-type) {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
ha-button[size="small"] ha-svg-icon {
|
ha-button[size="small"] ha-svg-icon {
|
||||||
--mdc-icon-size: 16px;
|
--mdc-icon-size: 16px;
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state, query } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { mdiClose } from "@mdi/js";
|
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-md-dialog";
|
import "../../../../components/ha-wa-dialog";
|
||||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
import "../../../../components/ha-dialog-footer";
|
||||||
import "../../../../components/ha-dialog-header";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-form/ha-form";
|
import "../../../../components/ha-form/ha-form";
|
||||||
import "../../../../components/ha-icon-button";
|
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||||
import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
|
import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
|
||||||
@@ -43,7 +41,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
|
|
||||||
@state() private _submitting = false;
|
@state() private _submitting = false;
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
@state() private _open = false;
|
||||||
|
|
||||||
public showDialog(params: LovelaceResourceDetailsDialogParams): void {
|
public showDialog(params: LovelaceResourceDetailsDialogParams): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
@@ -58,6 +56,11 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
url: "",
|
url: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
this._open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogClosed(): void {
|
private _dialogClosed(): void {
|
||||||
@@ -65,10 +68,6 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
|
||||||
this._dialog?.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._params) {
|
if (!this._params) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@@ -81,56 +80,45 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
"ui.panel.config.lovelace.resources.detail.new_resource"
|
"ui.panel.config.lovelace.resources.detail.new_resource"
|
||||||
);
|
);
|
||||||
|
|
||||||
const ariaLabel = this._params.resource?.url
|
|
||||||
? this.hass!.localize(
|
|
||||||
"ui.panel.config.lovelace.resources.detail.edit_resource"
|
|
||||||
)
|
|
||||||
: this.hass!.localize(
|
|
||||||
"ui.panel.config.lovelace.resources.detail.new_resource"
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog
|
<ha-wa-dialog
|
||||||
open
|
.hass=${this.hass}
|
||||||
disable-cancel-action
|
.open=${this._open}
|
||||||
|
prevent-scrim-close
|
||||||
|
header-title=${dialogTitle}
|
||||||
@closed=${this._dialogClosed}
|
@closed=${this._dialogClosed}
|
||||||
.ariaLabel=${ariaLabel}
|
|
||||||
>
|
>
|
||||||
<ha-dialog-header slot="headline">
|
<ha-alert
|
||||||
<ha-icon-button
|
alert-type="warning"
|
||||||
slot="navigationIcon"
|
.title=${this.hass!.localize(
|
||||||
.label=${this.hass.localize("ui.common.close") ?? "Close"}
|
"ui.panel.config.lovelace.resources.detail.warning_header"
|
||||||
.path=${mdiClose}
|
)}
|
||||||
@click=${this.closeDialog}
|
>
|
||||||
></ha-icon-button>
|
${this.hass!.localize(
|
||||||
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
|
"ui.panel.config.lovelace.resources.detail.warning_text"
|
||||||
</ha-dialog-header>
|
)}
|
||||||
<div slot="content">
|
</ha-alert>
|
||||||
<ha-alert
|
|
||||||
alert-type="warning"
|
|
||||||
.title=${this.hass!.localize(
|
|
||||||
"ui.panel.config.lovelace.resources.detail.warning_header"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.lovelace.resources.detail.warning_text"
|
|
||||||
)}
|
|
||||||
</ha-alert>
|
|
||||||
|
|
||||||
<ha-form
|
<ha-form
|
||||||
.schema=${this._schema(this._data)}
|
autofocus
|
||||||
.data=${this._data}
|
.schema=${this._schema(this._data)}
|
||||||
.hass=${this.hass}
|
.data=${this._data}
|
||||||
.error=${this._error}
|
.hass=${this.hass}
|
||||||
.computeLabel=${this._computeLabel}
|
.error=${this._error}
|
||||||
@value-changed=${this._valueChanged}
|
.computeLabel=${this._computeLabel}
|
||||||
></ha-form>
|
@value-changed=${this._valueChanged}
|
||||||
</div>
|
></ha-form>
|
||||||
<div slot="actions">
|
|
||||||
<ha-button appearance="plain" @click=${this.closeDialog}>
|
<ha-dialog-footer slot="footer">
|
||||||
|
<ha-button
|
||||||
|
appearance="plain"
|
||||||
|
slot="secondaryAction"
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
>
|
||||||
${this.hass!.localize("ui.common.cancel")}
|
${this.hass!.localize("ui.common.cancel")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
<ha-button
|
<ha-button
|
||||||
|
slot="primaryAction"
|
||||||
@click=${this._updateResource}
|
@click=${this._updateResource}
|
||||||
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
|
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
|
||||||
>
|
>
|
||||||
@@ -142,8 +130,8 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
"ui.panel.config.lovelace.resources.detail.create"
|
"ui.panel.config.lovelace.resources.detail.create"
|
||||||
)}
|
)}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</div>
|
</ha-dialog-footer>
|
||||||
</ha-md-dialog>
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
state: true,
|
state: true,
|
||||||
subscribe: false,
|
subscribe: false,
|
||||||
})
|
})
|
||||||
private _chartType: "bar" | "pie" = "bar";
|
private _chartType?: "bar" | "pie";
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
@@ -101,6 +101,14 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
this._config = config;
|
this._config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getAllowedModes(): ("bar" | "pie")[] {
|
||||||
|
// Empty array or undefined = allow all modes
|
||||||
|
if (!this._config?.modes || this._config.modes.length === 0) {
|
||||||
|
return ["bar", "pie"];
|
||||||
|
}
|
||||||
|
return this._config.modes;
|
||||||
|
}
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
return (
|
return (
|
||||||
hasConfigChanged(this, changedProps) ||
|
hasConfigChanged(this, changedProps) ||
|
||||||
@@ -109,8 +117,21 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
|
if (changedProps.has("_config") && this._config) {
|
||||||
|
const allowedModes = this._getAllowedModes();
|
||||||
|
|
||||||
|
// If _chartType is not set or not in allowed modes, use first from config
|
||||||
|
if (!this._chartType || !allowedModes.includes(this._chartType)) {
|
||||||
|
this._chartType = allowedModes[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass || !this._config) {
|
if (!this.hass || !this._config || !this._chartType) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,13 +139,19 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||||
<ha-icon-button
|
${this._getAllowedModes().length > 1
|
||||||
.path=${this._chartType === "pie" ? mdiChartBar : mdiChartDonut}
|
? html`
|
||||||
.label=${this.hass.localize(
|
<ha-icon-button
|
||||||
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
|
.path=${this._chartType === "pie"
|
||||||
)}
|
? mdiChartBar
|
||||||
@click=${this._handleChartTypeChange}
|
: mdiChartDonut}
|
||||||
></ha-icon-button>
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
|
||||||
|
)}
|
||||||
|
@click=${this._handleChartTypeChange}
|
||||||
|
></ha-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="content ${classMap({
|
class="content ${classMap({
|
||||||
@@ -529,7 +556,13 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleChartTypeChange(): void {
|
private _handleChartTypeChange(): void {
|
||||||
this._chartType = this._chartType === "pie" ? "bar" : "pie";
|
if (!this._chartType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allowedModes = this._getAllowedModes();
|
||||||
|
const currentIndex = allowedModes.indexOf(this._chartType);
|
||||||
|
const nextIndex = (currentIndex + 1) % allowedModes.length;
|
||||||
|
this._chartType = allowedModes[nextIndex];
|
||||||
this._getStatistics(this._data!);
|
this._getStatistics(this._data!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig {
|
|||||||
title?: string;
|
title?: string;
|
||||||
max_devices?: number;
|
max_devices?: number;
|
||||||
hide_compound_stats?: boolean;
|
hide_compound_stats?: boolean;
|
||||||
|
modes?: ("bar" | "pie")[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnergyDevicesDetailGraphCardConfig
|
export interface EnergyDevicesDetailGraphCardConfig
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -6759,6 +6760,7 @@
|
|||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"caption": "Analytics",
|
"caption": "Analytics",
|
||||||
|
"header": "Home Assistant analytics",
|
||||||
"description": "Learn how to share data to improve Home Assistant",
|
"description": "Learn how to share data to improve Home Assistant",
|
||||||
"preferences": {
|
"preferences": {
|
||||||
"base": {
|
"base": {
|
||||||
@@ -6776,10 +6778,17 @@
|
|||||||
"diagnostics": {
|
"diagnostics": {
|
||||||
"title": "Diagnostics",
|
"title": "Diagnostics",
|
||||||
"description": "Share crash reports when unexpected errors occur."
|
"description": "Share crash reports when unexpected errors occur."
|
||||||
|
},
|
||||||
|
"snapshots": {
|
||||||
|
"title": "Devices",
|
||||||
|
"description": "Generic information of your devices.",
|
||||||
|
"header": "Device analytics",
|
||||||
|
"info": "Anonymously share data about your devices to help build the Open Home Foundation’s device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).",
|
||||||
|
"learn_more": "Learn more about the device database and how we process your data"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"need_base_enabled": "You need to enable basic analytics for this option to be available",
|
"need_base_enabled": "You need to enable basic analytics for this option to be available",
|
||||||
"learn_more": "How we process your data",
|
"learn_more": "Learn how we process your data",
|
||||||
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
|
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
|
||||||
"download_device_info": "Preview device analytics"
|
"download_device_info": "Preview device analytics"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,26 +1,7 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { render } from "lit";
|
import { render } from "lit";
|
||||||
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
|
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
|
||||||
|
import { withViewTransition } from "../common/util/view-transition";
|
||||||
const removeElement = (
|
|
||||||
launchScreenElement: HTMLElement,
|
|
||||||
skipAnimation: boolean
|
|
||||||
) => {
|
|
||||||
if (skipAnimation) {
|
|
||||||
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
launchScreenElement.classList.add("removing");
|
|
||||||
|
|
||||||
const durationFromCss = getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue("--ha-animation-base-duration")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
|
||||||
}, parseAnimationDuration(durationFromCss));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeLaunchScreen = () => {
|
export const removeLaunchScreen = () => {
|
||||||
const launchScreenElement = document.getElementById("ha-launch-screen");
|
const launchScreenElement = document.getElementById("ha-launch-screen");
|
||||||
@@ -28,14 +9,22 @@ export const removeLaunchScreen = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.startViewTransition) {
|
withViewTransition((viewTransitionAvailable: boolean) => {
|
||||||
document.startViewTransition(() => {
|
if (!viewTransitionAvailable) {
|
||||||
removeElement(launchScreenElement, false);
|
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
||||||
});
|
return;
|
||||||
} else {
|
}
|
||||||
// Fallback: Direct removal without transition
|
|
||||||
removeElement(launchScreenElement, true);
|
launchScreenElement.classList.add("removing");
|
||||||
}
|
|
||||||
|
const durationFromCss = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue("--ha-animation-base-duration")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
||||||
|
}, parseAnimationDuration(durationFromCss));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
|
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -2365,10 +2365,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lokalise/node-api@npm:15.4.0":
|
"@lokalise/node-api@npm:15.3.1":
|
||||||
version: 15.4.0
|
version: 15.3.1
|
||||||
resolution: "@lokalise/node-api@npm:15.4.0"
|
resolution: "@lokalise/node-api@npm:15.3.1"
|
||||||
checksum: 10/fe7e36bb137310244079fba9978a10fdf65ca6566e075e8e25ed0fd461e7168649ca43929a0a3a0eaf2df72055996ef4d8a72302e7b502863feb9f9a6471aff1
|
checksum: 10/9175559660cfbde3f6451ee0ade96ca5ccf6686f3a8f07a23ae6abf3a58db5b5dc71683cdb7f19252765250df7b77dc67539a80e24c3b44a1a97bb2f2d9cd090
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -9233,7 +9233,7 @@ __metadata:
|
|||||||
"@lit-labs/virtualizer": "npm:2.1.1"
|
"@lit-labs/virtualizer": "npm:2.1.1"
|
||||||
"@lit/context": "npm:1.1.6"
|
"@lit/context": "npm:1.1.6"
|
||||||
"@lit/reactive-element": "npm:2.1.1"
|
"@lit/reactive-element": "npm:2.1.1"
|
||||||
"@lokalise/node-api": "npm:15.4.0"
|
"@lokalise/node-api": "npm:15.3.1"
|
||||||
"@material/chips": "npm:=14.0.0-canary.53b3cad2f.0"
|
"@material/chips": "npm:=14.0.0-canary.53b3cad2f.0"
|
||||||
"@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0"
|
"@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0"
|
||||||
"@material/mwc-base": "npm:0.27.0"
|
"@material/mwc-base": "npm:0.27.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user