mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-19 07:50:25 +00:00
Compare commits
7 Commits
save_panel
...
sec_pypi_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8535ee0694 | ||
|
|
b8110d1a45 | ||
|
|
19e9de39c5 | ||
|
|
f22f01e513 | ||
|
|
3f86f144b5 | ||
|
|
4efef5ed16 | ||
|
|
cac7ae2a40 |
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
|
||||
13
.github/workflows/release.yaml
vendored
13
.github/workflows/release.yaml
vendored
@@ -19,8 +19,11 @@ jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
id-token: write # For "Trusted Publisher" to PyPi
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
@@ -46,14 +49,18 @@ jobs:
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Build and release package
|
||||
run: |
|
||||
python3 -m pip install twine build
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
python3 -m pip install build
|
||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||
script/release
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
with:
|
||||
|
||||
@@ -260,6 +260,7 @@ const createRspackConfig = ({
|
||||
),
|
||||
},
|
||||
experiments: {
|
||||
layers: true,
|
||||
outputModule: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-yaml-editor";
|
||||
import type { LegacyTrigger } from "../../../../src/data/automation";
|
||||
import type { Trigger } from "../../../../src/data/automation";
|
||||
import { describeTrigger } from "../../../../src/data/automation_i18n";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
@@ -66,7 +66,7 @@ const triggers = [
|
||||
},
|
||||
];
|
||||
|
||||
const initialTrigger: LegacyTrigger = {
|
||||
const initialTrigger: Trigger = {
|
||||
trigger: "state",
|
||||
entity_id: "light.kitchen",
|
||||
};
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.18",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
# Pushes a new version to PyPi.
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
@@ -12,5 +11,4 @@ yarn install
|
||||
script/build_frontend
|
||||
|
||||
rm -rf dist home_assistant_frontend.egg-info
|
||||
python3 -m build
|
||||
python3 -m twine upload dist/*.whl --skip-existing
|
||||
python3 -m build -q
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Executes a callback within a View Transition if supported, otherwise runs it directly.
|
||||
*
|
||||
* @param callback - Function to execute. Can be synchronous or return a Promise. The callback will be passed a boolean indicating whether the view transition is available.
|
||||
* @returns Promise that resolves when the transition completes (or immediately if not supported)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Synchronous callback
|
||||
* withViewTransition(() => {
|
||||
* this.large = !this.large;
|
||||
* });
|
||||
*
|
||||
* // Async callback
|
||||
* await withViewTransition(async () => {
|
||||
* await this.updateData();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export const withViewTransition = (
|
||||
callback: (viewTransitionAvailable: boolean) => void | Promise<void>
|
||||
): Promise<void> => {
|
||||
if (document.startViewTransition) {
|
||||
return document.startViewTransition(() => callback(true)).finished;
|
||||
}
|
||||
|
||||
// Fallback: Execute callback directly without transition
|
||||
const result = callback(false);
|
||||
return result instanceof Promise ? result : Promise.resolve();
|
||||
};
|
||||
@@ -30,7 +30,6 @@ export class HaFilterChip extends FilterChip {
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--_label-text-font: var(--ha-font-family-body);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { titleCase } from "../common/string/title-case";
|
||||
import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
@@ -45,7 +44,7 @@ const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
||||
path: `/${panel.url_path}`,
|
||||
icon: panel.icon ?? "mdi:view-dashboard",
|
||||
title:
|
||||
panel.url_path === getDefaultPanelUrlPath(hass)
|
||||
panel.url_path === hass.defaultPanel
|
||||
? hass.localize("panel.states")
|
||||
: hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title ||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { StateSelector } from "../../data/selector";
|
||||
import { extractFromTarget } from "../../data/target";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-state-picker";
|
||||
@@ -27,29 +25,15 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public context?: {
|
||||
filter_attribute?: 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() {
|
||||
if (this.selector.state?.multiple) {
|
||||
return html`
|
||||
<ha-entity-states-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityIds}
|
||||
.entityId=${this.selector.state?.entity_id ||
|
||||
this.context?.filter_entity}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
@@ -66,7 +50,8 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
return html`
|
||||
<ha-entity-state-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityIds}
|
||||
.entityId=${this.selector.state?.entity_id ||
|
||||
this.context?.filter_entity}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
@@ -80,24 +65,6 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
></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 {
|
||||
|
||||
@@ -33,7 +33,6 @@ import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import type { PersistentNotification } from "../data/persistent_notification";
|
||||
import { subscribeNotifications } from "../data/persistent_notification";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
@@ -143,7 +142,7 @@ const defaultPanelSorter = (
|
||||
export const computePanels = memoizeOne(
|
||||
(
|
||||
panels: HomeAssistant["panels"],
|
||||
defaultPanel: string,
|
||||
defaultPanel: HomeAssistant["defaultPanel"],
|
||||
panelsOrder: string[],
|
||||
hiddenPanels: string[],
|
||||
locale: HomeAssistant["locale"]
|
||||
@@ -299,8 +298,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
hass.localize !== oldHass.localize ||
|
||||
hass.locale !== oldHass.locale ||
|
||||
hass.states !== oldHass.states ||
|
||||
hass.userData !== oldHass.userData ||
|
||||
hass.systemData !== oldHass.systemData ||
|
||||
hass.defaultPanel !== oldHass.defaultPanel ||
|
||||
hass.connected !== oldHass.connected
|
||||
);
|
||||
}
|
||||
@@ -403,11 +401,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
defaultPanel,
|
||||
this.hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels,
|
||||
this.hass.locale
|
||||
@@ -422,27 +418,23 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this._renderPanels(beforeSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderPanels(beforeSpacer, selectedPanel)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||
${this._renderExternalConfiguration()}
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanels(
|
||||
panels: PanelInfo[],
|
||||
selectedPanel: string,
|
||||
defaultPanel: string
|
||||
) {
|
||||
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||
return panels.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
panel.url_path === defaultPanel
|
||||
panel.url_path === this.hass.defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.icon,
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import {
|
||||
mdiAvTimer,
|
||||
mdiCalendar,
|
||||
mdiClockOutline,
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiFormatListBulleted,
|
||||
mdiGestureDoubleTap,
|
||||
mdiHomeAssistant,
|
||||
mdiMapMarker,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMessageAlert,
|
||||
mdiMicrophoneMessage,
|
||||
mdiNfcVariant,
|
||||
mdiNumeric,
|
||||
mdiStateMachine,
|
||||
mdiSwapHorizontal,
|
||||
mdiWeatherSunny,
|
||||
mdiWebhook,
|
||||
} from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
export const TRIGGER_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
device: mdiDevices,
|
||||
event: mdiGestureDoubleTap,
|
||||
state: mdiStateMachine,
|
||||
geo_location: mdiMapMarker,
|
||||
homeassistant: mdiHomeAssistant,
|
||||
mqtt: mdiSwapHorizontal,
|
||||
numeric_state: mdiNumeric,
|
||||
sun: mdiWeatherSunny,
|
||||
conversation: mdiMicrophoneMessage,
|
||||
tag: mdiNfcVariant,
|
||||
template: mdiCodeBraces,
|
||||
time: mdiClockOutline,
|
||||
time_pattern: mdiAvTimer,
|
||||
webhook: mdiWebhook,
|
||||
persistent_notification: mdiMessageAlert,
|
||||
zone: mdiMapMarkerRadius,
|
||||
list: mdiFormatListBulleted,
|
||||
};
|
||||
|
||||
@customElement("ha-trigger-icon")
|
||||
export class HaTriggerIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public trigger?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
}
|
||||
|
||||
if (!this.trigger) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = triggerIcon(this.hass, this.trigger).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
const domain = computeDomain(this.trigger!);
|
||||
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
.path=${TRIGGER_ICONS[this.trigger!] || FALLBACK_DOMAIN_ICONS[domain]}
|
||||
></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-trigger-icon": HaTriggerIcon;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ class SearchInput extends LitElement {
|
||||
return html`
|
||||
<ha-textfield
|
||||
.autofocus=${this.autofocus}
|
||||
autocomplete="off"
|
||||
.label=${this.label || this.hass.localize("ui.common.search")}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
|
||||
@@ -50,7 +50,7 @@ export const ACTION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
{
|
||||
groups: {
|
||||
device_id: {},
|
||||
dynamicGroups: {},
|
||||
serviceGroups: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -117,6 +117,14 @@ export const VIRTUAL_ACTIONS: Partial<
|
||||
},
|
||||
} 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 = [
|
||||
"ha-automation-action-choose",
|
||||
"ha-automation-action-condition",
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import type { WeekdayShort } from "../common/datetime/weekday";
|
||||
import { navigate } from "../common/navigate";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
@@ -14,19 +12,11 @@ import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import type { Action, Field, MODES } from "./script";
|
||||
import { migrateAutomationAction } from "./script";
|
||||
import type { TriggerDescription } from "./trigger";
|
||||
import type { WeekdayShort } from "../common/datetime/weekday";
|
||||
|
||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||
export const AUTOMATION_DEFAULT_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 {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
id?: string;
|
||||
@@ -96,12 +86,6 @@ export interface BaseTrigger {
|
||||
id?: string;
|
||||
variables?: Record<string, unknown>;
|
||||
enabled?: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PlatformTrigger extends BaseTrigger {
|
||||
trigger: Exclude<string, LegacyTrigger["trigger"]>;
|
||||
target?: HassServiceTarget;
|
||||
}
|
||||
|
||||
export interface StateTrigger extends BaseTrigger {
|
||||
@@ -211,7 +195,7 @@ export interface CalendarTrigger extends BaseTrigger {
|
||||
offset: string;
|
||||
}
|
||||
|
||||
export type LegacyTrigger =
|
||||
export type Trigger =
|
||||
| StateTrigger
|
||||
| MqttTrigger
|
||||
| GeoLocationTrigger
|
||||
@@ -228,9 +212,8 @@ export type LegacyTrigger =
|
||||
| TemplateTrigger
|
||||
| EventTrigger
|
||||
| DeviceTrigger
|
||||
| CalendarTrigger;
|
||||
|
||||
export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
|
||||
| CalendarTrigger
|
||||
| TriggerList;
|
||||
|
||||
interface BaseCondition {
|
||||
condition: string;
|
||||
@@ -592,7 +575,6 @@ export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
||||
insertAfter: (value: Trigger | Trigger[]) => boolean;
|
||||
toggleYamlMode: () => void;
|
||||
config: Trigger;
|
||||
description?: TriggerDescription;
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,8 @@ import {
|
||||
formatListWithAnds,
|
||||
formatListWithOrs,
|
||||
} from "../common/string/format-list";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { Condition, ForDict, LegacyTrigger, Trigger } from "./automation";
|
||||
import type { Condition, ForDict, Trigger } from "./automation";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import {
|
||||
localizeDeviceAutomationCondition,
|
||||
@@ -26,7 +25,8 @@ import {
|
||||
} from "./device_automation";
|
||||
import type { EntityRegistryEntry } from "./entity_registry";
|
||||
import type { FrontendLocaleData } from "./translation";
|
||||
import { getTriggerDomain, getTriggerObjectId, isTriggerList } from "./trigger";
|
||||
import { isTriggerList } from "./trigger";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
|
||||
const triggerTranslationBaseKey =
|
||||
"ui.panel.config.automation.editor.triggers.type";
|
||||
@@ -121,37 +121,6 @@ const tryDescribeTrigger = (
|
||||
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
|
||||
if (trigger.trigger === "event" && trigger.event_type) {
|
||||
const eventTypes: string[] = [];
|
||||
@@ -833,7 +802,13 @@ const describeLegacyTrigger = (
|
||||
}
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
|
||||
) ||
|
||||
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
|
||||
);
|
||||
};
|
||||
|
||||
export const describeCondition = (
|
||||
|
||||
@@ -360,35 +360,6 @@ export const getReferencedStatisticIds = (
|
||||
return statIDs;
|
||||
};
|
||||
|
||||
export const getReferencedStatisticIdsPower = (
|
||||
prefs: EnergyPreferences
|
||||
): string[] => {
|
||||
const statIDs: (string | undefined)[] = [];
|
||||
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "gas" || source.type === "water") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "solar") {
|
||||
statIDs.push(source.stat_rate);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "battery") {
|
||||
statIDs.push(source.stat_rate);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.power) {
|
||||
statIDs.push(...source.power.map((p) => p.stat_rate));
|
||||
}
|
||||
}
|
||||
statIDs.push(...prefs.device_consumption.map((d) => d.stat_rate));
|
||||
|
||||
return statIDs.filter(Boolean) as string[];
|
||||
};
|
||||
|
||||
export const enum CompareMode {
|
||||
NONE = "",
|
||||
PREVIOUS = "previous",
|
||||
@@ -436,10 +407,9 @@ const getEnergyData = async (
|
||||
"gas",
|
||||
"device",
|
||||
]);
|
||||
const powerStatIds = getReferencedStatisticIdsPower(prefs);
|
||||
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
|
||||
|
||||
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
|
||||
const allStatIDs = [...energyStatIds, ...waterStatIds];
|
||||
|
||||
const dayDifference = differenceInDays(end || new Date(), start);
|
||||
const period =
|
||||
@@ -450,8 +420,6 @@ const getEnergyData = async (
|
||||
: dayDifference > 2
|
||||
? "day"
|
||||
: "hour";
|
||||
const finePeriod =
|
||||
dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
|
||||
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
const statsMetadataArray = allStatIDs.length
|
||||
@@ -473,9 +441,6 @@ const getEnergyData = async (
|
||||
? (gasUnit as (typeof VOLUME_UNITS)[number])
|
||||
: undefined,
|
||||
};
|
||||
const powerUnits: StatisticsUnitConfiguration = {
|
||||
power: "kW",
|
||||
};
|
||||
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
|
||||
const waterUnits: StatisticsUnitConfiguration = {
|
||||
volume: waterUnit,
|
||||
@@ -486,12 +451,6 @@ const getEnergyData = async (
|
||||
"change",
|
||||
])
|
||||
: {};
|
||||
const _powerStats: Statistics | Promise<Statistics> = powerStatIds.length
|
||||
? fetchStatistics(hass!, start, end, powerStatIds, finePeriod, powerUnits, [
|
||||
"mean",
|
||||
])
|
||||
: {};
|
||||
|
||||
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
|
||||
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
|
||||
"change",
|
||||
@@ -598,7 +557,6 @@ const getEnergyData = async (
|
||||
|
||||
const [
|
||||
energyStats,
|
||||
powerStats,
|
||||
waterStats,
|
||||
energyStatsCompare,
|
||||
waterStatsCompare,
|
||||
@@ -606,14 +564,13 @@ const getEnergyData = async (
|
||||
fossilEnergyConsumptionCompare,
|
||||
] = await Promise.all([
|
||||
_energyStats,
|
||||
_powerStats,
|
||||
_waterStats,
|
||||
_energyStatsCompare,
|
||||
_waterStatsCompare,
|
||||
_fossilEnergyConsumption,
|
||||
_fossilEnergyConsumptionCompare,
|
||||
]);
|
||||
const stats = { ...energyStats, ...waterStats, ...powerStats };
|
||||
const stats = { ...energyStats, ...waterStats };
|
||||
if (compare) {
|
||||
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
export interface CoreFrontendUserData {
|
||||
showAdvanced?: boolean;
|
||||
showEntityIdPicker?: boolean;
|
||||
defaultPanel?: string;
|
||||
}
|
||||
|
||||
export interface SidebarFrontendUserData {
|
||||
@@ -11,24 +10,15 @@ export interface SidebarFrontendUserData {
|
||||
hiddenPanels: string[];
|
||||
}
|
||||
|
||||
export interface CoreFrontendSystemData {
|
||||
defaultPanel?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
core: CoreFrontendUserData;
|
||||
sidebar: SidebarFrontendUserData;
|
||||
}
|
||||
interface FrontendSystemData {
|
||||
core: CoreFrontendSystemData;
|
||||
}
|
||||
}
|
||||
|
||||
export type ValidUserDataKey = keyof FrontendUserData;
|
||||
|
||||
export type ValidSystemDataKey = keyof FrontendSystemData;
|
||||
|
||||
export const fetchFrontendUserData = async <
|
||||
UserDataKey extends ValidUserDataKey,
|
||||
>(
|
||||
@@ -69,46 +59,3 @@ export const subscribeFrontendUserData = <UserDataKey extends ValidUserDataKey>(
|
||||
key: userDataKey,
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchFrontendSystemData = async <
|
||||
SystemDataKey extends ValidSystemDataKey,
|
||||
>(
|
||||
conn: Connection,
|
||||
key: SystemDataKey
|
||||
): Promise<FrontendSystemData[SystemDataKey] | null> => {
|
||||
const result = await conn.sendMessagePromise<{
|
||||
value: FrontendSystemData[SystemDataKey] | null;
|
||||
}>({
|
||||
type: "frontend/get_system_data",
|
||||
key,
|
||||
});
|
||||
return result.value;
|
||||
};
|
||||
|
||||
export const saveFrontendSystemData = async <
|
||||
SystemDataKey extends ValidSystemDataKey,
|
||||
>(
|
||||
conn: Connection,
|
||||
key: SystemDataKey,
|
||||
value: FrontendSystemData[SystemDataKey]
|
||||
): Promise<void> =>
|
||||
conn.sendMessagePromise<undefined>({
|
||||
type: "frontend/set_system_data",
|
||||
key,
|
||||
value,
|
||||
});
|
||||
|
||||
export const subscribeFrontendSystemData = <
|
||||
SystemDataKey extends ValidSystemDataKey,
|
||||
>(
|
||||
conn: Connection,
|
||||
systemDataKey: SystemDataKey,
|
||||
onChange: (data: { value: FrontendSystemData[SystemDataKey] | null }) => void
|
||||
) =>
|
||||
conn.subscribeMessage<{ value: FrontendSystemData[SystemDataKey] | null }>(
|
||||
onChange,
|
||||
{
|
||||
type: "frontend/subscribe_system_data",
|
||||
key: systemDataKey,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -59,7 +59,6 @@ import type {
|
||||
} from "./entity_registry";
|
||||
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
|
||||
|
||||
/** Icon to use when no icon specified for service. */
|
||||
export const DEFAULT_SERVICE_ICON = mdiRoomService;
|
||||
@@ -134,19 +133,14 @@ const resources: {
|
||||
all?: Promise<Record<string, ServiceIcons>>;
|
||||
domains: Record<string, ServiceIcons | Promise<ServiceIcons>>;
|
||||
};
|
||||
triggers: {
|
||||
all?: Promise<Record<string, TriggerIcons>>;
|
||||
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
|
||||
};
|
||||
} = {
|
||||
entity: {},
|
||||
entity_component: {},
|
||||
services: { domains: {} },
|
||||
triggers: { domains: {} },
|
||||
};
|
||||
|
||||
interface IconResources<
|
||||
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
|
||||
T extends ComponentIcons | PlatformIcons | ServiceIcons,
|
||||
> {
|
||||
resources: Record<string, T>;
|
||||
}
|
||||
@@ -190,22 +184,12 @@ type ServiceIcons = Record<
|
||||
{ service: string; sections?: Record<string, string> }
|
||||
>;
|
||||
|
||||
type TriggerIcons = Record<
|
||||
string,
|
||||
{ trigger: string; sections?: Record<string, string> }
|
||||
>;
|
||||
|
||||
export type IconCategory =
|
||||
| "entity"
|
||||
| "entity_component"
|
||||
| "services"
|
||||
| "triggers";
|
||||
export type IconCategory = "entity" | "entity_component" | "services";
|
||||
|
||||
interface CategoryType {
|
||||
entity: PlatformIcons;
|
||||
entity_component: ComponentIcons;
|
||||
services: ServiceIcons;
|
||||
triggers: TriggerIcons;
|
||||
}
|
||||
|
||||
export const getHassIcons = async <T extends IconCategory>(
|
||||
@@ -274,59 +258,42 @@ export const getComponentIcons = async (
|
||||
return resources.entity_component.resources.then((res) => res[domain]);
|
||||
};
|
||||
|
||||
export const getCategoryIcons = async <
|
||||
T extends Exclude<IconCategory, "entity" | "entity_component">,
|
||||
>(
|
||||
export const getServiceIcons = async (
|
||||
hass: HomeAssistant,
|
||||
category: T,
|
||||
domain?: string,
|
||||
force = false
|
||||
): Promise<CategoryType[T] | Record<string, CategoryType[T]> | undefined> => {
|
||||
): Promise<ServiceIcons | Record<string, ServiceIcons> | undefined> => {
|
||||
if (!domain) {
|
||||
if (!force && resources[category].all) {
|
||||
return resources[category].all as Promise<
|
||||
Record<string, CategoryType[T]>
|
||||
>;
|
||||
if (!force && resources.services.all) {
|
||||
return resources.services.all;
|
||||
}
|
||||
resources[category].all = getHassIcons(hass, category).then((res) => {
|
||||
resources[category].domains = res.resources as any;
|
||||
return res?.resources as Record<string, CategoryType[T]>;
|
||||
}) as any;
|
||||
return resources[category].all as Promise<Record<string, CategoryType[T]>>;
|
||||
resources.services.all = getHassIcons(hass, "services", domain).then(
|
||||
(res) => {
|
||||
resources.services.domains = res.resources;
|
||||
return res?.resources;
|
||||
}
|
||||
);
|
||||
return resources.services.all;
|
||||
}
|
||||
if (!force && domain in resources[category].domains) {
|
||||
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
||||
if (!force && domain in resources.services.domains) {
|
||||
return resources.services.domains[domain];
|
||||
}
|
||||
if (resources[category].all && !force) {
|
||||
await resources[category].all;
|
||||
if (domain in resources[category].domains) {
|
||||
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
||||
if (resources.services.all && !force) {
|
||||
await resources.services.all;
|
||||
if (domain in resources.services.domains) {
|
||||
return resources.services.domains[domain];
|
||||
}
|
||||
}
|
||||
if (!isComponentLoaded(hass, domain)) {
|
||||
return undefined;
|
||||
}
|
||||
const result = getHassIcons(hass, category, domain);
|
||||
resources[category].domains[domain] = result.then(
|
||||
const result = getHassIcons(hass, "services", domain);
|
||||
resources.services.domains[domain] = result.then(
|
||||
(res) => res?.resources[domain]
|
||||
) as any;
|
||||
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
||||
);
|
||||
return resources.services.domains[domain];
|
||||
};
|
||||
|
||||
export const getServiceIcons = async (
|
||||
hass: HomeAssistant,
|
||||
domain?: string,
|
||||
force = false
|
||||
): Promise<ServiceIcons | Record<string, ServiceIcons> | undefined> =>
|
||||
getCategoryIcons(hass, "services", domain, force);
|
||||
|
||||
export const getTriggerIcons = async (
|
||||
hass: HomeAssistant,
|
||||
domain?: string,
|
||||
force = false
|
||||
): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> =>
|
||||
getCategoryIcons(hass, "triggers", domain, force);
|
||||
|
||||
// Cache for sorted range keys
|
||||
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
|
||||
|
||||
@@ -506,26 +473,6 @@ export const attributeIcon = async (
|
||||
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 (
|
||||
hass: HomeAssistant,
|
||||
service: string
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
export const DEFAULT_PANEL = "lovelace";
|
||||
|
||||
export const getLegacyDefaultPanelUrlPath = (): string | null => {
|
||||
export const getStorageDefaultPanelUrlPath = (): string => {
|
||||
const defaultPanel = window.localStorage.getItem("defaultPanel");
|
||||
return defaultPanel ? JSON.parse(defaultPanel) : null;
|
||||
|
||||
return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL;
|
||||
};
|
||||
|
||||
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string =>
|
||||
hass.userData?.defaultPanel ||
|
||||
hass.systemData?.defaultPanel ||
|
||||
getLegacyDefaultPanelUrlPath() ||
|
||||
DEFAULT_PANEL;
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
|
||||
const panel = getDefaultPanelUrlPath(hass);
|
||||
|
||||
return (panel ? hass.panels[panel] : undefined) ?? hass.panels[DEFAULT_PANEL];
|
||||
export const setDefaultPanel = (
|
||||
element: HTMLElement,
|
||||
urlPath: string
|
||||
): void => {
|
||||
fireEvent(element, "hass-default-panel", { defaultPanel: urlPath });
|
||||
};
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
|
||||
hass.panels[hass.defaultPanel]
|
||||
? hass.panels[hass.defaultPanel]
|
||||
: hass.panels[DEFAULT_PANEL];
|
||||
|
||||
export const getPanelNameTranslationKey = (panel: PanelInfo) => {
|
||||
if (panel.url_path === "lovelace") {
|
||||
return "panel.states" as const;
|
||||
|
||||
@@ -28,7 +28,6 @@ export interface TodoItem {
|
||||
status: TodoItemStatus | null;
|
||||
description?: string | null;
|
||||
due?: string | null;
|
||||
completed?: string | null;
|
||||
}
|
||||
|
||||
export const enum TodoListEntityFeature {
|
||||
|
||||
@@ -73,8 +73,7 @@ export type TranslationCategory =
|
||||
| "application_credentials"
|
||||
| "issues"
|
||||
| "selector"
|
||||
| "services"
|
||||
| "triggers";
|
||||
| "services";
|
||||
|
||||
export const subscribeTranslationPreferences = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -1,20 +1,57 @@
|
||||
import { mdiMapClock, mdiShape } from "@mdi/js";
|
||||
import {
|
||||
mdiAvTimer,
|
||||
mdiCalendar,
|
||||
mdiClockOutline,
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiFormatListBulleted,
|
||||
mdiGestureDoubleTap,
|
||||
mdiMapClock,
|
||||
mdiMapMarker,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMessageAlert,
|
||||
mdiMicrophoneMessage,
|
||||
mdiNfcVariant,
|
||||
mdiNumeric,
|
||||
mdiShape,
|
||||
mdiStateMachine,
|
||||
mdiSwapHorizontal,
|
||||
mdiWeatherSunny,
|
||||
mdiWebhook,
|
||||
} from "@mdi/js";
|
||||
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import type {
|
||||
AutomationElementGroupCollection,
|
||||
Trigger,
|
||||
TriggerList,
|
||||
} 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[] = [
|
||||
{
|
||||
groups: {
|
||||
device: {},
|
||||
dynamicGroups: {},
|
||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||
time_location: {
|
||||
icon: mdiMapClock,
|
||||
@@ -46,33 +83,3 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
|
||||
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
|
||||
"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,8 +2,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-formfield";
|
||||
import "../../components/ha-switch";
|
||||
import "../../components/ha-button";
|
||||
@@ -29,8 +28,6 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(
|
||||
params: ConfigEntrySystemOptionsDialogParams
|
||||
): Promise<void> {
|
||||
@@ -38,14 +35,9 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
this._error = undefined;
|
||||
this._disableNewEntities = params.entry.pref_disable_new_entities;
|
||||
this._disablePolling = params.entry.pref_disable_polling;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
@@ -57,19 +49,18 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.title",
|
||||
{
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.dialogs.config_entry_system_options.title", {
|
||||
integration:
|
||||
this.hass.localize(
|
||||
`component.${this._params.entry.domain}.title`
|
||||
) || this._params.entry.domain,
|
||||
}
|
||||
})
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
<ha-formfield
|
||||
@@ -91,10 +82,10 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
</p>`}
|
||||
>
|
||||
<ha-switch
|
||||
autofocus
|
||||
.checked=${!this._disableNewEntities}
|
||||
@change=${this._disableNewEntitiesChanged}
|
||||
.disabled=${this._submitting}
|
||||
dialogInitialFocus
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
|
||||
@@ -122,27 +113,22 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
.disabled=${this._submitting}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.update"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from "../../data/frontend";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { getDefaultPanelUrlPath } from "../../data/panel";
|
||||
|
||||
@customElement("dialog-edit-sidebar")
|
||||
class DialogEditSidebar extends LitElement {
|
||||
@@ -95,11 +94,9 @@ class DialogEditSidebar extends LitElement {
|
||||
|
||||
const panels = this._panels(this.hass.panels);
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
defaultPanel,
|
||||
this.hass.defaultPanel,
|
||||
this._order,
|
||||
this._hidden,
|
||||
this.hass.locale
|
||||
@@ -123,12 +120,12 @@ class DialogEditSidebar extends LitElement {
|
||||
].map((panel) => ({
|
||||
value: panel.url_path,
|
||||
label:
|
||||
panel.url_path === defaultPanel
|
||||
panel.url_path === this.hass.defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
|
||||
icon: panel.icon || undefined,
|
||||
iconPath:
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
|
||||
@@ -40,6 +40,12 @@
|
||||
color: var(--primary-text-color, #212121);
|
||||
height: 100vh;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: var(--primary-background-color, #111111);
|
||||
color: var(--primary-text-color, #e1e1e1);
|
||||
}
|
||||
}
|
||||
#ha-launch-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -56,6 +62,12 @@
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
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 {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -80,14 +92,6 @@
|
||||
opacity: .66;
|
||||
}
|
||||
@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 {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -46,13 +46,10 @@ export class HomeAssistantMain extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const sidebarNarrow = this._sidebarNarrow || this._externalSidebar;
|
||||
|
||||
const isPanelReady =
|
||||
this.hass.panels && this.hass.userData && this.hass.systemData;
|
||||
|
||||
return html`
|
||||
<ha-drawer
|
||||
.type=${sidebarNarrow ? "modal" : ""}
|
||||
.open=${sidebarNarrow ? this._drawerOpen : false}
|
||||
.open=${sidebarNarrow ? this._drawerOpen : undefined}
|
||||
.direction=${computeRTLDirection(this.hass)}
|
||||
@MDCDrawer:closed=${this._drawerClosed}
|
||||
>
|
||||
@@ -62,14 +59,12 @@ export class HomeAssistantMain extends LitElement {
|
||||
.route=${this.route}
|
||||
.alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"}
|
||||
></ha-sidebar>
|
||||
${isPanelReady
|
||||
? html`<partial-panel-resolver
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
slot="appContent"
|
||||
></partial-panel-resolver>`
|
||||
: nothing}
|
||||
<partial-panel-resolver
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
slot="appContent"
|
||||
></partial-panel-resolver>
|
||||
</ha-drawer>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { getStorageDefaultPanelUrlPath } from "../data/panel";
|
||||
import type { WindowWithPreloads } from "../data/preloads";
|
||||
import type { RecorderInfo } from "../data/recorder";
|
||||
import { getRecorderInfo } from "../data/recorder";
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../util/register-service-worker";
|
||||
import "./ha-init-page";
|
||||
import "./home-assistant-main";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
|
||||
const useHash = __DEMO__;
|
||||
const curPath = () =>
|
||||
@@ -52,6 +53,11 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
super();
|
||||
const path = curPath();
|
||||
|
||||
if (["", "/"].includes(path)) {
|
||||
navigate(`/${getStorageDefaultPanelUrlPath()}${location.search}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
this._route = {
|
||||
prefix: "",
|
||||
path,
|
||||
|
||||
@@ -14,13 +14,11 @@ import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
ACTION_BUILDING_BLOCKS,
|
||||
getService,
|
||||
isService,
|
||||
VIRTUAL_ACTIONS,
|
||||
} from "../../../../data/action";
|
||||
import {
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type AutomationClipboard,
|
||||
} from "../../../../data/automation";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import type { Action } from "../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
@@ -219,9 +217,9 @@ export default class HaAutomationAction extends LitElement {
|
||||
actions = this.actions.concat(deepClone(this._clipboard!.action));
|
||||
} else if (action in VIRTUAL_ACTIONS) {
|
||||
actions = this.actions.concat(VIRTUAL_ACTIONS[action]);
|
||||
} else if (isDynamic(action)) {
|
||||
} else if (isService(action)) {
|
||||
actions = this.actions.concat({
|
||||
action: getValueFromDynamic(action),
|
||||
action: getService(action),
|
||||
metadata: {},
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
@@ -41,39 +40,32 @@ import "../../../components/ha-md-list";
|
||||
import type { HaMdList } from "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-service-icon";
|
||||
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/search-input";
|
||||
import {
|
||||
ACTION_BUILDING_BLOCKS_GROUP,
|
||||
ACTION_COLLECTIONS,
|
||||
ACTION_ICONS,
|
||||
SERVICE_PREFIX,
|
||||
getService,
|
||||
isService,
|
||||
} from "../../../data/action";
|
||||
import {
|
||||
DYNAMIC_PREFIX,
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type AutomationElementGroup,
|
||||
type AutomationElementGroupCollection,
|
||||
import type {
|
||||
AutomationElementGroup,
|
||||
AutomationElementGroupCollection,
|
||||
} from "../../../data/automation";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS_GROUP,
|
||||
CONDITION_COLLECTIONS,
|
||||
CONDITION_ICONS,
|
||||
} from "../../../data/condition";
|
||||
import { getServiceIcons, getTriggerIcons } from "../../../data/icons";
|
||||
import { getServiceIcons } from "../../../data/icons";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import {
|
||||
domainToName,
|
||||
fetchIntegrationManifests,
|
||||
} from "../../../data/integration";
|
||||
import type { TriggerDescriptions } from "../../../data/trigger";
|
||||
import {
|
||||
TRIGGER_COLLECTIONS,
|
||||
getTriggerDomain,
|
||||
getTriggerObjectId,
|
||||
subscribeTriggers,
|
||||
} from "../../../data/trigger";
|
||||
import { TRIGGER_COLLECTIONS, TRIGGER_ICONS } from "../../../data/trigger";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { HaFuse } from "../../../resources/fuse";
|
||||
@@ -119,7 +111,7 @@ const ENTITY_DOMAINS_OTHER = new Set([
|
||||
|
||||
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
|
||||
|
||||
const ACTION_SERVICE_KEYWORDS = ["dynamicGroups", "helpers", "other"];
|
||||
const ACTION_SERVICE_KEYWORDS = ["serviceGroups", "helpers", "other"];
|
||||
|
||||
@customElement("add-automation-element-dialog")
|
||||
class DialogAddAutomationElement
|
||||
@@ -150,8 +142,6 @@ class DialogAddAutomationElement
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
||||
|
||||
@query(".items ha-md-list ha-md-list-item")
|
||||
private _itemsListFirstElement?: HaMdList;
|
||||
|
||||
@@ -162,8 +152,6 @@ class DialogAddAutomationElement
|
||||
|
||||
private _removeKeyboardShortcuts?: () => void;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
public showDialog(params): void {
|
||||
this._params = params;
|
||||
|
||||
@@ -175,17 +163,6 @@ class DialogAddAutomationElement
|
||||
this._calculateUsedDomains();
|
||||
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(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
@@ -199,10 +176,6 @@ class DialogAddAutomationElement
|
||||
|
||||
public closeDialog() {
|
||||
this.removeKeyboardShortcuts();
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this._params) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -344,11 +317,6 @@ class DialogAddAutomationElement
|
||||
);
|
||||
|
||||
const items = flattenGroups(groups).flat();
|
||||
if (type === "trigger") {
|
||||
items.push(
|
||||
...this._triggers(localize, this._triggerDescriptions, manifests)
|
||||
);
|
||||
}
|
||||
if (type === "action") {
|
||||
items.push(...this._services(localize, services, manifests));
|
||||
}
|
||||
@@ -371,7 +339,6 @@ class DialogAddAutomationElement
|
||||
domains: Set<string> | undefined,
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
triggerDescriptions: TriggerDescriptions,
|
||||
manifests?: DomainManifestLookup
|
||||
): {
|
||||
titleKey?: LocalizeKeys;
|
||||
@@ -395,32 +362,7 @@ class DialogAddAutomationElement
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
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
|
||||
collection.groups.serviceGroups
|
||||
? undefined
|
||||
: collection.groups.helpers
|
||||
? "helper"
|
||||
@@ -487,19 +429,10 @@ class DialogAddAutomationElement
|
||||
services: HomeAssistant["services"],
|
||||
manifests?: DomainManifestLookup
|
||||
): ListItem[] => {
|
||||
if (type === "action" && isDynamic(group)) {
|
||||
if (type === "action" && isService(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 result = Object.entries(groups).map(([key, options]) =>
|
||||
@@ -581,7 +514,7 @@ class DialogAddAutomationElement
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
`,
|
||||
key: `${DYNAMIC_PREFIX}${domain}`,
|
||||
key: `${SERVICE_PREFIX}${domain}`,
|
||||
name: domainToName(localize, domain, manifest),
|
||||
description: "",
|
||||
});
|
||||
@@ -592,102 +525,6 @@ 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(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
@@ -702,8 +539,8 @@ class DialogAddAutomationElement
|
||||
|
||||
let domain: string | undefined;
|
||||
|
||||
if (isDynamic(group)) {
|
||||
domain = getValueFromDynamic(group!);
|
||||
if (isService(group)) {
|
||||
domain = getService(group!);
|
||||
}
|
||||
|
||||
const addDomain = (dmn: string) => {
|
||||
@@ -717,7 +554,7 @@ class DialogAddAutomationElement
|
||||
.service=${`${dmn}.${service}`}
|
||||
></ha-service-icon>
|
||||
`,
|
||||
key: `${DYNAMIC_PREFIX}${dmn}.${service}`,
|
||||
key: `${SERVICE_PREFIX}${dmn}.${service}`,
|
||||
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
|
||||
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
|
||||
services[dmn][service]?.name ||
|
||||
@@ -831,15 +668,14 @@ class DialogAddAutomationElement
|
||||
this._domains,
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._triggerDescriptions,
|
||||
this._manifests
|
||||
);
|
||||
|
||||
const groupName = isDynamic(this._selectedGroup)
|
||||
const groupName = isService(this._selectedGroup)
|
||||
? domainToName(
|
||||
this.hass.localize,
|
||||
getValueFromDynamic(this._selectedGroup!),
|
||||
this._manifests?.[getValueFromDynamic(this._selectedGroup!)]
|
||||
getService(this._selectedGroup!),
|
||||
this._manifests?.[getService(this._selectedGroup!)]
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
|
||||
@@ -1336,7 +1172,7 @@ class DialogAddAutomationElement
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-1);
|
||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-family-body);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||
--md-list-item-one-line-container-height: var(--ha-space-10);
|
||||
}
|
||||
ha-bottom-sheet .groups {
|
||||
@@ -1400,7 +1236,7 @@ class DialogAddAutomationElement
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-2);
|
||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-family-body);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-0) var(--ha-space-4);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import type HaAutomationConditionEditor from "../action/ha-automation-action-edi
|
||||
import { getAutomationActionType } from "../action/ha-automation-action-row";
|
||||
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "../trigger/ha-automation-trigger-editor";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@customElement("ha-automation-sidebar-action")
|
||||
|
||||
@@ -17,6 +17,7 @@ import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@ import {
|
||||
} from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
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 { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
|
||||
@@ -15,15 +15,8 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import type {
|
||||
LegacyTrigger,
|
||||
TriggerSidebarConfig,
|
||||
} from "../../../../data/automation";
|
||||
import {
|
||||
getTriggerDomain,
|
||||
getTriggerObjectId,
|
||||
isTriggerList,
|
||||
} from "../../../../data/trigger";
|
||||
import type { TriggerSidebarConfig } from "../../../../data/automation";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
@@ -70,7 +63,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
|
||||
protected render() {
|
||||
const rowDisabled =
|
||||
"enabled" in this.config.config && this.config.config.enabled === false;
|
||||
this.disabled ||
|
||||
("enabled" in this.config.config && this.config.config.enabled === false);
|
||||
const type = isTriggerList(this.config.config)
|
||||
? "list"
|
||||
: this.config.config.trigger;
|
||||
@@ -79,18 +73,9 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
"ui.panel.config.automation.editor.triggers.trigger"
|
||||
);
|
||||
|
||||
const domain =
|
||||
"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`);
|
||||
const title = this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.${type}.label`
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-automation-sidebar-card
|
||||
@@ -284,7 +269,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.trigger=${this.config.config}
|
||||
.description=${this.config.description}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
@yaml-changed=${this._yamlChangedSidebar}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
|
||||
@@ -9,12 +9,10 @@ import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { Trigger } from "../../../../data/automation";
|
||||
import { migrateAutomationTrigger } from "../../../../data/automation";
|
||||
import type { TriggerDescription } from "../../../../data/trigger";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
import "./types/ha-automation-trigger-platform";
|
||||
|
||||
@customElement("ha-automation-trigger-editor")
|
||||
export default class HaAutomationTriggerEditor extends LitElement {
|
||||
@@ -33,8 +31,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
|
||||
|
||||
@property({ attribute: false }) public description?: TriggerDescription;
|
||||
|
||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||
|
||||
protected render() {
|
||||
@@ -91,18 +87,11 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${this.description
|
||||
? html`<ha-automation-trigger-platform
|
||||
.hass=${this.hass}
|
||||
.trigger=${this.trigger}
|
||||
.description=${this.description}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-trigger-platform>`
|
||||
: dynamicElement(`ha-automation-trigger-${type}`, {
|
||||
hass: this.hass,
|
||||
trigger: this.trigger,
|
||||
disabled: this.disabled,
|
||||
})}
|
||||
${dynamicElement(`ha-automation-trigger-${type}`, {
|
||||
hass: this.hass,
|
||||
trigger: this.trigger,
|
||||
disabled: this.disabled,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
@@ -40,11 +40,9 @@ import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Trigger,
|
||||
TriggerList,
|
||||
TriggerSidebarConfig,
|
||||
} from "../../../../data/automation";
|
||||
import { isTrigger, subscribeTrigger } from "../../../../data/automation";
|
||||
@@ -52,8 +50,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
@@ -75,7 +72,6 @@ import "./types/ha-automation-trigger-list";
|
||||
import "./types/ha-automation-trigger-mqtt";
|
||||
import "./types/ha-automation-trigger-numeric_state";
|
||||
import "./types/ha-automation-trigger-persistent_notification";
|
||||
import "./types/ha-automation-trigger-platform";
|
||||
import "./types/ha-automation-trigger-state";
|
||||
import "./types/ha-automation-trigger-sun";
|
||||
import "./types/ha-automation-trigger-tag";
|
||||
@@ -141,9 +137,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public triggerDescriptions: TriggerDescriptions = {};
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@query("ha-automation-trigger-editor")
|
||||
@@ -185,24 +178,18 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
const type = this._getType(this.trigger, this.triggerDescriptions);
|
||||
const type = this._getType(this.trigger);
|
||||
|
||||
const supported = this._uiSupported(type);
|
||||
|
||||
const yamlMode = this._yamlMode || !supported;
|
||||
|
||||
return html`
|
||||
${type === "list"
|
||||
? html`<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="trigger-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>`}
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="trigger-icon"
|
||||
.path=${TRIGGER_ICONS[type]}
|
||||
></ha-svg-icon>
|
||||
<h3 slot="header">
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
</h3>
|
||||
@@ -406,9 +393,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
<ha-automation-trigger-editor
|
||||
.hass=${this.hass}
|
||||
.trigger=${this.trigger}
|
||||
.description=${"trigger" in this.trigger
|
||||
? this.triggerDescriptions[this.trigger.trigger]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.uiSupported=${supported}
|
||||
@@ -568,7 +552,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
public openSidebar(trigger?: Trigger): void {
|
||||
trigger = trigger || this.trigger;
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
@@ -593,14 +576,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
duplicate: this._duplicateTrigger,
|
||||
cut: this._cutTrigger,
|
||||
insertAfter: this._insertAfter,
|
||||
config: trigger,
|
||||
uiSupported: this._uiSupported(
|
||||
this._getType(trigger, this.triggerDescriptions)
|
||||
),
|
||||
description:
|
||||
"trigger" in trigger
|
||||
? this.triggerDescriptions[trigger.trigger]
|
||||
: undefined,
|
||||
config: trigger || this.trigger,
|
||||
uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
|
||||
yamlMode: this._yamlMode,
|
||||
} satisfies TriggerSidebarConfig);
|
||||
this._selected = true;
|
||||
@@ -782,18 +759,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _getType = memoizeOne(
|
||||
(trigger: Trigger, triggerDescriptions: TriggerDescriptions) => {
|
||||
if (isTriggerList(trigger)) {
|
||||
return "list";
|
||||
}
|
||||
|
||||
if (trigger.trigger in triggerDescriptions) {
|
||||
return "platform";
|
||||
}
|
||||
|
||||
return trigger.trigger;
|
||||
}
|
||||
private _getType = memoizeOne((trigger: Trigger) =>
|
||||
isTriggerList(trigger) ? "list" : trigger.trigger
|
||||
);
|
||||
|
||||
private _uiSupported = memoizeOne(
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
@@ -13,16 +12,12 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type AutomationClipboard,
|
||||
type Trigger,
|
||||
type TriggerList,
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Trigger,
|
||||
TriggerList,
|
||||
} from "../../../../data/automation";
|
||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
||||
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
@@ -31,9 +26,10 @@ import {
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import "./ha-automation-trigger-row";
|
||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
|
||||
@customElement("ha-automation-trigger")
|
||||
export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
export default class HaAutomationTrigger extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public triggers!: Trigger[];
|
||||
@@ -66,23 +62,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
|
||||
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() {
|
||||
return html`
|
||||
<ha-sortable
|
||||
@@ -106,7 +85,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
.first=${idx === 0}
|
||||
.last=${idx === this.triggers.length - 1}
|
||||
.trigger=${trg}
|
||||
.triggerDescriptions=${this._triggerDescriptions}
|
||||
@duplicate=${this._duplicateTrigger}
|
||||
@insert-after=${this._insertAfter}
|
||||
@move-down=${this._moveDown}
|
||||
@@ -178,10 +156,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
let triggers: Trigger[];
|
||||
if (value === PASTE_VALUE) {
|
||||
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
|
||||
} else if (isDynamic(value)) {
|
||||
triggers = this.triggers.concat({
|
||||
trigger: getValueFromDynamic(value),
|
||||
});
|
||||
} else {
|
||||
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
|
||||
const elClass = customElements.get(
|
||||
|
||||
@@ -1,416 +0,0 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-selector/ha-selector";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
import type { PlatformTrigger } from "../../../../../data/automation";
|
||||
import type { IntegrationManifest } from "../../../../../data/integration";
|
||||
import { fetchIntegrationManifest } from "../../../../../data/integration";
|
||||
import type { TargetSelector } from "../../../../../data/selector";
|
||||
import {
|
||||
getTriggerDomain,
|
||||
getTriggerObjectId,
|
||||
type TriggerDescription,
|
||||
} from "../../../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
||||
|
||||
const showOptionalToggle = (field: TriggerDescription["fields"][string]) =>
|
||||
field.selector &&
|
||||
!field.required &&
|
||||
!("boolean" in field.selector && field.default);
|
||||
|
||||
@customElement("ha-automation-trigger-platform")
|
||||
export class HaPlatformTrigger extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trigger!: PlatformTrigger;
|
||||
|
||||
@property({ attribute: false }) public description?: TriggerDescription;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _checkedKeys = new Set();
|
||||
|
||||
@state() private _manifest?: IntegrationManifest;
|
||||
|
||||
public static get defaultConfig(): PlatformTrigger {
|
||||
return { trigger: "" };
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadBackendTranslation("triggers");
|
||||
this.hass.loadBackendTranslation("selector");
|
||||
}
|
||||
if (!changedProperties.has("trigger")) {
|
||||
return;
|
||||
}
|
||||
const oldValue = changedProperties.get("trigger") as
|
||||
| undefined
|
||||
| this["trigger"];
|
||||
|
||||
// Fetch the manifest if we have a trigger selected and the trigger domain changed.
|
||||
// If no trigger is selected, clear the manifest.
|
||||
if (this.trigger?.trigger) {
|
||||
const domain = getTriggerDomain(this.trigger.trigger);
|
||||
|
||||
const oldDomain = getTriggerDomain(oldValue?.trigger || "");
|
||||
|
||||
if (domain !== oldDomain) {
|
||||
this._fetchManifest(domain);
|
||||
}
|
||||
} else {
|
||||
this._manifest = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const domain = getTriggerDomain(this.trigger.trigger);
|
||||
const triggerName = getTriggerObjectId(this.trigger.trigger);
|
||||
|
||||
const description = this.hass.localize(
|
||||
`component.${domain}.triggers.${triggerName}.description`
|
||||
);
|
||||
|
||||
const triggerDesc = this.description;
|
||||
|
||||
const shouldRenderDataYaml = !triggerDesc?.fields;
|
||||
|
||||
const hasOptional = Boolean(
|
||||
triggerDesc?.fields &&
|
||||
Object.values(triggerDesc.fields).some((field) =>
|
||||
showOptionalToggle(field)
|
||||
)
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="description">
|
||||
${description ? html`<p>${description}</p>` : nothing}
|
||||
${this._manifest
|
||||
? html`<a
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._manifest.domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
class="help-icon"
|
||||
></ha-icon-button>
|
||||
</a>`
|
||||
: nothing}
|
||||
</div>
|
||||
${triggerDesc && "target" in triggerDesc
|
||||
? html`<ha-settings-row narrow>
|
||||
${hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: nothing}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_secondary"
|
||||
)}</span
|
||||
><ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._targetSelector(triggerDesc.target)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this.trigger?.target}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
: nothing}
|
||||
${shouldRenderDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.service-control.action_data"
|
||||
)}
|
||||
.name=${"data"}
|
||||
.readOnly=${this.disabled}
|
||||
.defaultValue=${this.trigger?.options}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: Object.entries(triggerDesc.fields).map(([fieldName, dataField]) =>
|
||||
this._renderField(
|
||||
fieldName,
|
||||
dataField,
|
||||
hasOptional,
|
||||
domain,
|
||||
triggerName
|
||||
)
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetSelector = memoizeOne(
|
||||
(targetSelector: TargetSelector["target"] | null | undefined) =>
|
||||
targetSelector ? { target: { ...targetSelector } } : { target: {} }
|
||||
);
|
||||
|
||||
private _renderField = (
|
||||
fieldName: string,
|
||||
dataField: TriggerDescription["fields"][string],
|
||||
hasOptional: boolean,
|
||||
domain: string | undefined,
|
||||
triggerName: string | undefined
|
||||
) => {
|
||||
const selector = dataField?.selector ?? { text: null };
|
||||
|
||||
const showOptional = showOptionalToggle(dataField);
|
||||
|
||||
return dataField.selector
|
||||
? html`<ha-settings-row narrow>
|
||||
${!showOptional
|
||||
? hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: nothing
|
||||
: html`<ha-checkbox
|
||||
.key=${fieldName}
|
||||
.checked=${this._checkedKeys.has(fieldName) ||
|
||||
(this.trigger?.options &&
|
||||
this.trigger.options[fieldName] !== undefined)}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.name`
|
||||
) || triggerName}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.description`
|
||||
)}</span
|
||||
>
|
||||
<ha-selector
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(fieldName) &&
|
||||
(!this.trigger?.options ||
|
||||
this.trigger.options[fieldName] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${selector}
|
||||
.context=${this._generateContext(dataField)}
|
||||
.key=${fieldName}
|
||||
@value-changed=${this._dataChanged}
|
||||
.value=${this.trigger?.options
|
||||
? this.trigger.options[fieldName]
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: nothing;
|
||||
};
|
||||
|
||||
private _generateContext(
|
||||
field: TriggerDescription["fields"][string]
|
||||
): Record<string, any> | undefined {
|
||||
if (!field.context) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
for (const [context_key, data_key] of Object.entries(field.context)) {
|
||||
context[context_key] =
|
||||
data_key === "target"
|
||||
? this.trigger.target
|
||||
: this.trigger.options?.[data_key];
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private _dataChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.isValid === false) {
|
||||
// Don't clear an object selector that returns invalid YAML
|
||||
return;
|
||||
}
|
||||
const key = (ev.currentTarget as any).key;
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
this.trigger?.options?.[key] === value ||
|
||||
((!this.trigger?.options || !(key in this.trigger.options)) &&
|
||||
(value === "" || value === undefined))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = { ...this.trigger?.options, [key]: value };
|
||||
|
||||
if (
|
||||
value === "" ||
|
||||
value === undefined ||
|
||||
(typeof value === "object" && !Object.keys(value).length)
|
||||
) {
|
||||
delete options[key];
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.trigger,
|
||||
options,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _targetChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.trigger,
|
||||
target: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _checkboxChanged(ev) {
|
||||
const checked = ev.currentTarget.checked;
|
||||
const key = ev.currentTarget.key;
|
||||
let options;
|
||||
|
||||
if (checked) {
|
||||
this._checkedKeys.add(key);
|
||||
const field =
|
||||
this.description &&
|
||||
Object.entries(this.description).find(([k, _value]) => k === key)?.[1];
|
||||
let defaultValue = field?.default;
|
||||
|
||||
if (
|
||||
defaultValue == null &&
|
||||
field?.selector &&
|
||||
"constant" in field.selector
|
||||
) {
|
||||
defaultValue = field.selector.constant?.value;
|
||||
}
|
||||
|
||||
if (
|
||||
defaultValue == null &&
|
||||
field?.selector &&
|
||||
"boolean" in field.selector
|
||||
) {
|
||||
defaultValue = false;
|
||||
}
|
||||
|
||||
if (defaultValue != null) {
|
||||
options = {
|
||||
...this.trigger?.options,
|
||||
[key]: defaultValue,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
this._checkedKeys.delete(key);
|
||||
options = { ...this.trigger?.options };
|
||||
delete options[key];
|
||||
}
|
||||
if (options) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.trigger,
|
||||
options,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.requestUpdate("_checkedKeys");
|
||||
}
|
||||
|
||||
private _localizeValueCallback = (key: string) => {
|
||||
if (!this.trigger?.trigger) {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${computeDomain(this.trigger.trigger)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
private async _fetchManifest(integration: string) {
|
||||
this._manifest = undefined;
|
||||
try {
|
||||
this._manifest = await fetchIntegrationManifest(this.hass, integration);
|
||||
} catch (_err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Unable to fetch integration manifest for ${integration}`);
|
||||
// Ignore if loading manifest fails. Probably bad JSON in manifest
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-settings-row {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
ha-settings-row[narrow] {
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-settings-row {
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
);
|
||||
}
|
||||
ha-service-picker,
|
||||
ha-entity-picker,
|
||||
ha-yaml-editor {
|
||||
display: block;
|
||||
margin: 0 var(--ha-space-4);
|
||||
}
|
||||
ha-yaml-editor {
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
p {
|
||||
margin: 0 var(--ha-space-4);
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
:host([hide-picker]) p {
|
||||
padding-top: 0;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: 32px;
|
||||
}
|
||||
ha-checkbox {
|
||||
margin-left: calc(var(--ha-space-4) * -1);
|
||||
margin-inline-start: calc(var(--ha-space-4) * -1);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.help-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.description {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 2px;
|
||||
padding-inline-end: 2px;
|
||||
padding-inline-start: initial;
|
||||
}
|
||||
.description p {
|
||||
direction: ltr;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-trigger-platform": HaPlatformTrigger;
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,18 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { slugify } from "../../../../common/string/slugify";
|
||||
import "../../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-button";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import { saveFrontendSystemData } from "../../../../data/frontend";
|
||||
import type {
|
||||
LovelaceDashboard,
|
||||
LovelaceDashboardCreateParams,
|
||||
LovelaceDashboardMutableParams,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import { DEFAULT_PANEL, setDefaultPanel } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-detail")
|
||||
@@ -61,8 +59,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
if (!this._params || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
const defaultPanelUrlPath =
|
||||
this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
const defaultPanelUrlPath = this.hass.defaultPanel;
|
||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||
|
||||
return html`
|
||||
@@ -254,38 +251,15 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private async _toggleDefault() {
|
||||
private _toggleDefault() {
|
||||
const urlPath = this._params?.urlPath;
|
||||
if (!urlPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
// Add warning dialog to saying that this will change the default dashboard for all users
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_title"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_text"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: false,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveFrontendSystemData(this.hass.connection, "core", {
|
||||
...this.hass.systemData,
|
||||
defaultPanel: urlPath === defaultPanel ? undefined : urlPath,
|
||||
});
|
||||
setDefaultPanel(
|
||||
this,
|
||||
urlPath === this.hass.defaultPanel ? DEFAULT_PANEL : urlPath
|
||||
);
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
fetchDashboards,
|
||||
updateDashboard,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
@@ -287,7 +286,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
);
|
||||
|
||||
private _getItems = memoize(
|
||||
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
|
||||
(dashboards: LovelaceDashboard[], defaultUrlPath: string) => {
|
||||
const defaultMode = (
|
||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
||||
).mode;
|
||||
@@ -404,8 +403,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
@@ -419,7 +416,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._dashboards,
|
||||
this.hass.localize
|
||||
)}
|
||||
.data=${this._getItems(this._dashboards, defaultPanel)}
|
||||
.data=${this._getItems(this._dashboards, this.hass.defaultPanel)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-button";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
|
||||
@@ -41,7 +43,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _open = false;
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public showDialog(params: LovelaceResourceDetailsDialogParams): void {
|
||||
this._params = params;
|
||||
@@ -56,11 +58,6 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
url: "",
|
||||
};
|
||||
}
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
@@ -68,6 +65,10 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
@@ -80,45 +81,56 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
"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`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
header-title=${dialogTitle}
|
||||
<ha-md-dialog
|
||||
open
|
||||
disable-cancel-action
|
||||
@closed=${this._dialogClosed}
|
||||
.ariaLabel=${ariaLabel}
|
||||
>
|
||||
<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
|
||||
autofocus
|
||||
.schema=${this._schema(this._data)}
|
||||
.data=${this._data}
|
||||
.hass=${this.hass}
|
||||
.error=${this._error}
|
||||
.computeLabel=${this._computeLabel}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<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
|
||||
.schema=${this._schema(this._data)}
|
||||
.data=${this._data}
|
||||
.hass=${this.hass}
|
||||
.error=${this._error}
|
||||
.computeLabel=${this._computeLabel}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button appearance="plain" @click=${this.closeDialog}>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateResource}
|
||||
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
|
||||
>
|
||||
@@ -130,8 +142,8 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
"ui.panel.config.lovelace.resources.detail.create"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,8 @@ import {
|
||||
import type {
|
||||
BarSeriesOption,
|
||||
CallbackDataParams,
|
||||
LineSeriesOption,
|
||||
TopLevelFormatterParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { LineDataItemOption } from "echarts/types/src/chart/line/LineSeries";
|
||||
import type { FrontendLocaleData } from "../../../../../data/translation";
|
||||
import { formatNumber } from "../../../../../common/number/format_number";
|
||||
import {
|
||||
@@ -172,10 +170,11 @@ function formatTooltip(
|
||||
compare
|
||||
? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `
|
||||
: ""
|
||||
}${formatTime(date, locale, config)}`;
|
||||
if (params[0].componentSubType === "bar") {
|
||||
period += ` – ${formatTime(addHours(date, 1), locale, config)}`;
|
||||
}
|
||||
}${formatTime(date, locale, config)} – ${formatTime(
|
||||
addHours(date, 1),
|
||||
locale,
|
||||
config
|
||||
)}`;
|
||||
}
|
||||
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
|
||||
|
||||
@@ -203,7 +202,7 @@ function formatTooltip(
|
||||
countNegative++;
|
||||
}
|
||||
}
|
||||
return `${param.marker} ${filterXSS(param.seriesName!)}: <div style="direction:ltr; display: inline;">${value} ${unit}</div>`;
|
||||
return `${param.marker} ${filterXSS(param.seriesName!)}: ${value} ${unit}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
let footer = "";
|
||||
@@ -282,35 +281,6 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function fillLineGaps(datasets: LineSeriesOption[]) {
|
||||
const buckets = Array.from(
|
||||
new Set(
|
||||
datasets
|
||||
.map((dataset) =>
|
||||
dataset.data!.map((datapoint) => Number(datapoint![0]))
|
||||
)
|
||||
.flat()
|
||||
)
|
||||
).sort((a, b) => a - b);
|
||||
buckets.forEach((bucket, index) => {
|
||||
for (let i = datasets.length - 1; i >= 0; i--) {
|
||||
const dataPoint = datasets[i].data![index];
|
||||
const item: LineDataItemOption =
|
||||
dataPoint && typeof dataPoint === "object" && "value" in dataPoint
|
||||
? dataPoint
|
||||
: ({ value: dataPoint } as LineDataItemOption);
|
||||
const x = item.value?.[0];
|
||||
if (x === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (Number(x) !== bucket) {
|
||||
datasets[i].data?.splice(index, 0, [bucket, 0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
return datasets;
|
||||
}
|
||||
|
||||
export function getCompareTransform(start: Date, compareStart?: Date) {
|
||||
if (!compareStart) {
|
||||
return (ts: Date) => ts;
|
||||
|
||||
@@ -60,7 +60,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _chartType?: "bar" | "pie";
|
||||
private _chartType: "bar" | "pie" = "bar";
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
@@ -101,14 +101,6 @@ export class HuiEnergyDevicesGraphCard
|
||||
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 {
|
||||
return (
|
||||
hasConfigChanged(this, changedProps) ||
|
||||
@@ -117,21 +109,8 @@ 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() {
|
||||
if (!this.hass || !this._config || !this._chartType) {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -139,19 +118,13 @@ export class HuiEnergyDevicesGraphCard
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||
${this._getAllowedModes().length > 1
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${this._chartType === "pie"
|
||||
? mdiChartBar
|
||||
: mdiChartDonut}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
|
||||
)}
|
||||
@click=${this._handleChartTypeChange}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
.path=${this._chartType === "pie" ? mdiChartBar : mdiChartDonut}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
|
||||
)}
|
||||
@click=${this._handleChartTypeChange}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div
|
||||
class="content ${classMap({
|
||||
@@ -185,7 +158,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
this.hass.locale,
|
||||
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
||||
)} kWh`;
|
||||
return `${title}${params.marker} ${params.seriesName}: <div style="direction:ltr; display: inline;">${value}</div>`;
|
||||
return `${title}${params.marker} ${params.seriesName}: ${value}`;
|
||||
}
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
@@ -556,13 +529,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
}
|
||||
|
||||
private _handleChartTypeChange(): void {
|
||||
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._chartType = this._chartType === "pie" ? "bar" : "pie";
|
||||
this._getStatistics(this._data!);
|
||||
}
|
||||
|
||||
|
||||
@@ -402,9 +402,7 @@ class HuiEnergySankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||
kWh</div>`;
|
||||
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} kWh`;
|
||||
|
||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
import { endOfToday, isToday, startOfToday } from "date-fns";
|
||||
import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import { graphic } from "echarts";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../../data/energy";
|
||||
import type { StatisticValue } from "../../../../data/recorder";
|
||||
import type { FrontendLocaleData } from "../../../../data/translation";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard } from "../../types";
|
||||
import type { PowerSourcesGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import { hex2rgb } from "../../../../common/color/convert-color";
|
||||
|
||||
@customElement("hui-power-sources-graph-card")
|
||||
export class HuiPowerSourcesGraphCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: PowerSourcesGraphCardConfig;
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@state() private _start = startOfToday();
|
||||
|
||||
@state() private _end = endOfToday();
|
||||
|
||||
@state() private _compareStart?: Date;
|
||||
|
||||
@state() private _compareEnd?: Date;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
getEnergyDataCollection(this.hass, {
|
||||
key: this._config?.collection_key,
|
||||
}).subscribe((data) => this._getStatistics(data)),
|
||||
];
|
||||
}
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public setConfig(config: PowerSourcesGraphCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return (
|
||||
hasConfigChanged(this, changedProps) ||
|
||||
changedProps.size > 1 ||
|
||||
!changedProps.has("hass")
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
${this._config.title
|
||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||
: ""}
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
})}"
|
||||
>
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._chartData}
|
||||
.options=${this._createOptions(
|
||||
this._start,
|
||||
this._end,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this._compareStart,
|
||||
this._compareEnd
|
||||
)}
|
||||
></ha-chart-base>
|
||||
${!this._chartData.some((dataset) => dataset.data!.length)
|
||||
? html`<div class="no-data">
|
||||
${isToday(this._start)
|
||||
? this.hass.localize("ui.panel.lovelace.cards.energy.no_data")
|
||||
: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.no_data_period"
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
(
|
||||
start: Date,
|
||||
end: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
compareStart?: Date,
|
||||
compareEnd?: Date
|
||||
): ECOption =>
|
||||
getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
locale,
|
||||
config,
|
||||
"kW",
|
||||
compareStart,
|
||||
compareEnd
|
||||
)
|
||||
);
|
||||
|
||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||
const datasets: LineSeriesOption[] = [];
|
||||
|
||||
const statIds = {
|
||||
solar: {
|
||||
stats: [] as string[],
|
||||
color: "--energy-solar-color",
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.solar"
|
||||
),
|
||||
},
|
||||
grid: {
|
||||
stats: [] as string[],
|
||||
color: "--energy-grid-consumption-color",
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.grid"
|
||||
),
|
||||
},
|
||||
battery: {
|
||||
stats: [] as string[],
|
||||
color: "--energy-battery-out-color",
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.battery"
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
for (const source of energyData.prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
if (source.stat_rate) {
|
||||
statIds.solar.stats.push(source.stat_rate);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "battery") {
|
||||
if (source.stat_rate) {
|
||||
statIds.battery.stats.push(source.stat_rate);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "grid" && source.power) {
|
||||
statIds.grid.stats.push(...source.power.map((p) => p.stat_rate));
|
||||
}
|
||||
}
|
||||
const commonSeriesOptions: LineSeriesOption = {
|
||||
type: "line",
|
||||
smooth: 0.4,
|
||||
smoothMonotone: "x",
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(statIds).forEach((key, keyIndex) => {
|
||||
if (statIds[key].stats.length) {
|
||||
const colorHex = computedStyles.getPropertyValue(statIds[key].color);
|
||||
const rgb = hex2rgb(colorHex);
|
||||
// Echarts is supposed to handle that but it is bugged when you use it together with stacking.
|
||||
// The interpolation breaks the stacking, so this positive/negative is a workaround
|
||||
const { positive, negative } = this._processData(
|
||||
statIds[key].stats.map((id: string) => energyData.stats[id] ?? [])
|
||||
);
|
||||
datasets.push({
|
||||
...commonSeriesOptions,
|
||||
id: key,
|
||||
name: statIds[key].name,
|
||||
color: colorHex,
|
||||
stack: "positive",
|
||||
areaStyle: {
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
|
||||
},
|
||||
]),
|
||||
},
|
||||
data: positive,
|
||||
z: 3 - keyIndex, // draw in reverse order so 0 value lines are overwritten
|
||||
});
|
||||
if (key !== "solar") {
|
||||
datasets.push({
|
||||
...commonSeriesOptions,
|
||||
id: `${key}-negative`,
|
||||
name: statIds[key].name,
|
||||
color: colorHex,
|
||||
stack: "negative",
|
||||
areaStyle: {
|
||||
color: new graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{
|
||||
offset: 0,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
|
||||
},
|
||||
]),
|
||||
},
|
||||
data: negative,
|
||||
z: 4 - keyIndex, // draw in reverse order but above positive series
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._start = energyData.start;
|
||||
this._end = energyData.end || endOfToday();
|
||||
|
||||
this._chartData = fillLineGaps(datasets);
|
||||
|
||||
const usageData: NonNullable<LineSeriesOption["data"]> = [];
|
||||
this._chartData[0]?.data!.forEach((item, i) => {
|
||||
// fillLineGaps ensures all datasets have the same x values
|
||||
const x =
|
||||
typeof item === "object" && "value" in item!
|
||||
? item.value![0]
|
||||
: item![0];
|
||||
usageData[i] = [x, 0];
|
||||
this._chartData.forEach((dataset) => {
|
||||
const y =
|
||||
typeof dataset.data![i] === "object" && "value" in dataset.data![i]!
|
||||
? dataset.data![i].value![1]
|
||||
: dataset.data![i]![1];
|
||||
usageData[i]![1] += y as number;
|
||||
});
|
||||
});
|
||||
this._chartData.push({
|
||||
...commonSeriesOptions,
|
||||
id: "usage",
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.usage"
|
||||
),
|
||||
color: computedStyles.getPropertyValue("--primary-color"),
|
||||
lineStyle: { width: 2 },
|
||||
data: usageData,
|
||||
z: 5,
|
||||
});
|
||||
}
|
||||
|
||||
private _processData(stats: StatisticValue[][]) {
|
||||
const data: Record<number, number[]> = {};
|
||||
stats.forEach((statSet) => {
|
||||
statSet.forEach((point) => {
|
||||
if (point.mean == null) {
|
||||
return;
|
||||
}
|
||||
const x = (point.start + point.end) / 2;
|
||||
data[x] = [...(data[x] ?? []), point.mean];
|
||||
});
|
||||
});
|
||||
const positive: [number, number][] = [];
|
||||
const negative: [number, number][] = [];
|
||||
Object.entries(data).forEach(([x, y]) => {
|
||||
const ts = Number(x);
|
||||
const meanY = y.reduce((a, b) => a + b, 0) / y.length;
|
||||
positive.push([ts, Math.max(0, meanY)]);
|
||||
negative.push([ts, Math.min(0, meanY)]);
|
||||
});
|
||||
return { positive, negative };
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
}
|
||||
.card-header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.content {
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
.has-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
.no-data {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20%;
|
||||
margin-left: var(--ha-space-8);
|
||||
margin-inline-start: var(--ha-space-8);
|
||||
margin-inline-end: initial;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-power-sources-graph-card": HuiPowerSourcesGraphCard;
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,6 @@ export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig {
|
||||
title?: string;
|
||||
max_devices?: number;
|
||||
hide_compound_stats?: boolean;
|
||||
modes?: ("bar" | "pie")[];
|
||||
}
|
||||
|
||||
export interface EnergyDevicesDetailGraphCardConfig
|
||||
@@ -231,11 +230,6 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
|
||||
group_by_area?: boolean;
|
||||
}
|
||||
|
||||
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "power-sources-graph";
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
||||
type: "entity-filter";
|
||||
entities: (EntityFilterEntityConfig | string)[];
|
||||
|
||||
@@ -66,8 +66,6 @@ const LAZY_LOAD_TYPES = {
|
||||
"energy-usage-graph": () =>
|
||||
import("../cards/energy/hui-energy-usage-graph-card"),
|
||||
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
||||
"power-sources-graph": () =>
|
||||
import("../cards/energy/hui-power-sources-graph-card"),
|
||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||
error: () => import("../cards/hui-error-card"),
|
||||
"home-summary": () => import("../cards/hui-home-summary-card"),
|
||||
|
||||
@@ -22,7 +22,6 @@ const NON_STANDARD_URLS = {
|
||||
"energy-devices-graph": "energy/#devices-energy-graph",
|
||||
"energy-devices-detail-graph": "energy/#detail-devices-energy-graph",
|
||||
"energy-sankey": "energy/#sankey-energy-graph",
|
||||
"power-sources-graph": "energy/#power-sources-graph",
|
||||
};
|
||||
|
||||
export const getCardDocumentationURL = (
|
||||
|
||||
@@ -3,21 +3,20 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-md-select";
|
||||
import "../../../../components/ha-md-select-option";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-spinner";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceDashboard } from "../../../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../../../data/lovelace/dashboard";
|
||||
import { getDefaultPanelUrlPath } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { SelectDashboardDialogParams } from "./show-select-dashboard-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
|
||||
@customElement("hui-dialog-select-dashboard")
|
||||
export class HuiDialogSelectDashboard extends LitElement {
|
||||
@@ -144,9 +143,7 @@ export class HuiDialogSelectDashboard extends LitElement {
|
||||
...(this._params!.dashboards || (await fetchDashboards(this.hass))),
|
||||
];
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
const currentPath = this._fromUrlPath || defaultPanel;
|
||||
const currentPath = this._fromUrlPath || this.hass.defaultPanel;
|
||||
for (const dashboard of this._dashboards!) {
|
||||
if (dashboard.url_path !== currentPath) {
|
||||
this._toUrlPath = dashboard.url_path;
|
||||
|
||||
@@ -16,7 +16,6 @@ import { fetchConfig } from "../../../../data/lovelace/config/types";
|
||||
import { isStrategyView } from "../../../../data/lovelace/config/view";
|
||||
import type { LovelaceDashboard } from "../../../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../../../data/lovelace/dashboard";
|
||||
import { getDefaultPanelUrlPath } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { SelectViewDialogParams } from "./show-select-view-dialog";
|
||||
@@ -61,9 +60,6 @@ export class HuiDialogSelectView extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@@ -80,7 +76,7 @@ export class HuiDialogSelectView extends LitElement {
|
||||
"ui.panel.lovelace.editor.select_view.dashboard_label"
|
||||
)}
|
||||
.disabled=${!this._dashboards.length}
|
||||
.value=${this._urlPath || defaultPanel}
|
||||
.value=${this._urlPath || this.hass.defaultPanel}
|
||||
@selected=${this._dashboardChanged}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
|
||||
@@ -109,7 +109,6 @@ export class HUIViewBackground extends LitElement {
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
let applyTheme = false;
|
||||
if (changedProperties.has("hass") && this.hass) {
|
||||
const oldHass = changedProperties.get("hass");
|
||||
if (
|
||||
@@ -117,18 +116,16 @@ export class HUIViewBackground extends LitElement {
|
||||
this.hass.themes !== oldHass.themes ||
|
||||
this.hass.selectedTheme !== oldHass.selectedTheme
|
||||
) {
|
||||
applyTheme = true;
|
||||
this._applyTheme();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("background")) {
|
||||
applyTheme = true;
|
||||
this._applyTheme();
|
||||
this._fetchMedia();
|
||||
}
|
||||
if (changedProperties.has("resolvedImage")) {
|
||||
applyTheme = true;
|
||||
}
|
||||
if (applyTheme) {
|
||||
this._applyTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ import "../../components/ha-select";
|
||||
import "../../components/ha-settings-row";
|
||||
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../data/lovelace/dashboard";
|
||||
import { setDefaultPanel } from "../../data/panel";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { saveFrontendUserData } from "../../data/frontend";
|
||||
|
||||
const USE_SYSTEM_VALUE = "___use_system___";
|
||||
|
||||
@customElement("ha-pick-dashboard-row")
|
||||
class HaPickDashboardRow extends LitElement {
|
||||
@@ -25,7 +23,6 @@ class HaPickDashboardRow extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const value = this.hass.userData?.defaultPanel || USE_SYSTEM_VALUE;
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
@@ -40,18 +37,14 @@ class HaPickDashboardRow extends LitElement {
|
||||
"ui.panel.profile.dashboard.dropdown_label"
|
||||
)}
|
||||
.disabled=${!this._dashboards?.length}
|
||||
.value=${value}
|
||||
.value=${this.hass.defaultPanel}
|
||||
@selected=${this._dashboardChanged}
|
||||
naturalMenuWidth
|
||||
>
|
||||
<ha-list-item .value=${USE_SYSTEM_VALUE}>
|
||||
${this.hass.localize("ui.panel.profile.dashboard.system")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="lovelace">
|
||||
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="home">
|
||||
${this.hass.localize("ui.panel.profile.dashboard.home")}
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.dashboard.default_dashboard_label"
|
||||
)}
|
||||
</ha-list-item>
|
||||
${this._dashboards.map((dashboard) => {
|
||||
if (!this.hass.user!.is_admin && dashboard.require_admin) {
|
||||
@@ -79,18 +72,11 @@ class HaPickDashboardRow extends LitElement {
|
||||
}
|
||||
|
||||
private _dashboardChanged(ev) {
|
||||
const value = ev.target.value as string;
|
||||
if (!value) {
|
||||
const urlPath = ev.target.value;
|
||||
if (!urlPath || urlPath === this.hass.defaultPanel) {
|
||||
return;
|
||||
}
|
||||
const urlPath = value === USE_SYSTEM_VALUE ? undefined : value;
|
||||
if (urlPath === this.hass.userData?.defaultPanel) {
|
||||
return;
|
||||
}
|
||||
saveFrontendUserData(this.hass.connection, "core", {
|
||||
...this.hass.userData,
|
||||
defaultPanel: urlPath,
|
||||
});
|
||||
setDefaultPanel(this, urlPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,10 +154,6 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-first-weekday-row>
|
||||
<ha-pick-dashboard-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-dashboard-row>
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
@@ -212,6 +208,10 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-theme-row>
|
||||
<ha-pick-dashboard-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-dashboard-row>
|
||||
${this.hass.dockedSidebar !== "auto" || !this.narrow
|
||||
? html`
|
||||
<ha-force-narrow-row
|
||||
|
||||
@@ -26,7 +26,6 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { TodoItemEditDialogParams } from "./show-dialog-todo-item-editor";
|
||||
import { supportsMarkdownHelper } from "../../common/translations/markdown_support";
|
||||
import { formatShortDateTimeWithConditionalYear } from "../../common/datetime/format_date_time";
|
||||
|
||||
@customElement("dialog-todo-item-editor")
|
||||
class DialogTodoItemEditor extends LitElement {
|
||||
@@ -42,8 +41,6 @@ class DialogTodoItemEditor extends LitElement {
|
||||
|
||||
@state() private _due?: Date;
|
||||
|
||||
@state() private _completedTime?: Date;
|
||||
|
||||
@state() private _checked = false;
|
||||
|
||||
@state() private _hasTime = false;
|
||||
@@ -68,9 +65,6 @@ class DialogTodoItemEditor extends LitElement {
|
||||
this._checked = entry.status === TodoItemStatus.Completed;
|
||||
this._summary = entry.summary;
|
||||
this._description = entry.description || "";
|
||||
this._completedTime = entry.completed
|
||||
? new Date(entry.completed)
|
||||
: undefined;
|
||||
this._hasTime = entry.due?.includes("T") || false;
|
||||
this._due = entry.due
|
||||
? new Date(this._hasTime ? entry.due : `${entry.due}T00:00:00`)
|
||||
@@ -144,17 +138,6 @@ class DialogTodoItemEditor extends LitElement {
|
||||
.disabled=${!canUpdate}
|
||||
></ha-textfield>
|
||||
</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(
|
||||
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
@@ -472,9 +455,6 @@ class DialogTodoItemEditor extends LitElement {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -199,3 +199,23 @@ export const baseEntrypointStyles = css`
|
||||
width: 100vw;
|
||||
}
|
||||
`;
|
||||
|
||||
export const baseAnimationStyles = css`
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const animationStyles = css`
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale {
|
||||
from {
|
||||
transform: scale(0);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -55,7 +55,6 @@ export const coreStyles = css`
|
||||
--ha-shadow-spread-sm: 0;
|
||||
--ha-shadow-spread-md: 0;
|
||||
--ha-shadow-spread-lg: 0;
|
||||
|
||||
--ha-animation-base-duration: 350ms;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fontStyles } from "../roboto";
|
||||
import { animationStyles } from "./animations.globals";
|
||||
import { colorDerivedVariables, colorStylesCollection } from "./color";
|
||||
import { coreDerivedVariables, coreStyles } from "./core.globals";
|
||||
import { mainDerivedVariables, mainStyles } from "./main.globals";
|
||||
@@ -18,7 +17,6 @@ export const themeStyles = [
|
||||
...colorStylesCollection,
|
||||
fontStyles.toString(),
|
||||
waMainStyles.toString(),
|
||||
animationStyles.toString(),
|
||||
].join("");
|
||||
|
||||
export const derivedStyles = {
|
||||
|
||||
@@ -8,16 +8,13 @@ import {
|
||||
subscribeServices,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { promiseTimeout } from "../common/util/promise-timeout";
|
||||
import { subscribeAreaRegistry } from "../data/area_registry";
|
||||
import { broadcastConnectionStatus } from "../data/connection-status";
|
||||
import { subscribeDeviceRegistry } from "../data/device_registry";
|
||||
import {
|
||||
subscribeFrontendSystemData,
|
||||
subscribeFrontendUserData,
|
||||
} from "../data/frontend";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import { forwardHaptic } from "../data/haptics";
|
||||
import { DEFAULT_PANEL } from "../data/panel";
|
||||
import { serviceCallWillDisconnect } from "../data/service";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -36,6 +33,7 @@ import { fetchWithAuth } from "../util/fetch-with-auth";
|
||||
import { getState } from "../util/ha-pref-storage";
|
||||
import hassCallApi, { hassCallApiRaw } from "../util/hass-call-api";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
|
||||
export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
superClass: T
|
||||
@@ -61,9 +59,8 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
panels: null as any,
|
||||
services: null as any,
|
||||
user: null as any,
|
||||
userData: undefined,
|
||||
systemData: undefined,
|
||||
panelUrl: (this as any)._panelUrl,
|
||||
defaultPanel: DEFAULT_PANEL,
|
||||
language,
|
||||
selectedLanguage: null,
|
||||
locale: {
|
||||
@@ -76,6 +73,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
},
|
||||
resources: null as any,
|
||||
localize: () => "",
|
||||
|
||||
translationMetadata,
|
||||
dockedSidebar: "docked",
|
||||
vibrate: true,
|
||||
@@ -211,9 +209,9 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
formatEntityAttributeName: (_stateObj, attribute) => attribute,
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
value != null ? value : (stateObj.attributes[attribute] ?? ""),
|
||||
formatEntityName: (stateObj) => computeStateName(stateObj),
|
||||
...getState(),
|
||||
...this._pendingHass,
|
||||
formatEntityName: (stateObj) => computeStateName(stateObj),
|
||||
};
|
||||
|
||||
this.hassConnected();
|
||||
@@ -284,26 +282,10 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
subscribeConfig(conn, (config) => this._updateHass({ config }));
|
||||
subscribeServices(conn, (services) => this._updateHass({ services }));
|
||||
subscribePanels(conn, (panels) => this._updateHass({ panels }));
|
||||
// Catch errors to userData and systemData subscription (e.g. if the
|
||||
// backend isn't up to date) and set to null so frontend can continue
|
||||
subscribeFrontendUserData(conn, "core", ({ value: userData }) =>
|
||||
this._updateHass({ userData: userData || {} })
|
||||
).catch(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Failed to subscribe to user data, setting to empty object"
|
||||
);
|
||||
this._updateHass({ userData: {} });
|
||||
});
|
||||
subscribeFrontendSystemData(conn, "core", ({ value: systemData }) =>
|
||||
this._updateHass({ systemData: systemData || {} })
|
||||
).catch(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Failed to subscribe to system data, setting to empty object"
|
||||
);
|
||||
this._updateHass({ systemData: {} });
|
||||
subscribeFrontendUserData(conn, "core", ({ value: userData }) => {
|
||||
this._updateHass({ userData });
|
||||
});
|
||||
|
||||
clearInterval(this.__backendPingInterval);
|
||||
this.__backendPingInterval = setInterval(() => {
|
||||
if (this.hass?.connected) {
|
||||
|
||||
@@ -7,14 +7,20 @@ interface DockSidebarParams {
|
||||
dock: HomeAssistant["dockedSidebar"];
|
||||
}
|
||||
|
||||
interface DefaultPanelParams {
|
||||
defaultPanel: HomeAssistant["defaultPanel"];
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"hass-dock-sidebar": DockSidebarParams;
|
||||
"hass-default-panel": DefaultPanelParams;
|
||||
}
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
"hass-dock-sidebar": HASSDomEvent<DockSidebarParams>;
|
||||
"hass-default-panel": HASSDomEvent<DefaultPanelParams>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,5 +32,9 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
this._updateHass({ dockedSidebar: ev.detail.dock });
|
||||
storeState(this.hass!);
|
||||
});
|
||||
this.addEventListener("hass-default-panel", (ev) => {
|
||||
this._updateHass({ defaultPanel: ev.detail.defaultPanel });
|
||||
storeState(this.hass!);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1130,7 +1130,6 @@
|
||||
"edit": "Edit item",
|
||||
"save": "Save item",
|
||||
"due": "Due date",
|
||||
"completed_time": "Completed { datetime }",
|
||||
"not_all_required_fields": "Not all required fields are filled in",
|
||||
"confirm_delete": {
|
||||
"delete": "Delete item",
|
||||
@@ -3515,12 +3514,8 @@
|
||||
"delete": "Delete",
|
||||
"update": "Update",
|
||||
"create": "Create",
|
||||
"set_default": "Set as default",
|
||||
"remove_default": "Remove as default",
|
||||
"set_default_confirm_title": "Set as default dashboard?",
|
||||
"set_default_confirm_text": "This will replace the current default dashboard. Users can still override their default dashboard in their profile settings.",
|
||||
"remove_default_confirm_title": "Remove default dashboard?",
|
||||
"remove_default_confirm_text": "The default dashboard will be changed to Overview for every user. Users can still override their default dashboard in their profile settings."
|
||||
"set_default": "Set as default on this device",
|
||||
"remove_default": "Remove as default on this device"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
@@ -7166,12 +7161,6 @@
|
||||
"low_carbon_energy_consumed": "Low-carbon electricity consumed",
|
||||
"low_carbon_energy_not_calculated": "Consumed low-carbon electricity couldn't be calculated"
|
||||
},
|
||||
"power_graph": {
|
||||
"grid": "Grid",
|
||||
"solar": "Solar",
|
||||
"battery": "Battery",
|
||||
"usage": "Used"
|
||||
},
|
||||
"energy_compare": {
|
||||
"info": "You are comparing the period {start} with the period {end}",
|
||||
"compare_previous_year": "Compare previous year",
|
||||
@@ -8667,11 +8656,9 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"header": "Dashboard",
|
||||
"description": "Pick a default dashboard to show.",
|
||||
"description": "Pick a default dashboard for this device.",
|
||||
"dropdown_label": "Dashboard",
|
||||
"lovelace": "Overview",
|
||||
"home": "Home",
|
||||
"system": "Auto (use system settings)"
|
||||
"default_dashboard_label": "Overview (default)"
|
||||
},
|
||||
"change_password": {
|
||||
"header": "Change password",
|
||||
|
||||
@@ -18,10 +18,7 @@ import type { AreaRegistryEntry } from "./data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "./data/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "./data/entity_registry";
|
||||
import type { FloorRegistryEntry } from "./data/floor_registry";
|
||||
import type {
|
||||
CoreFrontendSystemData,
|
||||
CoreFrontendUserData,
|
||||
} from "./data/frontend";
|
||||
import type { CoreFrontendUserData } from "./data/frontend";
|
||||
import type {
|
||||
FrontendLocaleData,
|
||||
getHassTranslations,
|
||||
@@ -251,10 +248,10 @@ export interface HomeAssistant {
|
||||
vibrate: boolean;
|
||||
debugConnection: boolean;
|
||||
dockedSidebar: "docked" | "always_hidden" | "auto";
|
||||
defaultPanel: string;
|
||||
moreInfoEntityId: string | null;
|
||||
user?: CurrentUser;
|
||||
userData?: CoreFrontendUserData;
|
||||
systemData?: CoreFrontendSystemData;
|
||||
userData?: CoreFrontendUserData | null;
|
||||
hassUrl(path?): string;
|
||||
callService<T = any>(
|
||||
domain: ServiceCallRequest["domain"],
|
||||
|
||||
@@ -8,9 +8,8 @@ const STORED_STATE = [
|
||||
"debugConnection",
|
||||
"suspendWhenHidden",
|
||||
"enableShortcuts",
|
||||
] as const;
|
||||
|
||||
type StoredHomeAssistant = Pick<HomeAssistant, (typeof STORED_STATE)[number]>;
|
||||
"defaultPanel",
|
||||
];
|
||||
|
||||
export function storeState(hass: HomeAssistant) {
|
||||
try {
|
||||
@@ -32,8 +31,8 @@ export function storeState(hass: HomeAssistant) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getState(): Partial<StoredHomeAssistant> {
|
||||
const state = {} as Partial<StoredHomeAssistant>;
|
||||
export function getState() {
|
||||
const state = {};
|
||||
|
||||
STORED_STATE.forEach((key) => {
|
||||
const storageItem = window.localStorage.getItem(key);
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { render } from "lit";
|
||||
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 = () => {
|
||||
const launchScreenElement = document.getElementById("ha-launch-screen");
|
||||
@@ -9,22 +28,14 @@ export const removeLaunchScreen = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
withViewTransition((viewTransitionAvailable: boolean) => {
|
||||
if (!viewTransitionAvailable) {
|
||||
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));
|
||||
});
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
removeElement(launchScreenElement, false);
|
||||
});
|
||||
} else {
|
||||
// Fallback: Direct removal without transition
|
||||
removeElement(launchScreenElement, true);
|
||||
}
|
||||
};
|
||||
|
||||
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("ha-pref-storage", () => {
|
||||
window.localStorage.setItem = vi.fn();
|
||||
|
||||
storeState(mockHass as unknown as HomeAssistant);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledTimes(7);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledTimes(8);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"dockedSidebar",
|
||||
JSON.stringify("auto")
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -9337,7 +9337,7 @@ __metadata:
|
||||
husky: "npm:9.1.7"
|
||||
idb-keyval: "npm:6.2.2"
|
||||
intl-messageformat: "npm:10.7.18"
|
||||
js-yaml: "npm:4.1.1"
|
||||
js-yaml: "npm:4.1.0"
|
||||
jsdom: "npm:27.1.0"
|
||||
jszip: "npm:3.10.1"
|
||||
leaflet: "npm:1.9.4"
|
||||
@@ -10407,14 +10407,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:4.1.1, js-yaml@npm:^4.1.0":
|
||||
version: 4.1.1
|
||||
resolution: "js-yaml@npm:4.1.1"
|
||||
"js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "js-yaml@npm:4.1.0"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.js
|
||||
checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77
|
||||
checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user