mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-16 22:40:55 +00:00
Compare commits
1 Commits
add-automa
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39a7d92ff0 |
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@feb19ddc698445db27401f1490f6ac182da0816f # v3.2.0
|
||||
uses: relative-ci/agent-action@8504826a02078b05756e4c07e380023cc2c4274a # v3.1.0
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
- name: Tar folder
|
||||
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
||||
|
||||
@@ -137,6 +137,6 @@ jobs:
|
||||
- name: Tar folder
|
||||
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
title: Dropdown
|
||||
---
|
||||
|
||||
# Dropdown `<ha-dropdown>`
|
||||
|
||||
## Implementation
|
||||
|
||||
A compact, accessible dropdown menu for choosing actions or settings. `ha-dropdown` supports composed menu items (`<ha-dropdown-item>`) for icons, submenus, checkboxes, disabled entries, and destructive variants. Use composition with `slot="trigger"` to control the trigger button and use `<ha-dropdown-item>` for rich item content.
|
||||
|
||||
### Example usage (composition)
|
||||
|
||||
```html
|
||||
<ha-dropdown open>
|
||||
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
|
||||
|
||||
<ha-dropdown-item>
|
||||
<ha-svg-icon .path="mdiContentCut" slot="icon"></ha-svg-icon>
|
||||
Cut
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item>
|
||||
<ha-svg-icon .path="mdiContentCopy" slot="icon"></ha-svg-icon>
|
||||
Copy
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item disabled>
|
||||
<ha-svg-icon .path="mdiContentPaste" slot="icon"></ha-svg-icon>
|
||||
Paste
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item>
|
||||
Show images
|
||||
<ha-dropdown-item slot="submenu" value="show-all-images"
|
||||
>Show all images</ha-dropdown-item
|
||||
>
|
||||
<ha-dropdown-item slot="submenu" value="show-thumbnails"
|
||||
>Show thumbnails</ha-dropdown-item
|
||||
>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item type="checkbox" checked>Emoji shortcuts</ha-dropdown-item>
|
||||
<ha-dropdown-item type="checkbox" checked>Word wrap</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item variant="danger">
|
||||
<ha-svg-icon .path="mdiDelete" slot="icon"></ha-svg-icon>
|
||||
Delete
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
This component is based on the webawesome dropdown component.
|
||||
Check the [webawesome documentation](https://webawesome.com/docs/components/dropdown/) for more details.
|
||||
@@ -1,133 +0,0 @@
|
||||
import "@home-assistant/webawesome/dist/components/button/button";
|
||||
import "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
||||
import "@home-assistant/webawesome/dist/components/icon/icon";
|
||||
import "@home-assistant/webawesome/dist/components/popup/popup";
|
||||
import {
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiContentPaste,
|
||||
mdiDelete,
|
||||
} from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-dropdown";
|
||||
import "../../../../src/components/ha-dropdown-item";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
|
||||
@customElement("demo-components-ha-dropdown")
|
||||
export class DemoHaDropdown extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-button in ${mode}">
|
||||
<div class="card-content">
|
||||
<ha-dropdown open>
|
||||
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
|
||||
|
||||
<ha-dropdown-item>
|
||||
<ha-svg-icon
|
||||
.path=${mdiContentCut}
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
Cut
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item>
|
||||
<ha-svg-icon
|
||||
.path=${mdiContentCopy}
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
Copy
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item disabled>
|
||||
<ha-svg-icon
|
||||
.path=${mdiContentPaste}
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
Paste
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item>
|
||||
Show images
|
||||
<ha-dropdown-item slot="submenu" value="show-all-images"
|
||||
>Show All Images</ha-dropdown-item
|
||||
>
|
||||
<ha-dropdown-item slot="submenu" value="show-thumbnails"
|
||||
>Show Thumbnails</ha-dropdown-item
|
||||
>
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item type="checkbox" checked
|
||||
>Emoji Shortcuts</ha-dropdown-item
|
||||
>
|
||||
<ha-dropdown-item type="checkbox" checked
|
||||
>Word Wrap</ha-dropdown-item
|
||||
>
|
||||
<ha-dropdown-item variant="danger">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
|
||||
Delete
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
.button {
|
||||
padding: unset;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
.card-content div {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-dropdown": DemoHaDropdown;
|
||||
}
|
||||
}
|
||||
16
package.json
16
package.json
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.19",
|
||||
"@fullcalendar/luxon3": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@home-assistant/webawesome": "3.0.0",
|
||||
"@home-assistant/webawesome": "3.0.0-beta.6.ha.7",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
@@ -89,8 +89,8 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.9.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.5",
|
||||
"@vaadin/combo-box": "24.9.4",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.4",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@@ -122,7 +122,7 @@
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.0",
|
||||
"marked": "16.4.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -157,8 +157,8 @@
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.3.8",
|
||||
"@rspack/core": "1.6.1",
|
||||
"@rsdoctor/rspack-plugin": "1.3.7",
|
||||
"@rspack/core": "1.6.0",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
@@ -178,7 +178,7 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.8",
|
||||
"@vitest/coverage-v8": "4.0.6",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -219,7 +219,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.46.3",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "4.0.8",
|
||||
"vitest": "4.0.6",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import type {
|
||||
Condition,
|
||||
TimeCondition,
|
||||
} from "../../panels/lovelace/common/validate-condition";
|
||||
|
||||
/**
|
||||
* Extract media queries from conditions recursively
|
||||
*/
|
||||
export function extractMediaQueries(conditions: Condition[]): string[] {
|
||||
return conditions.reduce<string[]>((array, c) => {
|
||||
if ("conditions" in c && c.conditions) {
|
||||
array.push(...extractMediaQueries(c.conditions));
|
||||
}
|
||||
if (c.condition === "screen" && c.media_query) {
|
||||
array.push(c.media_query);
|
||||
}
|
||||
return array;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract time conditions from conditions recursively
|
||||
*/
|
||||
export function extractTimeConditions(
|
||||
conditions: Condition[]
|
||||
): TimeCondition[] {
|
||||
return conditions.reduce<TimeCondition[]>((array, c) => {
|
||||
if ("conditions" in c && c.conditions) {
|
||||
array.push(...extractTimeConditions(c.conditions));
|
||||
}
|
||||
if (c.condition === "time") {
|
||||
array.push(c);
|
||||
}
|
||||
return array;
|
||||
}, []);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { listenMediaQuery } from "../dom/media_query";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { Condition } from "../../panels/lovelace/common/validate-condition";
|
||||
import { checkConditionsMet } from "../../panels/lovelace/common/validate-condition";
|
||||
import { extractMediaQueries, extractTimeConditions } from "./extract";
|
||||
import { calculateNextTimeUpdate } from "./time-calculator";
|
||||
|
||||
/** Maximum delay for setTimeout (2^31 - 1 milliseconds, ~24.8 days)
|
||||
* Values exceeding this will overflow and execute immediately
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value
|
||||
*/
|
||||
const MAX_TIMEOUT_DELAY = 2147483647;
|
||||
|
||||
/**
|
||||
* Helper to setup media query listeners for conditional visibility
|
||||
*/
|
||||
export function setupMediaQueryListeners(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
addListener: (unsub: () => void) => void,
|
||||
onUpdate: (conditionsMet: boolean) => void
|
||||
): void {
|
||||
const mediaQueries = extractMediaQueries(conditions);
|
||||
|
||||
if (mediaQueries.length === 0) return;
|
||||
|
||||
// Optimization for single media query
|
||||
const hasOnlyMediaQuery =
|
||||
conditions.length === 1 &&
|
||||
conditions[0].condition === "screen" &&
|
||||
!!conditions[0].media_query;
|
||||
|
||||
mediaQueries.forEach((mediaQuery) => {
|
||||
const unsub = listenMediaQuery(mediaQuery, (matches) => {
|
||||
if (hasOnlyMediaQuery) {
|
||||
onUpdate(matches);
|
||||
} else {
|
||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
||||
onUpdate(conditionsMet);
|
||||
}
|
||||
});
|
||||
addListener(unsub);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to setup time-based listeners for conditional visibility
|
||||
*/
|
||||
export function setupTimeListeners(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
addListener: (unsub: () => void) => void,
|
||||
onUpdate: (conditionsMet: boolean) => void
|
||||
): void {
|
||||
const timeConditions = extractTimeConditions(conditions);
|
||||
|
||||
if (timeConditions.length === 0) return;
|
||||
|
||||
timeConditions.forEach((timeCondition) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const scheduleUpdate = () => {
|
||||
const delay = calculateNextTimeUpdate(hass, timeCondition);
|
||||
|
||||
if (delay === undefined) return;
|
||||
|
||||
// Cap delay to prevent setTimeout overflow
|
||||
const cappedDelay = Math.min(delay, MAX_TIMEOUT_DELAY);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
if (delay <= MAX_TIMEOUT_DELAY) {
|
||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
||||
onUpdate(conditionsMet);
|
||||
}
|
||||
scheduleUpdate();
|
||||
}, cappedDelay);
|
||||
};
|
||||
|
||||
// Register cleanup function once, outside of scheduleUpdate
|
||||
addListener(() => {
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
|
||||
scheduleUpdate();
|
||||
});
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import {
|
||||
startOfDay,
|
||||
addDays,
|
||||
addMinutes,
|
||||
differenceInMilliseconds,
|
||||
} from "date-fns";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import { parseTimeString } from "../datetime/check_time";
|
||||
import type { TimeCondition } from "../../panels/lovelace/common/validate-condition";
|
||||
|
||||
/**
|
||||
* Calculate milliseconds until next time boundary for a time condition
|
||||
* @param hass Home Assistant object
|
||||
* @param timeCondition Time condition to calculate next update for
|
||||
* @returns Milliseconds until next boundary, or undefined if no boundaries
|
||||
*/
|
||||
export function calculateNextTimeUpdate(
|
||||
hass: HomeAssistant,
|
||||
{ after, before, weekdays }: Omit<TimeCondition, "condition">
|
||||
): number | undefined {
|
||||
const timezone =
|
||||
hass.locale.time_zone === TimeZone.server
|
||||
? hass.config.time_zone
|
||||
: Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
const now = new TZDate(new Date(), timezone);
|
||||
const updates: Date[] = [];
|
||||
|
||||
// Calculate next occurrence of after time
|
||||
if (after) {
|
||||
let afterDate = parseTimeString(after, timezone);
|
||||
if (afterDate <= now) {
|
||||
// If time has passed today, schedule for tomorrow
|
||||
afterDate = addDays(afterDate, 1);
|
||||
}
|
||||
updates.push(afterDate);
|
||||
}
|
||||
|
||||
// Calculate next occurrence of before time
|
||||
if (before) {
|
||||
let beforeDate = parseTimeString(before, timezone);
|
||||
if (beforeDate <= now) {
|
||||
// If time has passed today, schedule for tomorrow
|
||||
beforeDate = addDays(beforeDate, 1);
|
||||
}
|
||||
updates.push(beforeDate);
|
||||
}
|
||||
|
||||
// If weekdays are specified, check for midnight (weekday transition)
|
||||
if (weekdays && weekdays.length > 0 && weekdays.length < 7) {
|
||||
// Calculate next midnight using startOfDay + addDays
|
||||
const tomorrow = addDays(now, 1);
|
||||
const midnight = startOfDay(tomorrow);
|
||||
updates.push(midnight);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find the soonest update time
|
||||
const nextUpdate = updates.reduce((soonest, current) =>
|
||||
current < soonest ? current : soonest
|
||||
);
|
||||
|
||||
// Add 1 minute buffer to ensure we're past the boundary
|
||||
const updateWithBuffer = addMinutes(nextUpdate, 1);
|
||||
|
||||
// Calculate difference in milliseconds
|
||||
return differenceInMilliseconds(updateWithBuffer, now);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import { isBefore, isAfter, isWithinInterval } from "date-fns";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import { WEEKDAY_MAP } from "./weekday";
|
||||
import type { TimeCondition } from "../../panels/lovelace/common/validate-condition";
|
||||
|
||||
/**
|
||||
* Validate a time string format and value ranges without creating Date objects
|
||||
* @param timeString Time string to validate (HH:MM or HH:MM:SS)
|
||||
* @returns true if valid, false otherwise
|
||||
*/
|
||||
export function isValidTimeString(timeString: string): boolean {
|
||||
// Reject empty strings
|
||||
if (!timeString || timeString.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = timeString.split(":");
|
||||
|
||||
if (parts.length < 2 || parts.length > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure each part contains only digits (and optional leading zeros)
|
||||
// This prevents "8:00 AM" from passing validation
|
||||
if (!parts.every((part) => /^\d+$/.test(part))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hours = parseInt(parts[0], 10);
|
||||
const minutes = parseInt(parts[1], 10);
|
||||
const seconds = parts.length === 3 ? parseInt(parts[2], 10) : 0;
|
||||
|
||||
if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
hours >= 0 &&
|
||||
hours <= 23 &&
|
||||
minutes >= 0 &&
|
||||
minutes <= 59 &&
|
||||
seconds >= 0 &&
|
||||
seconds <= 59
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a time string (HH:MM or HH:MM:SS) and set it on today's date in the given timezone
|
||||
*
|
||||
* Note: This function assumes the time string has already been validated by
|
||||
* isValidTimeString() at configuration time. It does not re-validate at runtime
|
||||
* for consistency with other condition types (screen, user, location, etc.)
|
||||
*
|
||||
* @param timeString The time string to parse (must be pre-validated)
|
||||
* @param timezone The timezone to use
|
||||
* @returns The Date object
|
||||
*/
|
||||
export const parseTimeString = (timeString: string, timezone: string): Date => {
|
||||
const parts = timeString.split(":");
|
||||
const hours = parseInt(parts[0], 10);
|
||||
const minutes = parseInt(parts[1], 10);
|
||||
const seconds = parts.length === 3 ? parseInt(parts[2], 10) : 0;
|
||||
|
||||
const now = new TZDate(new Date(), timezone);
|
||||
const dateWithTime = new TZDate(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
0,
|
||||
timezone
|
||||
);
|
||||
|
||||
return new Date(dateWithTime.getTime());
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the current time matches the time condition (after/before/weekday)
|
||||
* @param hass Home Assistant object
|
||||
* @param timeCondition Time condition to check
|
||||
* @returns true if current time matches the condition
|
||||
*/
|
||||
export const checkTimeInRange = (
|
||||
hass: HomeAssistant,
|
||||
{ after, before, weekdays }: Omit<TimeCondition, "condition">
|
||||
): boolean => {
|
||||
const timezone =
|
||||
hass.locale.time_zone === TimeZone.server
|
||||
? hass.config.time_zone
|
||||
: Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
const now = new TZDate(new Date(), timezone);
|
||||
|
||||
// Check weekday condition
|
||||
if (weekdays && weekdays.length > 0) {
|
||||
const currentWeekday = WEEKDAY_MAP[now.getDay()];
|
||||
if (!weekdays.includes(currentWeekday)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check time conditions
|
||||
if (!after && !before) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const afterDate = after ? parseTimeString(after, timezone) : undefined;
|
||||
const beforeDate = before ? parseTimeString(before, timezone) : undefined;
|
||||
|
||||
if (afterDate && beforeDate) {
|
||||
if (isBefore(beforeDate, afterDate)) {
|
||||
// Crosses midnight (e.g., 22:00 to 06:00)
|
||||
return !isBefore(now, afterDate) || !isAfter(now, beforeDate);
|
||||
}
|
||||
return isWithinInterval(now, { start: afterDate, end: beforeDate });
|
||||
}
|
||||
|
||||
if (afterDate) {
|
||||
return !isBefore(now, afterDate);
|
||||
}
|
||||
|
||||
if (beforeDate) {
|
||||
return !isAfter(now, beforeDate);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -1,7 +1,18 @@
|
||||
import { getWeekStartByLocale } from "weekstart";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { FirstWeekday } from "../../data/translation";
|
||||
import { WEEKDAYS_LONG, type WeekdayIndex } from "./weekday";
|
||||
|
||||
export const weekdays = [
|
||||
"sunday",
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
] as const;
|
||||
|
||||
type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
|
||||
if (locale.first_weekday === FirstWeekday.language) {
|
||||
@@ -12,12 +23,12 @@ export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
|
||||
}
|
||||
return (getWeekStartByLocale(locale.language) % 7) as WeekdayIndex;
|
||||
}
|
||||
return WEEKDAYS_LONG.includes(locale.first_weekday)
|
||||
? (WEEKDAYS_LONG.indexOf(locale.first_weekday) as WeekdayIndex)
|
||||
return weekdays.includes(locale.first_weekday)
|
||||
? (weekdays.indexOf(locale.first_weekday) as WeekdayIndex)
|
||||
: 1;
|
||||
};
|
||||
|
||||
export const firstWeekday = (locale: FrontendLocaleData) => {
|
||||
const index = firstWeekdayIndex(locale);
|
||||
return WEEKDAYS_LONG[index];
|
||||
return weekdays[index];
|
||||
};
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
export type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
export type WeekdayShort =
|
||||
| "sun"
|
||||
| "mon"
|
||||
| "tue"
|
||||
| "wed"
|
||||
| "thu"
|
||||
| "fri"
|
||||
| "sat";
|
||||
|
||||
export type WeekdayLong =
|
||||
| "sunday"
|
||||
| "monday"
|
||||
| "tuesday"
|
||||
| "wednesday"
|
||||
| "thursday"
|
||||
| "friday"
|
||||
| "saturday";
|
||||
|
||||
export const WEEKDAYS_SHORT = [
|
||||
"sun",
|
||||
"mon",
|
||||
"tue",
|
||||
"wed",
|
||||
"thu",
|
||||
"fri",
|
||||
"sat",
|
||||
] as const satisfies readonly WeekdayShort[];
|
||||
|
||||
export const WEEKDAYS_LONG = [
|
||||
"sunday",
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
] as const satisfies readonly WeekdayLong[];
|
||||
|
||||
export const WEEKDAY_MAP = {
|
||||
0: "sun",
|
||||
1: "mon",
|
||||
2: "tue",
|
||||
3: "wed",
|
||||
4: "thu",
|
||||
5: "fri",
|
||||
6: "sat",
|
||||
} as const satisfies Record<WeekdayIndex, WeekdayShort>;
|
||||
|
||||
export const WEEKDAY_SHORT_TO_LONG = {
|
||||
sun: "sunday",
|
||||
mon: "monday",
|
||||
tue: "tuesday",
|
||||
wed: "wednesday",
|
||||
thu: "thursday",
|
||||
fri: "friday",
|
||||
sat: "saturday",
|
||||
} as const satisfies Record<WeekdayShort, WeekdayLong>;
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ThemeVars } from "../../data/ws-themes";
|
||||
import { darkColorVariables } from "../../resources/theme/color";
|
||||
import { darkSemanticVariables } from "../../resources/theme/semantic.globals";
|
||||
import { derivedStyles } from "../../resources/theme/theme";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
@@ -53,7 +52,7 @@ export const applyThemesOnElement = (
|
||||
|
||||
if (themeToApply && darkMode) {
|
||||
cacheKey = `${cacheKey}__dark`;
|
||||
themeRules = { ...darkSemanticVariables, ...darkColorVariables };
|
||||
themeRules = { ...darkColorVariables };
|
||||
}
|
||||
|
||||
if (themeToApply === "default") {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Parses a CSS duration string (e.g., "300ms", "3s") and returns the duration in milliseconds.
|
||||
*
|
||||
* @param duration - A CSS duration string (e.g., "300ms", "3s", "0.5s")
|
||||
* @returns The duration in milliseconds, or 0 if the input is invalid
|
||||
*
|
||||
* @example
|
||||
* parseAnimationDuration("300ms") // Returns 300
|
||||
* parseAnimationDuration("3s") // Returns 3000
|
||||
* parseAnimationDuration("0.5s") // Returns 500
|
||||
* parseAnimationDuration("invalid") // Returns 0
|
||||
*/
|
||||
export const parseAnimationDuration = (duration: string): number => {
|
||||
const trimmed = duration.trim();
|
||||
|
||||
let value: number;
|
||||
let multiplier: number;
|
||||
|
||||
if (trimmed.endsWith("ms")) {
|
||||
value = parseFloat(trimmed.slice(0, -2));
|
||||
multiplier = 1;
|
||||
} else if (trimmed.endsWith("s")) {
|
||||
value = parseFloat(trimmed.slice(0, -1));
|
||||
multiplier = 1000;
|
||||
} else {
|
||||
// No recognized unit, try parsing as number (assume ms)
|
||||
value = parseFloat(trimmed);
|
||||
multiplier = 1;
|
||||
}
|
||||
|
||||
if (!isFinite(value) || value < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return value * multiplier;
|
||||
};
|
||||
@@ -119,8 +119,8 @@ type Thresholds = Record<
|
||||
>;
|
||||
|
||||
export const DEFAULT_THRESHOLDS: Thresholds = {
|
||||
second: 59, // seconds to minute
|
||||
minute: 59, // minutes to hour
|
||||
second: 45, // seconds to minute
|
||||
minute: 45, // minutes to hour
|
||||
hour: 22, // hour to day
|
||||
day: 5, // day to week
|
||||
week: 4, // week to months
|
||||
|
||||
@@ -6,8 +6,7 @@ export function downSampleLineData<
|
||||
data: T[] | undefined,
|
||||
maxDetails: number,
|
||||
minX?: number,
|
||||
maxX?: number,
|
||||
useMean = false
|
||||
maxX?: number
|
||||
): T[] {
|
||||
if (!data) {
|
||||
return [];
|
||||
@@ -18,13 +17,15 @@ export function downSampleLineData<
|
||||
const min = minX ?? getPointData(data[0]!)[0];
|
||||
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
|
||||
const step = Math.ceil((max - min) / Math.floor(maxDetails));
|
||||
|
||||
// Group points into frames
|
||||
const frames = new Map<
|
||||
number,
|
||||
{ point: (typeof data)[number]; x: number; y: number }[]
|
||||
{
|
||||
min: { point: (typeof data)[number]; x: number; y: number };
|
||||
max: { point: (typeof data)[number]; x: number; y: number };
|
||||
}
|
||||
>();
|
||||
|
||||
// Group points into frames
|
||||
for (const point of data) {
|
||||
const pointData = getPointData(point);
|
||||
if (!Array.isArray(pointData)) continue;
|
||||
@@ -35,53 +36,28 @@ export function downSampleLineData<
|
||||
const frameIndex = Math.floor((x - min) / step);
|
||||
const frame = frames.get(frameIndex);
|
||||
if (!frame) {
|
||||
frames.set(frameIndex, [{ point, x, y }]);
|
||||
frames.set(frameIndex, { min: { point, x, y }, max: { point, x, y } });
|
||||
} else {
|
||||
frame.push({ point, x, y });
|
||||
if (frame.min.y > y) {
|
||||
frame.min = { point, x, y };
|
||||
}
|
||||
if (frame.max.y < y) {
|
||||
frame.max = { point, x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert frames back to points
|
||||
const result: T[] = [];
|
||||
|
||||
if (useMean) {
|
||||
// Use mean values for each frame
|
||||
for (const [_i, framePoints] of frames) {
|
||||
const sumY = framePoints.reduce((acc, p) => acc + p.y, 0);
|
||||
const meanY = sumY / framePoints.length;
|
||||
const sumX = framePoints.reduce((acc, p) => acc + p.x, 0);
|
||||
const meanX = sumX / framePoints.length;
|
||||
|
||||
const firstPoint = framePoints[0].point;
|
||||
const pointData = getPointData(firstPoint);
|
||||
const meanPoint = (
|
||||
Array.isArray(pointData) ? [meanX, meanY] : { value: [meanX, meanY] }
|
||||
) as T;
|
||||
result.push(meanPoint);
|
||||
}
|
||||
} else {
|
||||
// Use min/max values for each frame
|
||||
for (const [_i, framePoints] of frames) {
|
||||
let minPoint = framePoints[0];
|
||||
let maxPoint = framePoints[0];
|
||||
|
||||
for (const p of framePoints) {
|
||||
if (p.y < minPoint.y) {
|
||||
minPoint = p;
|
||||
}
|
||||
if (p.y > maxPoint.y) {
|
||||
maxPoint = p;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [_i, frame] of frames) {
|
||||
// Use min/max points to preserve visual accuracy
|
||||
// The order of the data must be preserved so max may be before min
|
||||
if (minPoint.x > maxPoint.x) {
|
||||
result.push(maxPoint.point);
|
||||
}
|
||||
result.push(minPoint.point);
|
||||
if (minPoint.x < maxPoint.x) {
|
||||
result.push(maxPoint.point);
|
||||
if (frame.min.x > frame.max.x) {
|
||||
result.push(frame.max.point);
|
||||
}
|
||||
result.push(frame.min.point);
|
||||
if (frame.min.x < frame.max.x) {
|
||||
result.push(frame.max.point);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -427,7 +427,6 @@ export class HaChartBase extends LitElement {
|
||||
...axis.axisPointer?.handle,
|
||||
show: true,
|
||||
},
|
||||
label: { show: false },
|
||||
},
|
||||
}
|
||||
: axis
|
||||
@@ -628,10 +627,6 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
private _createTheme(style: CSSStyleDeclaration) {
|
||||
const textBorderColor =
|
||||
style.getPropertyValue("--ha-card-background") ||
|
||||
style.getPropertyValue("--card-background-color");
|
||||
const textBorderWidth = 2;
|
||||
return {
|
||||
color: getAllGraphColors(style),
|
||||
backgroundColor: "transparent",
|
||||
@@ -655,22 +650,22 @@ export class HaChartBase extends LitElement {
|
||||
graph: {
|
||||
label: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
textBorderColor,
|
||||
textBorderWidth,
|
||||
textBorderColor: style.getPropertyValue("--primary-background-color"),
|
||||
textBorderWidth: 2,
|
||||
},
|
||||
},
|
||||
pie: {
|
||||
label: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
textBorderColor,
|
||||
textBorderWidth,
|
||||
textBorderColor: style.getPropertyValue("--primary-background-color"),
|
||||
textBorderWidth: 2,
|
||||
},
|
||||
},
|
||||
sankey: {
|
||||
label: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
textBorderColor,
|
||||
textBorderWidth,
|
||||
textBorderColor: style.getPropertyValue("--primary-background-color"),
|
||||
textBorderWidth: 2,
|
||||
},
|
||||
},
|
||||
categoryAxis: {
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { EChartsType } from "echarts/core";
|
||||
import type { GraphSeriesOption } from "echarts/charts";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
TopLevelFormatterParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
|
||||
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
@@ -19,7 +16,6 @@ import { deepEqual } from "../../common/util/deep-equal";
|
||||
export interface NetworkNode {
|
||||
id: string;
|
||||
name?: string;
|
||||
context?: string;
|
||||
category?: number;
|
||||
value?: number;
|
||||
symbolSize?: number;
|
||||
@@ -192,25 +188,6 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
label: {
|
||||
show: showLabels,
|
||||
position: "right",
|
||||
formatter: (params: CallbackDataParams) => {
|
||||
const node = params.data as NetworkNode;
|
||||
if (node.context) {
|
||||
return `{primary|${node.name ?? ""}}\n{secondary|${node.context}}`;
|
||||
}
|
||||
return node.name ?? "";
|
||||
},
|
||||
rich: {
|
||||
primary: {
|
||||
fontSize: 12,
|
||||
},
|
||||
secondary: {
|
||||
fontSize: 12,
|
||||
color: getComputedStyle(document.body).getPropertyValue(
|
||||
"--secondary-text-color"
|
||||
),
|
||||
lineHeight: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
focus: isMobile ? "none" : "adjacency",
|
||||
@@ -248,7 +225,6 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
context: node.context,
|
||||
category: node.category,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
|
||||
@@ -62,7 +62,6 @@ class HaDataTableLabels extends LitElement {
|
||||
@click=${clickAction ? this._labelClicked : undefined}
|
||||
@keydown=${clickAction ? this._labelClicked : undefined}
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label?.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
|
||||
@@ -197,6 +197,9 @@ export class HaDevicePicker extends LitElement {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.device-picker.placeholder");
|
||||
const notFoundLabel = this.hass.localize(
|
||||
"ui.components.device-picker.no_match"
|
||||
);
|
||||
|
||||
const valueRenderer = this._valueRenderer(this._configEntryLookup);
|
||||
|
||||
@@ -206,10 +209,7 @@ export class HaDevicePicker extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.device-picker.no_devices"
|
||||
)}
|
||||
.notFoundLabel=${notFoundLabel}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
@@ -233,11 +233,6 @@ export class HaDevicePicker extends LitElement {
|
||||
this.value = value;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.device-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -269,6 +269,9 @@ export class HaEntityPicker extends LitElement {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.entity.entity-picker.placeholder");
|
||||
const notFoundLabel = this.hass.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
@@ -279,7 +282,7 @@ export class HaEntityPicker extends LitElement {
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.notFoundLabel=${notFoundLabel}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.addButton ? undefined : this.value}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
@@ -353,11 +356,6 @@ export class HaEntityPicker extends LitElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.entity.entity-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -21,6 +21,7 @@ import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-input-helper-text";
|
||||
import type {
|
||||
PickerComboBoxItem,
|
||||
PickerComboBoxSearchFn,
|
||||
@@ -270,6 +271,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
||||
|
||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
||||
output.push({
|
||||
@@ -277,6 +279,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
statistic_id: id,
|
||||
primary,
|
||||
secondary,
|
||||
a11y_label: a11yLabel,
|
||||
stateObj: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||
@@ -455,6 +458,9 @@ export class HaStatisticPicker extends LitElement {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.statistic-picker.placeholder");
|
||||
const notFoundLabel = this.hass.localize(
|
||||
"ui.components.statistic-picker.no_match"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
@@ -462,10 +468,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.label=${this.label}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.statistic-picker.no_statistics"
|
||||
)}
|
||||
.notFoundLabel=${notFoundLabel}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
@@ -474,7 +477,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.helper=${this.helper}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
@@ -519,11 +521,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.statistic-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -369,10 +369,9 @@ export class HaAreaPicker extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.area-picker.no_match"
|
||||
)}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
@@ -426,11 +425,6 @@ export class HaAreaPicker extends LitElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.area-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* Home Assistant dropdown item component
|
||||
*
|
||||
* @element ha-dropdown-item
|
||||
* @extends {DropdownItem}
|
||||
*
|
||||
* @summary
|
||||
* A stylable dropdown item component supporting Home Assistant theming, variants, and appearances based on webawesome dropdown item.
|
||||
*
|
||||
*/
|
||||
@customElement("ha-dropdown-item")
|
||||
export class HaDropdownItem extends DropdownItem {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
DropdownItem.styles,
|
||||
css`
|
||||
:host {
|
||||
min-height: var(--ha-space-10);
|
||||
}
|
||||
|
||||
#icon ::slotted(*) {
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
|
||||
:host([variant="danger"]) #icon ::slotted(*) {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dropdown-item": HaDropdownItem;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* Home Assistant dropdown component
|
||||
*
|
||||
* @element ha-dropdown
|
||||
* @extends {Dropdown}
|
||||
*
|
||||
* @summary
|
||||
* A stylable dropdown component supporting Home Assistant theming, variants, and appearances based on webawesome dropdown.
|
||||
*
|
||||
*/
|
||||
@customElement("ha-dropdown")
|
||||
export class HaDropdown extends Dropdown {
|
||||
@property({ attribute: false }) dropdownTag = "ha-dropdown";
|
||||
|
||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Dropdown.styles,
|
||||
css`
|
||||
:host {
|
||||
font-size: var(--ha-dropdown-font-size, var(--ha-font-size-m));
|
||||
--wa-color-surface-raised: var(
|
||||
--card-background-color,
|
||||
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
|
||||
);
|
||||
}
|
||||
|
||||
#menu {
|
||||
padding: var(--ha-space-1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dropdown": HaDropdown;
|
||||
}
|
||||
}
|
||||
@@ -109,10 +109,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
||||
.selected=${(this.value || []).includes(label.label_id)}
|
||||
hasMeta
|
||||
>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||
${label.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
|
||||
@@ -383,9 +383,8 @@ export class HaFloorPicker extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.floor-picker.no_floors"
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.floor-picker.no_match"
|
||||
)}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
@@ -445,11 +444,6 @@ export class HaFloorPicker extends LitElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.floor-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -25,6 +25,9 @@ import "./ha-svg-icon";
|
||||
export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
@@ -46,11 +49,8 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public getItems?: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
@property({ attribute: false, type: Array })
|
||||
public getItems?: () => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
@@ -64,11 +64,8 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public notFoundLabel?: string | ((search: string) => string);
|
||||
|
||||
@property({ attribute: "empty-label" })
|
||||
public emptyLabel?: string;
|
||||
@property({ attribute: "not-found-label", type: String })
|
||||
public notFoundLabel?: string;
|
||||
|
||||
@property({ attribute: "popover-placement" })
|
||||
public popoverPlacement:
|
||||
@@ -88,25 +85,6 @@ export class HaGenericPicker extends LitElement {
|
||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||
|
||||
/** Section filter buttons for the list, section headers needs to be defined in getItems as strings */
|
||||
@property({ attribute: false }) public sections?: (
|
||||
| {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
| "separator"
|
||||
)[];
|
||||
|
||||
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
|
||||
firstIndex: number;
|
||||
lastIndex: number;
|
||||
firstItem: PickerComboBoxItem | string;
|
||||
secondItem: PickerComboBoxItem | string;
|
||||
itemsCount: number;
|
||||
}) => string | undefined;
|
||||
|
||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||
|
||||
@query(".container") private _containerElement?: HTMLDivElement;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
@@ -119,11 +97,6 @@ export class HaGenericPicker extends LitElement {
|
||||
|
||||
@state() private _openedNarrow = false;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
private _narrow = false;
|
||||
|
||||
// helper to set new value after closing picker, to avoid flicker
|
||||
@@ -216,19 +189,16 @@ export class HaGenericPicker extends LitElement {
|
||||
<ha-picker-combo-box
|
||||
.hass=${this.hass}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.searchLabel}
|
||||
.label=${this.searchLabel ??
|
||||
(this.hass?.localize("ui.common.search") || "Search")}
|
||||
.value=${this.value}
|
||||
@value-changed=${this._valueChanged}
|
||||
.rowRenderer=${this.rowRenderer}
|
||||
.notFoundLabel=${this.notFoundLabel}
|
||||
.emptyLabel=${this.emptyLabel}
|
||||
.getItems=${this.getItems}
|
||||
.getAdditionalItems=${this.getAdditionalItems}
|
||||
.searchFn=${this.searchFn}
|
||||
.mode=${dialogMode ? "dialog" : "popover"}
|
||||
.sections=${this.sections}
|
||||
.sectionTitleFunction=${this.sectionTitleFunction}
|
||||
.selectedSection=${this.selectedSection}
|
||||
></ha-picker-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -154,10 +154,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
return this._getLabelsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.hass,
|
||||
this._labels,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
@@ -227,9 +224,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.label-picker.no_labels"
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.label-picker.no_match"
|
||||
)}
|
||||
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
|
||||
.placeholder=${placeholder}
|
||||
@@ -292,11 +288,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.label-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,32 +1,17 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { uid } from "../common/util/uid";
|
||||
import "./ha-tooltip";
|
||||
|
||||
@customElement("ha-label")
|
||||
class HaLabel extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) dense = false;
|
||||
|
||||
@property({ attribute: "description" })
|
||||
public description?: string;
|
||||
|
||||
private _elementId = "label-" + uid();
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-tooltip
|
||||
.for=${this._elementId}
|
||||
.disabled=${!this.description?.trim()}
|
||||
>
|
||||
${this.description}
|
||||
</ha-tooltip>
|
||||
<div class="container" .id=${this._elementId}>
|
||||
<span class="content">
|
||||
<slot name="icon"></slot>
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -51,7 +36,9 @@ class HaLabel extends LitElement {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
letter-spacing: 0.1px;
|
||||
vertical-align: middle;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
color: var(--ha-label-text-color);
|
||||
--mdc-icon-size: 12px;
|
||||
@@ -79,23 +66,14 @@ class HaLabel extends LitElement {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
:host([dense]) {
|
||||
height: 20px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
:host([dense]) .container {
|
||||
padding: 0 12px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
:host([dense]) ::slotted([slot="icon"]) {
|
||||
margin-right: 4px;
|
||||
|
||||
@@ -21,7 +21,6 @@ import "./chips/ha-input-chip";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-label-picker";
|
||||
import type { HaLabelPicker } from "./ha-label-picker";
|
||||
import "./ha-tooltip";
|
||||
|
||||
@customElement("ha-labels-picker")
|
||||
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -143,17 +142,9 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
const color = label?.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
const elementId = "label-" + label.label_id;
|
||||
return html`
|
||||
<ha-tooltip
|
||||
.for=${elementId}
|
||||
.disabled=${!label?.description?.trim()}
|
||||
>
|
||||
${label?.description}
|
||||
</ha-tooltip>
|
||||
<ha-input-chip
|
||||
.item=${label}
|
||||
.id=${elementId}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._openDetail}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -125,10 +125,9 @@ export class HaLanguagePicker extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
popover-placement="bottom-end"
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass?.localize(
|
||||
"ui.components.language-picker.no_languages"
|
||||
) || "No languages available"}
|
||||
.notFoundLabel=${this.hass?.localize(
|
||||
"ui.components.language-picker.no_match"
|
||||
)}
|
||||
.placeholder=${this.label ??
|
||||
(this.hass?.localize("ui.components.language-picker.language") ||
|
||||
"Language")}
|
||||
@@ -173,15 +172,6 @@ export class HaLanguagePicker extends LitElement {
|
||||
this.value = ev.detail.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) => {
|
||||
const term = html`<b>‘${search}’</b>`;
|
||||
return this.hass
|
||||
? this.hass.localize("ui.components.language-picker.no_match", {
|
||||
term,
|
||||
})
|
||||
: html`No languages found for ${term}`;
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ListItemEl } from "@material/web/list/internal/listitem/list-item";
|
||||
import { styles } from "@material/web/list/internal/listitem/list-item-styles";
|
||||
import { css, html, nothing, type TemplateResult } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "./ha-ripple";
|
||||
|
||||
export const haMdListStyles = [
|
||||
styles,
|
||||
@@ -26,18 +25,6 @@ export const haMdListStyles = [
|
||||
@customElement("ha-md-list-item")
|
||||
export class HaMdListItem extends ListItemEl {
|
||||
static override styles = haMdListStyles;
|
||||
|
||||
protected renderRipple(): TemplateResult | typeof nothing {
|
||||
if (this.type === "text") {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-ripple
|
||||
part="ripple"
|
||||
for="item"
|
||||
?disabled=${this.disabled && this.type !== "link"}
|
||||
></ha-ripple>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiMagnify, mdiMinusBoxOutline } from "@mdi/js";
|
||||
import { mdiMagnify } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -14,12 +14,11 @@ import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HaFuse } from "../resources/fuse";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-filter-chip";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
import "./ha-textfield";
|
||||
@@ -28,18 +27,28 @@ import type { HaTextField } from "./ha-textfield";
|
||||
export interface PickerComboBoxItem {
|
||||
id: string;
|
||||
primary: string;
|
||||
a11y_label?: string;
|
||||
secondary?: string;
|
||||
search_labels?: string[];
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
icon?: string;
|
||||
}
|
||||
const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
||||
|
||||
// Hack to force empty label to always display empty value by default in the search field
|
||||
export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem {
|
||||
a11y_label: string;
|
||||
}
|
||||
|
||||
const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
|
||||
|
||||
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
||||
item
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
<ha-combo-box-item
|
||||
.type=${item.id === NO_MATCHING_ITEMS_FOUND_ID ? "text" : "button"}
|
||||
compact
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: item.icon_path
|
||||
@@ -78,11 +87,8 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
@state() private _listScrolled = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public getItems?: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
@property({ attribute: false, type: Array })
|
||||
public getItems?: () => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
@@ -90,45 +96,21 @@ export class HaPickerComboBox extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public notFoundLabel?: string | ((search: string) => string);
|
||||
|
||||
@property({ attribute: "empty-label" })
|
||||
public emptyLabel?: string;
|
||||
@property({ attribute: "not-found-label", type: String })
|
||||
public notFoundLabel?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||
|
||||
@property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
|
||||
|
||||
/** Section filter buttons for the list, section headers needs to be defined in getItems as strings */
|
||||
@property({ attribute: false }) public sections?: (
|
||||
| {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
| "separator"
|
||||
)[];
|
||||
|
||||
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
|
||||
firstIndex: number;
|
||||
lastIndex: number;
|
||||
firstItem: PickerComboBoxItem | string;
|
||||
secondItem: PickerComboBoxItem | string;
|
||||
itemsCount: number;
|
||||
}) => string | undefined;
|
||||
|
||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||
|
||||
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
||||
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
|
||||
@state() private _items: (PickerComboBoxItem | string)[] = [];
|
||||
@state() private _items: PickerComboBoxItemWithLabel[] = [];
|
||||
|
||||
@state() private _sectionTitle?: string;
|
||||
|
||||
private _allItems: (PickerComboBoxItem | string)[] = [];
|
||||
private _allItems: PickerComboBoxItemWithLabel[] = [];
|
||||
|
||||
private _selectedItemIndex = -1;
|
||||
|
||||
@@ -139,8 +121,6 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
private _removeKeyboardShortcuts?: () => void;
|
||||
|
||||
private _search = "";
|
||||
|
||||
protected firstUpdated() {
|
||||
this._registerKeyboardShortcuts();
|
||||
}
|
||||
@@ -165,142 +145,74 @@ export class HaPickerComboBox extends LitElement {
|
||||
"Search"}
|
||||
@input=${this._filterChanged}
|
||||
></ha-textfield>
|
||||
${this._renderSectionButtons()}
|
||||
${this.sections?.length
|
||||
? html`
|
||||
<div class="section-title-wrapper">
|
||||
<div
|
||||
class="section-title ${!this.selectedSection &&
|
||||
this._sectionTitle
|
||||
? "show"
|
||||
: ""}"
|
||||
>
|
||||
${this._sectionTitle}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<lit-virtualizer
|
||||
.keyFunction=${this._keyFunction}
|
||||
@scroll=${this._onScrollList}
|
||||
tabindex="0"
|
||||
scroller
|
||||
.items=${this._items}
|
||||
.renderItem=${this._renderItem}
|
||||
style="min-height: 36px;"
|
||||
class=${this._listScrolled ? "scrolled" : ""}
|
||||
@scroll=${this._onScrollList}
|
||||
@focus=${this._focusList}
|
||||
@visibilityChanged=${this._visibilityChanged}
|
||||
>
|
||||
</lit-virtualizer> `;
|
||||
}
|
||||
|
||||
private _renderSectionButtons() {
|
||||
if (!this.sections || this.sections.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
private _defaultNotFoundItem = memoizeOne(
|
||||
(
|
||||
label: this["notFoundLabel"],
|
||||
localize?: LocalizeFunc
|
||||
): PickerComboBoxItemWithLabel => ({
|
||||
id: NO_MATCHING_ITEMS_FOUND_ID,
|
||||
primary:
|
||||
label ||
|
||||
(localize && localize("ui.components.combo-box.no_match")) ||
|
||||
"No matching items found",
|
||||
icon_path: mdiMagnify,
|
||||
a11y_label:
|
||||
label ||
|
||||
(localize && localize("ui.components.combo-box.no_match")) ||
|
||||
"No matching items found",
|
||||
})
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-chip-set class="sections">
|
||||
${this.sections.map((section) =>
|
||||
section === "separator"
|
||||
? html`<div class="separator"></div>`
|
||||
: html`<ha-filter-chip
|
||||
@click=${this._toggleSection}
|
||||
.section-id=${section.id}
|
||||
.selected=${this.selectedSection === section.id}
|
||||
.label=${section.label}
|
||||
>
|
||||
</ha-filter-chip>`
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`;
|
||||
}
|
||||
private _getAdditionalItems = (searchString?: string) => {
|
||||
const items = this.getAdditionalItems?.(searchString) || [];
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _visibilityChanged(ev) {
|
||||
if (
|
||||
this._virtualizerElement &&
|
||||
this.sectionTitleFunction &&
|
||||
this.sections?.length
|
||||
) {
|
||||
const firstItem = this._virtualizerElement.items[ev.first];
|
||||
const secondItem = this._virtualizerElement.items[ev.first + 1];
|
||||
this._sectionTitle = this.sectionTitleFunction({
|
||||
firstIndex: ev.first,
|
||||
lastIndex: ev.last,
|
||||
firstItem: firstItem as PickerComboBoxItem | string,
|
||||
secondItem: secondItem as PickerComboBoxItem | string,
|
||||
itemsCount: this._virtualizerElement.items.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
return items.map<PickerComboBoxItemWithLabel>((item) => ({
|
||||
...item,
|
||||
a11y_label: item.a11y_label || item.primary,
|
||||
}));
|
||||
};
|
||||
|
||||
private _getAdditionalItems = (searchString?: string) =>
|
||||
this.getAdditionalItems?.(searchString) || [];
|
||||
private _getItems = (): PickerComboBoxItemWithLabel[] => {
|
||||
const items = this.getItems ? this.getItems() : [];
|
||||
|
||||
private _getItems = () => {
|
||||
let items = [
|
||||
...(this.getItems
|
||||
? this.getItems(this._search, this.selectedSection)
|
||||
: []),
|
||||
];
|
||||
|
||||
if (!this.sections?.length) {
|
||||
items = items.sort((entityA, entityB) =>
|
||||
const sortedItems = items
|
||||
.map<PickerComboBoxItemWithLabel>((item) => ({
|
||||
...item,
|
||||
a11y_label: item.a11y_label || item.primary,
|
||||
}))
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
(entityA as PickerComboBoxItem).sorting_label!,
|
||||
(entityB as PickerComboBoxItem).sorting_label!,
|
||||
entityA.sorting_label!,
|
||||
entityB.sorting_label!,
|
||||
this.hass?.locale.language ?? navigator.language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
items.push(NO_ITEMS_AVAILABLE_ID);
|
||||
if (!sortedItems.length) {
|
||||
sortedItems.push(
|
||||
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
||||
);
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
items.push(...additionalItems);
|
||||
|
||||
if (this.mode === "dialog") {
|
||||
items.push("padding"); // padding for safe area inset
|
||||
}
|
||||
|
||||
return items;
|
||||
sortedItems.push(...additionalItems);
|
||||
return sortedItems;
|
||||
};
|
||||
|
||||
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
||||
if (item === "padding") {
|
||||
return html`<div class="bottom-padding"></div>`;
|
||||
}
|
||||
if (item === NO_ITEMS_AVAILABLE_ID) {
|
||||
return html`
|
||||
<div class="combo-box-row">
|
||||
<ha-combo-box-item type="text" compact>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${this._search ? mdiMagnify : mdiMinusBoxOutline}
|
||||
></ha-svg-icon>
|
||||
<span slot="headline"
|
||||
>${this._search
|
||||
? typeof this.notFoundLabel === "function"
|
||||
? this.notFoundLabel(this._search)
|
||||
: this.notFoundLabel ||
|
||||
this.hass?.localize("ui.components.combo-box.no_match") ||
|
||||
"No matching items found"
|
||||
: this.emptyLabel ||
|
||||
this.hass?.localize("ui.components.combo-box.no_items") ||
|
||||
"No items available"}</span
|
||||
>
|
||||
</ha-combo-box-item>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (typeof item === "string") {
|
||||
return html`<div class="title">${item}</div>`;
|
||||
}
|
||||
|
||||
private _renderItem = (item: PickerComboBoxItem, index: number) => {
|
||||
const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
|
||||
return html`<div
|
||||
id=${`list-item-${index}`}
|
||||
@@ -309,7 +221,9 @@ export class HaPickerComboBox extends LitElement {
|
||||
.index=${index}
|
||||
@click=${this._valueSelected}
|
||||
>
|
||||
${renderer(item, index)}
|
||||
${item.id === NO_MATCHING_ITEMS_FOUND_ID
|
||||
? DEFAULT_ROW_RENDERER(item, index)
|
||||
: renderer(item, index)}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
@@ -328,6 +242,10 @@ export class HaPickerComboBox extends LitElement {
|
||||
const value = (ev.currentTarget as any).value as string;
|
||||
const newValue = value?.trim();
|
||||
|
||||
if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
};
|
||||
|
||||
@@ -338,19 +256,15 @@ export class HaPickerComboBox extends LitElement {
|
||||
private _filterChanged = (ev: Event) => {
|
||||
const textfield = ev.target as HaTextField;
|
||||
const searchString = textfield.value.trim();
|
||||
this._search = searchString;
|
||||
|
||||
if (this.sections?.length) {
|
||||
this._items = this._getItems();
|
||||
} else {
|
||||
if (!searchString) {
|
||||
this._items = this._allItems;
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
|
||||
const index = this._fuseIndex(this._allItems);
|
||||
const fuse = new HaFuse(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
this._allItems,
|
||||
{
|
||||
shouldSort: false,
|
||||
minMatchCharLength: Math.min(searchString.length, 2),
|
||||
@@ -359,62 +273,34 @@ export class HaPickerComboBox extends LitElement {
|
||||
);
|
||||
|
||||
const results = fuse.multiTermsSearch(searchString);
|
||||
let filteredItems = [...this._allItems];
|
||||
|
||||
let filteredItems = this._allItems as PickerComboBoxItem[];
|
||||
if (results) {
|
||||
const items: (PickerComboBoxItem | string)[] = results.map(
|
||||
(result) => result.item
|
||||
const items = results.map((result) => result.item);
|
||||
if (items.length === 0) {
|
||||
items.push(
|
||||
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
||||
);
|
||||
|
||||
if (!items.length) {
|
||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
const additionalItems = this._getAdditionalItems(searchString);
|
||||
items.push(...additionalItems);
|
||||
|
||||
filteredItems = items;
|
||||
}
|
||||
|
||||
if (this.searchFn) {
|
||||
filteredItems = this.searchFn(
|
||||
searchString,
|
||||
filteredItems as PickerComboBoxItem[],
|
||||
this._allItems as PickerComboBoxItem[]
|
||||
filteredItems,
|
||||
this._allItems
|
||||
);
|
||||
}
|
||||
|
||||
this._items = filteredItems as PickerComboBoxItem[];
|
||||
}
|
||||
|
||||
this._items = filteredItems as PickerComboBoxItemWithLabel[];
|
||||
this._selectedItemIndex = -1;
|
||||
if (this._virtualizerElement) {
|
||||
this._virtualizerElement.scrollTo(0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
private _toggleSection(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._resetSelectedItem();
|
||||
this._sectionTitle = undefined;
|
||||
const section = (ev.target as HTMLElement)["section-id"] as string;
|
||||
if (!section) {
|
||||
return;
|
||||
}
|
||||
if (this.selectedSection === section) {
|
||||
this.selectedSection = undefined;
|
||||
} else {
|
||||
this.selectedSection = section;
|
||||
}
|
||||
|
||||
this._items = this._getItems();
|
||||
|
||||
// Reset scroll position when filter changes
|
||||
if (this._virtualizerElement) {
|
||||
this._virtualizerElement.scrollToIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
private _registerKeyboardShortcuts() {
|
||||
this._removeKeyboardShortcuts = tinykeys(this, {
|
||||
ArrowUp: this._selectPreviousItem,
|
||||
@@ -458,7 +344,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof items[nextIndex] === "string") {
|
||||
if (items[nextIndex].id === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
// Skip titles, padding and empty search
|
||||
if (nextIndex === maxItems) {
|
||||
return;
|
||||
@@ -487,7 +373,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof items[nextIndex] === "string") {
|
||||
if (items[nextIndex]?.id === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
// Skip titles, padding and empty search
|
||||
if (nextIndex === 0) {
|
||||
return;
|
||||
@@ -509,6 +395,13 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
const nextIndex = 0;
|
||||
|
||||
if (
|
||||
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
|
||||
NO_MATCHING_ITEMS_FOUND_ID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex + 1;
|
||||
} else {
|
||||
@@ -526,6 +419,13 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
const nextIndex = this._virtualizerElement.items.length - 1;
|
||||
|
||||
if (
|
||||
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
|
||||
NO_MATCHING_ITEMS_FOUND_ID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex - 1;
|
||||
} else {
|
||||
@@ -553,7 +453,10 @@ export class HaPickerComboBox extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
||||
|
||||
if (this._virtualizerElement?.items.length === 1) {
|
||||
if (
|
||||
this._virtualizerElement?.items.length === 1 &&
|
||||
firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
|
||||
) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: firstItem.id,
|
||||
});
|
||||
@@ -569,7 +472,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
const item = this._virtualizerElement?.items[
|
||||
this._selectedItemIndex
|
||||
] as PickerComboBoxItem;
|
||||
if (item) {
|
||||
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
fireEvent(this, "value-changed", { value: item.id });
|
||||
}
|
||||
};
|
||||
@@ -581,9 +484,6 @@ export class HaPickerComboBox extends LitElement {
|
||||
this._selectedItemIndex = -1;
|
||||
}
|
||||
|
||||
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
||||
typeof item === "string" ? item : item.id;
|
||||
|
||||
static styles = [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
@@ -658,80 +558,6 @@ export class HaPickerComboBox extends LitElement {
|
||||
background-color: var(--ha-color-fill-neutral-normal-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.sections {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-3) var(--ha-space-3);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) .sections {
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
}
|
||||
|
||||
.sections ha-filter-chip {
|
||||
flex-shrink: 0;
|
||||
--md-filter-chip-selected-container-color: var(
|
||||
--ha-color-fill-primary-normal-hover
|
||||
);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.sections .separator {
|
||||
height: var(--ha-space-8);
|
||||
width: 0;
|
||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.title {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) .title {
|
||||
padding: var(--ha-space-1) var(--ha-space-4);
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) ha-textfield {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
.section-title-wrapper {
|
||||
height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
width: calc(100% - var(--ha-space-8));
|
||||
}
|
||||
|
||||
.section-title.show {
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.empty-search {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-section-title")
|
||||
class HaSectionTitle extends LitElement {
|
||||
protected render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-section-title": HaSectionTitle;
|
||||
}
|
||||
}
|
||||
@@ -157,9 +157,7 @@ export const computePanels = memoizeOne(
|
||||
Object.values(panels).forEach((panel) => {
|
||||
if (
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(!panel.title && panel.url_path !== defaultPanel) ||
|
||||
(panel.default_visible === false &&
|
||||
!panelsOrder.includes(panel.url_path))
|
||||
(!panel.title && panel.url_path !== defaultPanel)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import { consume } from "@lit/context";
|
||||
// @ts-ignore
|
||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import {
|
||||
getAreasAndFloors,
|
||||
type AreaFloorValue,
|
||||
type FloorComboBoxItem,
|
||||
} from "../data/area_floor";
|
||||
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
|
||||
import { labelsContext } from "../data/context";
|
||||
import { getDevices, type DevicePickerItem } from "../data/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../data/entity";
|
||||
import { getEntities, type EntityComboBoxItem } from "../data/entity_registry";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { getLabels, type LabelRegistryEntry } from "../data/label_registry";
|
||||
import {
|
||||
areaMeetsFilter,
|
||||
deviceMeetsFilter,
|
||||
@@ -34,23 +18,18 @@ import {
|
||||
type TargetTypeFloorless,
|
||||
} from "../data/target";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
|
||||
import { HaFuse } from "../resources/fuse";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tree-indicator";
|
||||
import "./target-picker/ha-target-picker-item-group";
|
||||
import "./target-picker/ha-target-picker-selector";
|
||||
import type { HaTargetPickerSelector } from "./target-picker/ha-target-picker-selector";
|
||||
import "./target-picker/ha-target-picker-value-chip";
|
||||
|
||||
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
|
||||
const SEPARATOR = "________";
|
||||
const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
@customElement("ha-target-picker")
|
||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -89,54 +68,23 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: "add-on-top", type: Boolean }) public addOnTop = false;
|
||||
|
||||
@state() private _selectedSection?: TargetTypeFloorless;
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
@state() private _addTargetWidth = 0;
|
||||
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
private _labelRegistry!: LabelRegistryEntry[];
|
||||
@state() private _narrow = false;
|
||||
|
||||
@state() private _pickerFilter?: TargetTypeFloorless;
|
||||
|
||||
@state() private _pickerWrapperOpen = false;
|
||||
|
||||
@query(".add-target-wrapper") private _addTargetWrapper?: HTMLDivElement;
|
||||
|
||||
@query("ha-target-picker-selector")
|
||||
private _targetPickerSelectorElement?: HaTargetPickerSelector;
|
||||
|
||||
private _newTarget?: { type: TargetType; id: string };
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(getDevices);
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||
|
||||
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||
|
||||
private get _showEntityId() {
|
||||
return this.hass.userData?.showEntityIdPicker;
|
||||
}
|
||||
|
||||
private _fuseIndexes = {
|
||||
area: memoizeOne((states: FloorComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
),
|
||||
entity: memoizeOne((states: EntityComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
),
|
||||
device: memoizeOne((states: DevicePickerItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
),
|
||||
label: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
),
|
||||
};
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
}
|
||||
|
||||
private _createFuseIndex = (states) =>
|
||||
Fuse.createIndex(["search_labels"], states);
|
||||
|
||||
protected render() {
|
||||
if (this.addOnTop) {
|
||||
return html` ${this._renderPicker()} ${this._renderItems()} `;
|
||||
@@ -341,63 +289,137 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
const sections = [
|
||||
{
|
||||
id: "entity",
|
||||
label: this.hass.localize("ui.components.target-picker.type.entities"),
|
||||
},
|
||||
{
|
||||
id: "device",
|
||||
label: this.hass.localize("ui.components.target-picker.type.devices"),
|
||||
},
|
||||
{
|
||||
id: "area",
|
||||
label: this.hass.localize("ui.components.target-picker.type.areas"),
|
||||
},
|
||||
"separator" as const,
|
||||
{
|
||||
id: "label",
|
||||
label: this.hass.localize("ui.components.target-picker.type.labels"),
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="add-target-wrapper">
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.autofocus=${this.autofocus}
|
||||
.helper=${this.helper}
|
||||
.sections=${sections}
|
||||
.notFoundLabel=${this._noTargetFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.target-picker.no_targets"
|
||||
)}
|
||||
.sectionTitleFunction=${this._sectionTitleFunction}
|
||||
.selectedSection=${this._selectedSection}
|
||||
.rowRenderer=${this._renderRow}
|
||||
.getItems=${this._getItems}
|
||||
@value-changed=${this._targetPicked}
|
||||
.addButtonLabel=${this.hass.localize(
|
||||
<ha-button
|
||||
id="add-target-button"
|
||||
size="small"
|
||||
appearance="filled"
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize("ui.components.target-picker.add_target")}
|
||||
</ha-button>
|
||||
${!this._narrow && (this._pickerWrapperOpen || this._open)
|
||||
? html`
|
||||
<wa-popover
|
||||
.open=${this._pickerWrapperOpen}
|
||||
style="--body-width: ${this._addTargetWidth}px;"
|
||||
without-arrow
|
||||
distance="-4"
|
||||
placement="bottom-start"
|
||||
for="add-target-button"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
@wa-after-show=${this._showSelector}
|
||||
@wa-after-hide=${this._hidePicker}
|
||||
trap-focus
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_target"
|
||||
)}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
${this._renderTargetSelector()}
|
||||
</wa-popover>
|
||||
`
|
||||
: this._pickerWrapperOpen || this._open
|
||||
? html`<ha-bottom-sheet
|
||||
flexcontent
|
||||
.open=${this._pickerWrapperOpen}
|
||||
@wa-after-show=${this._showSelector}
|
||||
@closed=${this._hidePicker}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_target"
|
||||
)}
|
||||
>
|
||||
${this._renderTargetSelector(true)}
|
||||
</ha-bottom-sheet>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetPicked(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (value.startsWith(CREATE_ID)) {
|
||||
this._createNewDomainElement(value.substring(CREATE_ID.length));
|
||||
return;
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._handleResize();
|
||||
window.addEventListener("resize", this._handleResize);
|
||||
}
|
||||
|
||||
const [type, id] = ev.detail.value.split(SEPARATOR);
|
||||
this._addTarget(id, type as TargetType);
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("resize", this._handleResize);
|
||||
}
|
||||
|
||||
private _handleResize = () => {
|
||||
this._narrow =
|
||||
window.matchMedia("(max-width: 870px)").matches ||
|
||||
window.matchMedia("(max-height: 500px)").matches;
|
||||
};
|
||||
|
||||
private _showPicker() {
|
||||
this._addTargetWidth = this._addTargetWrapper?.offsetWidth || 0;
|
||||
this._pickerWrapperOpen = true;
|
||||
}
|
||||
|
||||
// wait for drawer animation to finish
|
||||
private _showSelector = () => {
|
||||
this._open = true;
|
||||
requestAnimationFrame(() => {
|
||||
this._targetPickerSelectorElement?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
private _handleUpdatePickerFilter(
|
||||
ev: CustomEvent<TargetTypeFloorless | undefined>
|
||||
) {
|
||||
this._updatePickerFilter(
|
||||
typeof ev.detail === "string" ? ev.detail : undefined
|
||||
);
|
||||
}
|
||||
|
||||
private _updatePickerFilter = (filter?: TargetTypeFloorless) => {
|
||||
this._pickerFilter = filter;
|
||||
};
|
||||
|
||||
private _hidePicker(ev) {
|
||||
ev.stopPropagation();
|
||||
this._open = false;
|
||||
this._pickerWrapperOpen = false;
|
||||
|
||||
if (this._newTarget) {
|
||||
this._addTarget(this._newTarget.id, this._newTarget.type);
|
||||
this._newTarget = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderTargetSelector(dialogMode = false) {
|
||||
if (!this._open) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-target-picker-selector
|
||||
.hass=${this.hass}
|
||||
@filter-type-changed=${this._handleUpdatePickerFilter}
|
||||
.filterType=${this._pickerFilter}
|
||||
@target-picked=${this._handleTargetPicked}
|
||||
@create-domain-picked=${this._handleCreateDomain}
|
||||
.targetValue=${this.value}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.createDomains=${this.createDomains}
|
||||
.mode=${dialogMode ? "dialog" : "popover"}
|
||||
></ha-target-picker-selector>
|
||||
`;
|
||||
}
|
||||
|
||||
private _addTarget(id: string, type: TargetType) {
|
||||
@@ -432,7 +454,26 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
?.removeAttribute("collapsed");
|
||||
}
|
||||
|
||||
private _createNewDomainElement = (domain: string) => {
|
||||
private _handleTargetPicked = async (
|
||||
ev: CustomEvent<{ type: TargetType; id: string }>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
|
||||
this._pickerWrapperOpen = false;
|
||||
|
||||
if (!ev.detail.type || !ev.detail.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// save new target temporarily to add it after dialog closes
|
||||
this._newTarget = ev.detail;
|
||||
};
|
||||
|
||||
private _handleCreateDomain = (ev: CustomEvent<string>) => {
|
||||
this._pickerWrapperOpen = false;
|
||||
|
||||
const domain = ev.detail;
|
||||
|
||||
showHelperDetailDialog(this, {
|
||||
domain,
|
||||
dialogClosedCallback: (item) => {
|
||||
@@ -634,462 +675,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getRowType = (
|
||||
item:
|
||||
| PickerComboBoxItem
|
||||
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
) => {
|
||||
if (
|
||||
(item as FloorComboBoxItem).type === "area" ||
|
||||
(item as FloorComboBoxItem).type === "floor"
|
||||
) {
|
||||
return (item as FloorComboBoxItem).type;
|
||||
}
|
||||
|
||||
if ("domain" in item) {
|
||||
return "device";
|
||||
}
|
||||
|
||||
if ("stateObj" in item) {
|
||||
return "entity";
|
||||
}
|
||||
|
||||
if (item.id === EMPTY_SEARCH) {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return "label";
|
||||
};
|
||||
|
||||
private _sectionTitleFunction = ({
|
||||
firstIndex,
|
||||
lastIndex,
|
||||
firstItem,
|
||||
secondItem,
|
||||
itemsCount,
|
||||
}: {
|
||||
firstIndex: number;
|
||||
lastIndex: number;
|
||||
firstItem: PickerComboBoxItem | string;
|
||||
secondItem: PickerComboBoxItem | string;
|
||||
itemsCount: number;
|
||||
}) => {
|
||||
if (
|
||||
firstItem === undefined ||
|
||||
secondItem === undefined ||
|
||||
typeof firstItem === "string" ||
|
||||
(typeof secondItem === "string" && secondItem !== "padding") ||
|
||||
(firstIndex === 0 && lastIndex === itemsCount - 1)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const type = this._getRowType(firstItem as PickerComboBoxItem);
|
||||
const translationType:
|
||||
| "areas"
|
||||
| "entities"
|
||||
| "devices"
|
||||
| "labels"
|
||||
| undefined =
|
||||
type === "area" || type === "floor"
|
||||
? "areas"
|
||||
: type === "entity"
|
||||
? "entities"
|
||||
: type && type !== "empty"
|
||||
? `${type}s`
|
||||
: undefined;
|
||||
|
||||
return translationType
|
||||
? this.hass.localize(
|
||||
`ui.components.target-picker.type.${translationType}`
|
||||
)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
private _getItems = (searchString: string, section: string) => {
|
||||
this._selectedSection = section as TargetTypeFloorless | undefined;
|
||||
|
||||
return this._getItemsMemoized(
|
||||
this.hass.localize,
|
||||
this.entityFilter,
|
||||
this.deviceFilter,
|
||||
this.includeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.value,
|
||||
searchString,
|
||||
this._configEntryLookup,
|
||||
this._selectedSection
|
||||
);
|
||||
};
|
||||
|
||||
private _getItemsMemoized = memoizeOne(
|
||||
(
|
||||
localize: HomeAssistant["localize"],
|
||||
entityFilter: this["entityFilter"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
includeDomains: this["includeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
targetValue: this["value"],
|
||||
searchTerm: string,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
filterType?: TargetTypeFloorless
|
||||
) => {
|
||||
const items: (
|
||||
| string
|
||||
| FloorComboBoxItem
|
||||
| EntityComboBoxItem
|
||||
| PickerComboBoxItem
|
||||
)[] = [];
|
||||
|
||||
if (!filterType || filterType === "entity") {
|
||||
let entityItems = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
includeDomains,
|
||||
undefined,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
undefined,
|
||||
undefined,
|
||||
targetValue?.entity_id
|
||||
? ensureArray(targetValue.entity_id)
|
||||
: undefined,
|
||||
undefined,
|
||||
`entity${SEPARATOR}`
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
entityItems = this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
(item: EntityComboBoxItem) =>
|
||||
item.stateObj?.entity_id === searchTerm
|
||||
) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!filterType && entityItems.length) {
|
||||
// show group title
|
||||
items.push(localize("ui.components.target-picker.type.entities"));
|
||||
}
|
||||
|
||||
items.push(...entityItems);
|
||||
}
|
||||
|
||||
if (!filterType || filterType === "device") {
|
||||
let deviceItems = this._getDevicesMemoized(
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
includeDomains,
|
||||
undefined,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
targetValue?.device_id
|
||||
? ensureArray(targetValue.device_id)
|
||||
: undefined,
|
||||
undefined,
|
||||
`device${SEPARATOR}`
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
deviceItems = this._filterGroup("device", deviceItems, searchTerm);
|
||||
}
|
||||
|
||||
if (!filterType && deviceItems.length) {
|
||||
// show group title
|
||||
items.push(localize("ui.components.target-picker.type.devices"));
|
||||
}
|
||||
|
||||
items.push(...deviceItems);
|
||||
}
|
||||
|
||||
if (!filterType || filterType === "area") {
|
||||
let areasAndFloors = this._getAreasAndFloorsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.floors,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
memoizeOne((value: AreaFloorValue): string =>
|
||||
[value.type, value.id].join(SEPARATOR)
|
||||
),
|
||||
includeDomains,
|
||||
undefined,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
targetValue?.area_id ? ensureArray(targetValue.area_id) : undefined,
|
||||
targetValue?.floor_id ? ensureArray(targetValue.floor_id) : undefined
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
areasAndFloors = this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!filterType && areasAndFloors.length) {
|
||||
// show group title
|
||||
items.push(localize("ui.components.target-picker.type.areas"));
|
||||
}
|
||||
|
||||
items.push(
|
||||
...areasAndFloors.map((item, index) => {
|
||||
const nextItem = areasAndFloors[index + 1];
|
||||
|
||||
if (
|
||||
!nextItem ||
|
||||
(item.type === "area" && nextItem.type === "floor")
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
last: true,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!filterType || filterType === "label") {
|
||||
let labels = this._getLabelsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._labelRegistry,
|
||||
includeDomains,
|
||||
undefined,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
targetValue?.label_id ? ensureArray(targetValue.label_id) : undefined,
|
||||
`label${SEPARATOR}`
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
labels = this._filterGroup("label", labels, searchTerm);
|
||||
}
|
||||
|
||||
if (!filterType && labels.length) {
|
||||
// show group title
|
||||
items.push(localize("ui.components.target-picker.type.labels"));
|
||||
}
|
||||
|
||||
items.push(...labels);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _filterGroup(
|
||||
type: TargetType,
|
||||
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
|
||||
searchTerm: string,
|
||||
checkExact?: (
|
||||
item: FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem
|
||||
) => boolean
|
||||
) {
|
||||
const fuseIndex = this._fuseIndexes[type](items);
|
||||
const fuse = new HaFuse(
|
||||
items,
|
||||
{
|
||||
shouldSort: false,
|
||||
minMatchCharLength: Math.min(searchTerm.length, 2),
|
||||
},
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
const results = fuse.multiTermsSearch(searchTerm);
|
||||
let filteredItems = items;
|
||||
if (results) {
|
||||
filteredItems = results.map((result) => result.item);
|
||||
}
|
||||
|
||||
if (!checkExact) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
// If there is exact match for entity id, put it first
|
||||
const index = filteredItems.findIndex((item) => checkExact(item));
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
|
||||
|
||||
private _getCreateItems = memoizeOne(
|
||||
(createDomains: this["createDomains"]) => {
|
||||
if (!createDomains?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return createDomains.map((domain) => {
|
||||
const primary = this.hass.localize(
|
||||
"ui.components.entity.entity-picker.create_helper",
|
||||
{
|
||||
domain: isHelperDomain(domain)
|
||||
? this.hass.localize(`ui.panel.config.helpers.types.${domain}`)
|
||||
: domainToName(this.hass.localize, domain),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id: CREATE_ID + domain,
|
||||
primary: primary,
|
||||
secondary: this.hass.localize(
|
||||
"ui.components.entity.entity-picker.new_entity"
|
||||
),
|
||||
icon_path: mdiPlus,
|
||||
} satisfies EntityComboBoxItem;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
this._configEntryLookup = Object.fromEntries(
|
||||
configEntries.map((entry) => [entry.entry_id, entry])
|
||||
);
|
||||
}
|
||||
|
||||
private _renderRow = (
|
||||
item:
|
||||
| PickerComboBoxItem
|
||||
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem,
|
||||
index: number
|
||||
) => {
|
||||
if (!item) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const type = this._getRowType(item);
|
||||
let hasFloor = false;
|
||||
let rtl = false;
|
||||
let showEntityId = false;
|
||||
|
||||
if (type === "area" || type === "floor") {
|
||||
item.id = item[type]?.[`${type}_id`];
|
||||
|
||||
rtl = computeRTL(this.hass);
|
||||
hasFloor =
|
||||
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
|
||||
}
|
||||
|
||||
if (type === "entity") {
|
||||
showEntityId = !!this._showEntityId;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item
|
||||
id=${`list-item-${index}`}
|
||||
tabindex="-1"
|
||||
.type=${type === "empty" ? "text" : "button"}
|
||||
class=${type === "empty" ? "empty" : ""}
|
||||
style=${(item as FloorComboBoxItem).type === "area" && hasFloor
|
||||
? "--md-list-item-leading-space: var(--ha-space-12);"
|
||||
: ""}
|
||||
>
|
||||
${(item as FloorComboBoxItem).type === "area" && hasFloor
|
||||
? html`
|
||||
<ha-tree-indicator
|
||||
style=${styleMap({
|
||||
width: "var(--ha-space-12)",
|
||||
position: "absolute",
|
||||
top: "var(--ha-space-0)",
|
||||
left: rtl ? undefined : "var(--ha-space-1)",
|
||||
right: rtl ? "var(--ha-space-1)" : undefined,
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
})}
|
||||
.end=${(
|
||||
item as FloorComboBoxItem & { last?: boolean | undefined }
|
||||
).last}
|
||||
slot="start"
|
||||
></ha-tree-indicator>
|
||||
`
|
||||
: nothing}
|
||||
${item.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: item.icon_path
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>`
|
||||
: type === "entity" && (item as EntityComboBoxItem).stateObj
|
||||
? html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${(item as EntityComboBoxItem).stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: type === "device" && (item as DevicePickerItem).domain
|
||||
? html`
|
||||
<img
|
||||
slot="start"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl({
|
||||
domain: (item as DevicePickerItem).domain!,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes.darkMode,
|
||||
})}
|
||||
/>
|
||||
`
|
||||
: type === "floor"
|
||||
? html`<ha-floor-icon
|
||||
slot="start"
|
||||
.floor=${(item as FloorComboBoxItem).floor!}
|
||||
></ha-floor-icon>`
|
||||
: type === "area"
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path || mdiTextureBox}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${(item as EntityComboBoxItem).stateObj && showEntityId
|
||||
? html`
|
||||
<span slot="supporting-text" class="code">
|
||||
${(item as EntityComboBoxItem).stateObj?.entity_id}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
${(item as EntityComboBoxItem).domain_name &&
|
||||
(type !== "entity" || !showEntityId)
|
||||
? html`
|
||||
<div slot="trailing-supporting-text" class="domain">
|
||||
${(item as EntityComboBoxItem).domain_name}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
};
|
||||
|
||||
private _noTargetFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.target-picker.no_target_found", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.add-target-wrapper {
|
||||
@@ -1098,8 +683,31 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
margin-top: var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
wa-popover {
|
||||
--wa-space-l: var(--ha-space-0);
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
width: min(max(var(--body-width), 336px), 600px);
|
||||
max-width: min(max(var(--body-width), 336px), 600px);
|
||||
max-height: 500px;
|
||||
height: 70vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-height: 1000px) {
|
||||
wa-popover::part(body) {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-bottom-sheet {
|
||||
--ha-bottom-sheet-height: 90vh;
|
||||
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
||||
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
|
||||
--ha-bottom-sheet-max-width: 600px;
|
||||
--ha-bottom-sheet-padding: var(--ha-space-0);
|
||||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
||||
}
|
||||
|
||||
${unsafeCSS(chipStyles)}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -6,9 +8,6 @@ import {
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -32,8 +31,6 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
*
|
||||
* @slot header - Replace the entire header area.
|
||||
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
|
||||
* @slot headerTitle - Custom title content (used when header-title is not set).
|
||||
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
|
||||
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
|
||||
* @slot - Dialog content body.
|
||||
* @slot footer - Dialog footer content.
|
||||
@@ -55,8 +52,8 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
* @attr {boolean} open - Controls the dialog open state.
|
||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
|
||||
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
|
||||
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
|
||||
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
|
||||
* @attr {string} header-title - Header title text when no custom title slot is provided.
|
||||
* @attr {string} header-subtitle - Header subtitle text when no custom subtitle slot is provided.
|
||||
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
|
||||
* @attr {boolean} flexcontent - Makes the dialog body a flex container for flexible layouts.
|
||||
*
|
||||
@@ -75,12 +72,6 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
export class HaWaDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@property({ attribute: "aria-describedby" })
|
||||
public ariaDescribedBy?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
|
||||
@@ -90,11 +81,11 @@ export class HaWaDialog extends LitElement {
|
||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||
public preventScrimClose = false;
|
||||
|
||||
@property({ attribute: "header-title" })
|
||||
public headerTitle?: string;
|
||||
@property({ type: String, attribute: "header-title" })
|
||||
public headerTitle = "";
|
||||
|
||||
@property({ attribute: "header-subtitle" })
|
||||
public headerSubtitle?: string;
|
||||
@property({ type: String, attribute: "header-subtitle" })
|
||||
public headerSubtitle = "";
|
||||
|
||||
@property({ type: String, attribute: "header-subtitle-position" })
|
||||
public headerSubtitlePosition: "above" | "below" = "below";
|
||||
@@ -126,11 +117,6 @@ export class HaWaDialog extends LitElement {
|
||||
.open=${this._open}
|
||||
.lightDismiss=${!this.preventScrimClose}
|
||||
without-header
|
||||
aria-labelledby=${ifDefined(
|
||||
this.ariaLabelledBy ||
|
||||
(this.headerTitle !== undefined ? "ha-wa-dialog-title" : undefined)
|
||||
)}
|
||||
aria-describedby=${ifDefined(this.ariaDescribedBy)}
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-after-show=${this._handleAfterShow}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
@@ -147,14 +133,14 @@ export class HaWaDialog extends LitElement {
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle
|
||||
? html`<span slot="title" class="title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
: nothing}
|
||||
${this.headerSubtitle
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
: nothing}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>
|
||||
|
||||
@@ -545,7 +545,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
name: entityName || deviceName || item,
|
||||
context,
|
||||
stateObject,
|
||||
notFound: !stateObject && item !== "all" && item !== "none",
|
||||
notFound: !stateObject && item !== "all",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
1105
src/components/target-picker/ha-target-picker-selector.ts
Normal file
1105
src/components/target-picker/ha-target-picker-selector.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -128,7 +128,9 @@ class HaUserPicker extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.user-picker.no_match"
|
||||
)}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
@@ -147,11 +149,6 @@ class HaUserPicker extends LitElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.user-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -24,54 +24,11 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||
area?: AreaRegistryEntry;
|
||||
}
|
||||
|
||||
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
|
||||
floor?: FloorRegistryEntry;
|
||||
areas: FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
|
||||
areas: FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
export interface AreaFloorValue {
|
||||
id: string;
|
||||
type: "floor" | "area";
|
||||
}
|
||||
|
||||
export const getAreasNestedInFloors = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
formatId: (value: AreaFloorValue) => string,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[],
|
||||
includeEmptyFloors = false
|
||||
) =>
|
||||
getAreasAndFloorsItems(
|
||||
states,
|
||||
haFloors,
|
||||
haAreas,
|
||||
haDevices,
|
||||
haEntities,
|
||||
formatId,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
excludeFloors,
|
||||
includeEmptyFloors,
|
||||
true
|
||||
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
|
||||
|
||||
export const getAreasAndFloors = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
@@ -85,47 +42,8 @@ export const getAreasAndFloors = (
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[],
|
||||
includeEmptyFloors = false
|
||||
) =>
|
||||
getAreasAndFloorsItems(
|
||||
states,
|
||||
haFloors,
|
||||
haAreas,
|
||||
haDevices,
|
||||
haEntities,
|
||||
formatId,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
excludeFloors,
|
||||
includeEmptyFloors
|
||||
) as FloorComboBoxItem[];
|
||||
|
||||
const getAreasAndFloorsItems = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
formatId: (value: AreaFloorValue) => string,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[],
|
||||
includeEmptyFloors = false,
|
||||
nested = false
|
||||
): (
|
||||
| FloorComboBoxItem
|
||||
| FloorNestedComboBoxItem
|
||||
| UnassignedAreasFloorComboBoxItem
|
||||
)[] => {
|
||||
excludeFloors?: string[]
|
||||
): FloorComboBoxItem[] => {
|
||||
const floors = Object.values(haFloors);
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
@@ -271,14 +189,6 @@ const getAreasAndFloorsItems = (
|
||||
|
||||
const compare = floorCompare(haFloors);
|
||||
|
||||
if (includeEmptyFloors) {
|
||||
Object.values(haFloors).forEach((floor) => {
|
||||
if (!floorAreaLookup[floor.floor_id]) {
|
||||
floorAreaLookup[floor.floor_id] = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const floorAreaEntries: [
|
||||
FloorRegistryEntry | undefined,
|
||||
@@ -290,15 +200,9 @@ const getAreasAndFloorsItems = (
|
||||
})
|
||||
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
|
||||
|
||||
const items: (
|
||||
| FloorComboBoxItem
|
||||
| FloorNestedComboBoxItem
|
||||
| UnassignedAreasFloorComboBoxItem
|
||||
)[] = [];
|
||||
const items: FloorComboBoxItem[] = [];
|
||||
|
||||
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||
let floorItem: FloorComboBoxItem | FloorNestedComboBoxItem;
|
||||
|
||||
if (floor) {
|
||||
const floorName = computeFloorName(floor);
|
||||
|
||||
@@ -309,7 +213,7 @@ const getAreasAndFloorsItems = (
|
||||
})
|
||||
.flat();
|
||||
|
||||
floorItem = {
|
||||
items.push({
|
||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||
type: "floor",
|
||||
primary: floorName,
|
||||
@@ -321,9 +225,10 @@ const getAreasAndFloorsItems = (
|
||||
...floor.aliases,
|
||||
...areaSearchLabels,
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
const floorAreasItems = floorAreas.map((area) => {
|
||||
items.push(
|
||||
...floorAreas.map((area) => {
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
@@ -333,20 +238,12 @@ const getAreasAndFloorsItems = (
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (floor) {
|
||||
items.push(floorItem!);
|
||||
}
|
||||
|
||||
if (nested && floor) {
|
||||
(floorItem! as FloorNestedComboBoxItem).areas = floorAreasItems;
|
||||
} else {
|
||||
items.push(...floorAreasItems);
|
||||
}
|
||||
});
|
||||
|
||||
const unassignedAreaItems = unassignedAreas.map((area) => {
|
||||
items.push(
|
||||
...unassignedAreas.map((area) => {
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
@@ -356,15 +253,8 @@ const getAreasAndFloorsItems = (
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||
};
|
||||
});
|
||||
|
||||
if (nested && unassignedAreaItems.length) {
|
||||
items.push({
|
||||
areas: unassignedAreaItems,
|
||||
} as UnassignedAreasFloorComboBoxItem);
|
||||
} else {
|
||||
items.push(...unassignedAreaItems);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -60,12 +60,11 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
|
||||
});
|
||||
|
||||
export const getAreaEntityLookup = (
|
||||
entities: EntityRegistryEntry[],
|
||||
filterHidden = false
|
||||
entities: EntityRegistryEntry[]
|
||||
): AreaEntityLookup => {
|
||||
const areaEntityLookup: AreaEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.area_id || (filterHidden && entity.hidden_by)) {
|
||||
if (!entity.area_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.area_id in areaEntityLookup)) {
|
||||
|
||||
@@ -12,7 +12,6 @@ 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 { WeekdayShort } from "../common/datetime/weekday";
|
||||
|
||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||
@@ -258,11 +257,13 @@ export interface ZoneCondition extends BaseCondition {
|
||||
zone: string;
|
||||
}
|
||||
|
||||
type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
|
||||
|
||||
export interface TimeCondition extends BaseCondition {
|
||||
condition: "time";
|
||||
after?: string;
|
||||
before?: string;
|
||||
weekday?: WeekdayShort | WeekdayShort[];
|
||||
weekday?: Weekday | Weekday[];
|
||||
}
|
||||
|
||||
export interface TemplateCondition extends BaseCondition {
|
||||
|
||||
@@ -107,12 +107,11 @@ export const sortDeviceRegistryByName = (
|
||||
);
|
||||
|
||||
export const getDeviceEntityLookup = (
|
||||
entities: EntityRegistryEntry[],
|
||||
filterHidden = false
|
||||
entities: EntityRegistryEntry[]
|
||||
): DeviceEntityLookup => {
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id || (filterHidden && entity.hidden_by)) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
@@ -187,8 +186,7 @@ export const getDevices = (
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeDevices?: string[],
|
||||
value?: string,
|
||||
idPrefix = ""
|
||||
value?: string
|
||||
): DevicePickerItem[] => {
|
||||
const devices = Object.values(hass.devices);
|
||||
const entities = Object.values(hass.entities);
|
||||
@@ -300,7 +298,7 @@ export const getDevices = (
|
||||
const domainName = domain ? domainToName(hass.localize, domain) : undefined;
|
||||
|
||||
return {
|
||||
id: `${idPrefix}${device.id}`,
|
||||
id: device.id,
|
||||
label: "",
|
||||
primary:
|
||||
deviceName ||
|
||||
|
||||
@@ -102,7 +102,6 @@ export type EnergySolarForecasts = Record<string, EnergySolarForecast>;
|
||||
export interface DeviceConsumptionEnergyPreference {
|
||||
// This is an ever increasing value
|
||||
stat_consumption: string;
|
||||
stat_rate?: string;
|
||||
name?: string;
|
||||
included_in_stat?: string;
|
||||
}
|
||||
@@ -131,17 +130,11 @@ export interface FlowToGridSourceEnergyPreference {
|
||||
number_energy_price: number | null;
|
||||
}
|
||||
|
||||
export interface GridPowerSourceEnergyPreference {
|
||||
// W meter
|
||||
stat_rate: string;
|
||||
}
|
||||
|
||||
export interface GridSourceTypeEnergyPreference {
|
||||
type: "grid";
|
||||
|
||||
flow_from: FlowFromGridSourceEnergyPreference[];
|
||||
flow_to: FlowToGridSourceEnergyPreference[];
|
||||
power?: GridPowerSourceEnergyPreference[];
|
||||
|
||||
cost_adjustment_day: number;
|
||||
}
|
||||
@@ -150,7 +143,6 @@ export interface SolarSourceTypeEnergyPreference {
|
||||
type: "solar";
|
||||
|
||||
stat_energy_from: string;
|
||||
stat_rate?: string;
|
||||
config_entry_solar_forecast: string[] | null;
|
||||
}
|
||||
|
||||
@@ -158,7 +150,6 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
type: "battery";
|
||||
stat_energy_from: string;
|
||||
stat_energy_to: string;
|
||||
stat_rate?: string;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
type: "gas";
|
||||
|
||||
@@ -344,8 +344,7 @@ export const getEntities = (
|
||||
includeUnitOfMeasurement?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeEntities?: string[],
|
||||
value?: string,
|
||||
idPrefix = ""
|
||||
value?: string
|
||||
): EntityComboBoxItem[] => {
|
||||
let items: EntityComboBoxItem[] = [];
|
||||
|
||||
@@ -396,9 +395,10 @@ export const getEntities = (
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
||||
|
||||
return {
|
||||
id: `${idPrefix}${entityId}`,
|
||||
id: entityId,
|
||||
primary: primary,
|
||||
secondary: secondary,
|
||||
domain_name: domainName,
|
||||
@@ -411,6 +411,7 @@ export const getEntities = (
|
||||
friendlyName,
|
||||
entityId,
|
||||
].filter(Boolean) as string[],
|
||||
a11y_label: a11yLabel,
|
||||
stateObj: stateObj,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -101,25 +101,21 @@ export const deleteLabelRegistryEntry = (
|
||||
});
|
||||
|
||||
export const getLabels = (
|
||||
hassStates: HomeAssistant["states"],
|
||||
hassAreas: HomeAssistant["areas"],
|
||||
hassDevices: HomeAssistant["devices"],
|
||||
hassEntities: HomeAssistant["entities"],
|
||||
hass: HomeAssistant,
|
||||
labels?: LabelRegistryEntry[],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeLabels?: string[],
|
||||
idPrefix = ""
|
||||
excludeLabels?: string[]
|
||||
): PickerComboBoxItem[] => {
|
||||
if (!labels || labels.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const devices = Object.values(hassDevices);
|
||||
const entities = Object.values(hassEntities);
|
||||
const devices = Object.values(hass.devices);
|
||||
const entities = Object.values(hass.entities);
|
||||
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
@@ -173,7 +169,7 @@ export const getLabels = (
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -184,7 +180,7 @@ export const getLabels = (
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
@@ -203,7 +199,7 @@ export const getLabels = (
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -211,7 +207,7 @@ export const getLabels = (
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -248,7 +244,7 @@ export const getLabels = (
|
||||
|
||||
if (areaIds) {
|
||||
areaIds.forEach((areaId) => {
|
||||
const area = hassAreas[areaId];
|
||||
const area = hass.areas[areaId];
|
||||
area.labels.forEach((label) => usedLabels.add(label));
|
||||
});
|
||||
}
|
||||
@@ -266,7 +262,7 @@ export const getLabels = (
|
||||
}
|
||||
|
||||
const items = outputLabels.map<PickerComboBoxItem>((label) => ({
|
||||
id: `${idPrefix}${label.label_id}`,
|
||||
id: label.label_id,
|
||||
primary: label.name,
|
||||
secondary: label.description ?? "",
|
||||
icon: label.icon || undefined,
|
||||
|
||||
@@ -222,7 +222,6 @@ export interface StopAction extends BaseAction {
|
||||
|
||||
export interface SequenceAction extends BaseAction {
|
||||
sequence: (ManualScriptConfig | Action)[];
|
||||
metadata?: {};
|
||||
}
|
||||
|
||||
export interface ParallelAction extends BaseAction {
|
||||
@@ -480,7 +479,6 @@ export const migrateAutomationAction = (
|
||||
}
|
||||
|
||||
if (typeof action === "object" && action !== null && "sequence" in action) {
|
||||
delete (action as SequenceAction).metadata;
|
||||
for (const sequenceAction of (action as SequenceAction).sequence) {
|
||||
migrateAutomationAction(sequenceAction);
|
||||
}
|
||||
|
||||
@@ -102,17 +102,6 @@ class DialogEditSidebar extends LitElement {
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
// Add default hidden panels that are missing in hidden
|
||||
for (const panel of panels) {
|
||||
if (
|
||||
panel.default_visible === false &&
|
||||
!this._order.includes(panel.url_path) &&
|
||||
!this._hidden.includes(panel.url_path)
|
||||
) {
|
||||
this._hidden.push(panel.url_path);
|
||||
}
|
||||
}
|
||||
|
||||
const items = [
|
||||
...beforeSpacer,
|
||||
...panels.filter((panel) => this._hidden!.includes(panel.url_path)),
|
||||
|
||||
@@ -20,21 +20,6 @@
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<%= renderTemplate("_style_base.html.template") %>
|
||||
<style>
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
::view-transition-group(launch-screen) {
|
||||
animation-duration: var(--ha-animation-base-duration, 350ms);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
::view-transition-old(launch-screen) {
|
||||
animation: fade-out var(--ha-animation-base-duration, 350ms) ease-out;
|
||||
}
|
||||
html {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
color: var(--primary-text-color, #212121);
|
||||
@@ -47,29 +32,11 @@
|
||||
}
|
||||
}
|
||||
#ha-launch-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
view-transition-name: launch-screen;
|
||||
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;
|
||||
}
|
||||
#ha-launch-screen svg {
|
||||
width: 112px;
|
||||
|
||||
@@ -11,10 +11,14 @@ class HaInitPage extends LitElement {
|
||||
|
||||
@state() private _retryInSeconds = 60;
|
||||
|
||||
@state() private _loadingTimeout = false;
|
||||
|
||||
private _showProgressIndicatorTimeout?: number;
|
||||
|
||||
private _retryInterval?: number;
|
||||
|
||||
private _loadingTimeoutHandle?: number;
|
||||
|
||||
protected render() {
|
||||
return this.error
|
||||
? html`
|
||||
@@ -51,8 +55,35 @@ class HaInitPage extends LitElement {
|
||||
The upgrade may need a long time to complete, please be
|
||||
patient.
|
||||
`
|
||||
: this._loadingTimeout
|
||||
? html`
|
||||
<p>Loading is taking longer than expected.</p>
|
||||
<p class="hint">
|
||||
This could be caused by a slow connection or cached data.
|
||||
</p>
|
||||
`
|
||||
: "Loading data"}
|
||||
</div>
|
||||
${this._loadingTimeout && !this.migration
|
||||
? html`
|
||||
<div class="button-row">
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
@click=${this._retry}
|
||||
>
|
||||
Retry now
|
||||
</ha-button>
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
@click=${this._clearCacheAndRetry}
|
||||
>
|
||||
Clear cache and retry
|
||||
</ha-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -64,10 +95,16 @@ class HaInitPage extends LitElement {
|
||||
if (this._retryInterval) {
|
||||
clearInterval(this._retryInterval);
|
||||
}
|
||||
if (this._loadingTimeoutHandle) {
|
||||
clearTimeout(this._loadingTimeoutHandle);
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("error") && this.error) {
|
||||
if (
|
||||
(changedProperties.has("error") && this.error) ||
|
||||
(changedProperties.has("_loadingTimeout") && this._loadingTimeout)
|
||||
) {
|
||||
import("../components/ha-button");
|
||||
}
|
||||
}
|
||||
@@ -77,6 +114,8 @@ class HaInitPage extends LitElement {
|
||||
import("../components/ha-spinner");
|
||||
}, 5000);
|
||||
|
||||
// Only start retry interval for error state
|
||||
if (this.error) {
|
||||
this._retryInterval = window.setInterval(() => {
|
||||
const remainingSeconds = this._retryInSeconds--;
|
||||
if (remainingSeconds <= 0) {
|
||||
@@ -85,6 +124,14 @@ class HaInitPage extends LitElement {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Start loading timeout for normal loading (not error, not migration)
|
||||
if (!this.error && !this.migration) {
|
||||
this._loadingTimeoutHandle = window.setTimeout(() => {
|
||||
this._loadingTimeout = true;
|
||||
}, 30000); // 30 seconds
|
||||
}
|
||||
}
|
||||
|
||||
private _retry() {
|
||||
if (this._retryInterval) {
|
||||
clearInterval(this._retryInterval);
|
||||
@@ -92,6 +139,25 @@ class HaInitPage extends LitElement {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
private async _clearCacheAndRetry() {
|
||||
// Deregister service worker and clear caches
|
||||
if ("serviceWorker" in navigator) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.getRegistration();
|
||||
if (registration) {
|
||||
await registration.unregister();
|
||||
}
|
||||
if ("caches" in window) {
|
||||
const cacheNames = await caches.keys();
|
||||
await Promise.all(cacheNames.map((name) => caches.delete(name)));
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Ignore errors, we'll reload anyway
|
||||
}
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
flex: 0;
|
||||
@@ -111,12 +177,23 @@ class HaInitPage extends LitElement {
|
||||
.retry-text {
|
||||
margin-top: 0;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 8px;
|
||||
font-size: 0.9em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
p,
|
||||
#loading-text {
|
||||
max-width: 350px;
|
||||
color: var(--primary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ const COMPONENTS = {
|
||||
light: () => import("../panels/light/ha-panel-light"),
|
||||
security: () => import("../panels/security/ha-panel-security"),
|
||||
climate: () => import("../panels/climate/ha-panel-climate"),
|
||||
home: () => import("../panels/home/ha-panel-home"),
|
||||
};
|
||||
|
||||
@customElement("partial-panel-resolver")
|
||||
|
||||
@@ -1,56 +1,82 @@
|
||||
import type { ReactiveElement } from "lit";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import {
|
||||
setupMediaQueryListeners,
|
||||
setupTimeListeners,
|
||||
} from "../common/condition/listeners";
|
||||
import type { Condition } from "../panels/lovelace/common/validate-condition";
|
||||
import { checkConditionsMet } from "../panels/lovelace/common/validate-condition";
|
||||
|
||||
type Constructor<T> = abstract new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
* Base config type that can be used with conditional listeners
|
||||
* Extract media queries from conditions recursively
|
||||
*/
|
||||
export interface ConditionalConfig {
|
||||
visibility?: Condition[];
|
||||
[key: string]: any;
|
||||
export function extractMediaQueries(conditions: Condition[]): string[] {
|
||||
return conditions.reduce<string[]>((array, c) => {
|
||||
if ("conditions" in c && c.conditions) {
|
||||
array.push(...extractMediaQueries(c.conditions));
|
||||
}
|
||||
if (c.condition === "screen" && c.media_query) {
|
||||
array.push(c.media_query);
|
||||
}
|
||||
return array;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to setup media query listeners for conditional visibility
|
||||
*/
|
||||
export function setupMediaQueryListeners(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
addListener: (unsub: () => void) => void,
|
||||
onUpdate: (conditionsMet: boolean) => void
|
||||
): void {
|
||||
const mediaQueries = extractMediaQueries(conditions);
|
||||
|
||||
if (mediaQueries.length === 0) return;
|
||||
|
||||
// Optimization for single media query
|
||||
const hasOnlyMediaQuery =
|
||||
conditions.length === 1 &&
|
||||
conditions[0].condition === "screen" &&
|
||||
!!conditions[0].media_query;
|
||||
|
||||
mediaQueries.forEach((mediaQuery) => {
|
||||
const unsub = listenMediaQuery(mediaQuery, (matches) => {
|
||||
if (hasOnlyMediaQuery) {
|
||||
onUpdate(matches);
|
||||
} else {
|
||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
||||
onUpdate(conditionsMet);
|
||||
}
|
||||
});
|
||||
addListener(unsub);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixin to handle conditional listeners for visibility control
|
||||
*
|
||||
* Provides lifecycle management for listeners that control conditional
|
||||
* visibility of components.
|
||||
* Provides lifecycle management for listeners (media queries, time-based, state changes, etc.)
|
||||
* that control conditional visibility of components.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Extend your component with ConditionalListenerMixin<YourConfigType>(ReactiveElement)
|
||||
* 2. Ensure component has config.visibility or _config.visibility property with conditions
|
||||
* 3. Ensure component has _updateVisibility() or _updateElement() method
|
||||
* 4. Override setupConditionalListeners() if custom behavior needed (e.g., filter conditions)
|
||||
* 1. Extend your component with ConditionalListenerMixin(ReactiveElement)
|
||||
* 2. Override setupConditionalListeners() to setup your listeners
|
||||
* 3. Use addConditionalListener() to register unsubscribe functions
|
||||
* 4. Call clearConditionalListeners() and setupConditionalListeners() when config changes
|
||||
*
|
||||
* The mixin automatically:
|
||||
* - Sets up listeners when component connects to DOM
|
||||
* - Cleans up listeners when component disconnects from DOM
|
||||
* - Handles conditional visibility based on defined conditions
|
||||
*/
|
||||
export const ConditionalListenerMixin = <
|
||||
TConfig extends ConditionalConfig = ConditionalConfig,
|
||||
T extends Constructor<ReactiveElement>,
|
||||
>(
|
||||
superClass: Constructor<ReactiveElement>
|
||||
superClass: T
|
||||
) => {
|
||||
abstract class ConditionalListenerClass extends superClass {
|
||||
private __listeners: (() => void)[] = [];
|
||||
|
||||
protected _config?: TConfig;
|
||||
|
||||
public config?: TConfig;
|
||||
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
protected _updateElement?(config: TConfig): void;
|
||||
|
||||
protected _updateVisibility?(conditionsMet?: boolean): void;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setupConditionalListeners();
|
||||
@@ -61,72 +87,17 @@ export const ConditionalListenerMixin = <
|
||||
this.clearConditionalListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear conditional listeners
|
||||
*
|
||||
* This method is called when the component is disconnected from the DOM.
|
||||
* It clears all the listeners that were set up by the setupConditionalListeners() method.
|
||||
*/
|
||||
protected clearConditionalListeners(): void {
|
||||
this.__listeners.forEach((unsub) => unsub());
|
||||
this.__listeners = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a conditional listener to the list of listeners
|
||||
*
|
||||
* This method is called when a new listener is added.
|
||||
* It adds the listener to the list of listeners.
|
||||
*
|
||||
* @param unsubscribe - The unsubscribe function to call when the listener is no longer needed
|
||||
* @returns void
|
||||
*/
|
||||
protected addConditionalListener(unsubscribe: () => void): void {
|
||||
this.__listeners.push(unsubscribe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup conditional listeners for visibility control
|
||||
*
|
||||
* Default implementation:
|
||||
* - Checks config.visibility or _config.visibility for conditions (if not provided)
|
||||
* - Sets up appropriate listeners based on condition types
|
||||
* - Calls _updateVisibility() or _updateElement() when conditions change
|
||||
*
|
||||
* Override this method to customize behavior (e.g., filter conditions first)
|
||||
* and call super.setupConditionalListeners(customConditions) to reuse the base implementation
|
||||
*
|
||||
* @param conditions - Optional conditions array. If not provided, will check config.visibility or _config.visibility
|
||||
*/
|
||||
protected setupConditionalListeners(conditions?: Condition[]): void {
|
||||
const config = this.config || this._config;
|
||||
const finalConditions = conditions || config?.visibility;
|
||||
|
||||
if (!finalConditions || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onUpdate = (conditionsMet: boolean) => {
|
||||
if (this._updateVisibility) {
|
||||
this._updateVisibility(conditionsMet);
|
||||
} else if (this._updateElement && config) {
|
||||
this._updateElement(config);
|
||||
}
|
||||
};
|
||||
|
||||
setupMediaQueryListeners(
|
||||
finalConditions,
|
||||
this.hass,
|
||||
(unsub) => this.addConditionalListener(unsub),
|
||||
onUpdate
|
||||
);
|
||||
|
||||
setupTimeListeners(
|
||||
finalConditions,
|
||||
this.hass,
|
||||
(unsub) => this.addConditionalListener(unsub),
|
||||
onUpdate
|
||||
);
|
||||
protected setupConditionalListeners(): void {
|
||||
// Override in subclass
|
||||
}
|
||||
}
|
||||
return ConditionalListenerClass;
|
||||
|
||||
@@ -104,6 +104,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
|
||||
@@ -588,11 +588,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
...this._clipboard,
|
||||
action: deepClone(this.action),
|
||||
};
|
||||
let action = this.action;
|
||||
if ("sequence" in action) {
|
||||
action = { ...this.action, metadata: {} };
|
||||
}
|
||||
copyToClipboard(dump(action));
|
||||
copyToClipboard(dump(this.action));
|
||||
}
|
||||
|
||||
private _onDisable = () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiClose,
|
||||
@@ -6,7 +5,6 @@ import {
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { SingleHassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
@@ -22,17 +20,12 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeFloorName } from "../../../common/entity/compute_floor_name";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../../common/translations/localize";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import "../../../components/ha-bottom-sheet";
|
||||
@@ -46,7 +39,6 @@ import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-list";
|
||||
import type { HaMdList } from "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-section-title";
|
||||
import "../../../components/ha-service-icon";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/search-input";
|
||||
@@ -58,10 +50,6 @@ import {
|
||||
getService,
|
||||
isService,
|
||||
} from "../../../data/action";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../data/area_registry";
|
||||
import type {
|
||||
AutomationElementGroup,
|
||||
AutomationElementGroupCollection,
|
||||
@@ -71,10 +59,6 @@ import {
|
||||
CONDITION_COLLECTIONS,
|
||||
CONDITION_ICONS,
|
||||
} from "../../../data/condition";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import { getDeviceEntityLookup } from "../../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { getFloorAreaLookup } from "../../../data/floor_registry";
|
||||
import { getServiceIcons } from "../../../data/icons";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import {
|
||||
@@ -88,8 +72,6 @@ import { HaFuse } from "../../../resources/fuse";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "./add-automation-element/ha-automation-add-from-target";
|
||||
import type HaAutomationAddFromTarget from "./add-automation-element/ha-automation-add-from-target";
|
||||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||||
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
||||
|
||||
@@ -144,9 +126,7 @@ class DialogAddAutomationElement
|
||||
|
||||
@state() private _selectedGroup?: string;
|
||||
|
||||
@state() private _selectedTarget?: SingleHassServiceTarget;
|
||||
|
||||
@state() private _tab: "targets" | "groups" | "blocks" = "targets";
|
||||
@state() private _tab: "groups" | "blocks" = "groups";
|
||||
|
||||
@state() private _filter = "";
|
||||
|
||||
@@ -162,22 +142,12 @@ class DialogAddAutomationElement
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
private fullEntities!: EntityRegistryEntry[];
|
||||
|
||||
@query(".items ha-md-list ha-md-list-item")
|
||||
private _itemsListFirstElement?: HaMdList;
|
||||
|
||||
@query("ha-automation-add-from-target")
|
||||
private _targetPickerElement?: HaAutomationAddFromTarget;
|
||||
|
||||
@query(".items")
|
||||
private _itemsListElement?: HTMLDivElement;
|
||||
|
||||
@query(".content")
|
||||
private _contentElement?: HTMLDivElement;
|
||||
|
||||
private _fullScreen = false;
|
||||
|
||||
private _removeKeyboardShortcuts?: () => void;
|
||||
@@ -214,8 +184,7 @@ class DialogAddAutomationElement
|
||||
this._bottomSheetMode = false;
|
||||
this._params = undefined;
|
||||
this._selectedGroup = undefined;
|
||||
this._tab = "targets";
|
||||
this._selectedTarget = undefined;
|
||||
this._tab = "groups";
|
||||
this._selectedCollectionIndex = undefined;
|
||||
this._filter = "";
|
||||
this._manifests = undefined;
|
||||
@@ -662,11 +631,10 @@ class DialogAddAutomationElement
|
||||
private _renderContent() {
|
||||
const automationElementType = this._params!.type;
|
||||
|
||||
const items =
|
||||
this._filter || this._selectedTarget
|
||||
const items = this._filter
|
||||
? this._getFilteredItems(
|
||||
automationElementType,
|
||||
this._filter || "Ti",
|
||||
this._filter,
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._manifests
|
||||
@@ -703,25 +671,35 @@ class DialogAddAutomationElement
|
||||
this._manifests
|
||||
);
|
||||
|
||||
const groupName = isService(this._selectedGroup)
|
||||
? domainToName(
|
||||
this.hass.localize,
|
||||
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
|
||||
) ||
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys
|
||||
);
|
||||
|
||||
const typeTitle = this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.add`
|
||||
);
|
||||
|
||||
const tabButtons = [
|
||||
{
|
||||
label: this.hass.localize(`ui.panel.config.automation.editor.targets`),
|
||||
value: "targets",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.name`
|
||||
),
|
||||
value: "groups",
|
||||
},
|
||||
];
|
||||
|
||||
if (this._params?.type !== "trigger") {
|
||||
tabButtons.push({
|
||||
{
|
||||
label: this.hass.localize(`ui.panel.config.automation.editor.blocks`),
|
||||
value: "blocks",
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const hideCollections =
|
||||
this._filter ||
|
||||
@@ -730,8 +708,28 @@ class DialogAddAutomationElement
|
||||
|
||||
return html`
|
||||
<div slot="header">
|
||||
${this._renderHeader()}
|
||||
${!this._narrow || (!this._selectedGroup && !this._selectedTarget)
|
||||
<ha-dialog-header subtitle-position="above">
|
||||
<span slot="title"
|
||||
>${this._narrow && this._selectedGroup
|
||||
? groupName
|
||||
: typeTitle}</span
|
||||
>
|
||||
|
||||
${this._narrow && this._selectedGroup
|
||||
? html`<span slot="subtitle">${typeTitle}</span>`
|
||||
: nothing}
|
||||
${this._narrow && this._selectedGroup
|
||||
? html`<ha-icon-button-prev
|
||||
slot="navigationIcon"
|
||||
@click=${this._back}
|
||||
></ha-icon-button-prev>`
|
||||
: html`<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
@click=${this._close}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button>`}
|
||||
</ha-dialog-header>
|
||||
${!this._narrow || !this._selectedGroup
|
||||
? html`
|
||||
<search-input
|
||||
?autofocus=${!this._narrow}
|
||||
@@ -744,8 +742,9 @@ class DialogAddAutomationElement
|
||||
></search-input>
|
||||
`
|
||||
: nothing}
|
||||
${!this._filter &&
|
||||
(!this._narrow || (!this._selectedGroup && !this._selectedTarget))
|
||||
${this._params?.type !== "trigger" &&
|
||||
!this._filter &&
|
||||
(!this._narrow || !this._selectedGroup)
|
||||
? html`<ha-button-toggle-group
|
||||
variant="neutral"
|
||||
active-variant="brand"
|
||||
@@ -757,21 +756,7 @@ class DialogAddAutomationElement
|
||||
></ha-button-toggle-group>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div
|
||||
class=${classMap({
|
||||
content: true,
|
||||
column: this._narrow && this._selectedTarget,
|
||||
})}
|
||||
>
|
||||
${this._tab === "targets"
|
||||
? html`<ha-automation-add-from-target
|
||||
.hass=${this.hass}
|
||||
.value=${this._selectedTarget}
|
||||
@value-changed=${this._handleTargetSelected}
|
||||
.narrow=${this._narrow}
|
||||
class=${this._getAddFromTargetHidden()}
|
||||
></ha-automation-add-from-target>`
|
||||
: html`
|
||||
<div class="content">
|
||||
<ha-md-list
|
||||
class=${classMap({
|
||||
groups: true,
|
||||
@@ -827,17 +812,14 @@ class DialogAddAutomationElement
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
<ha-md-divider
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>`
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||
: nothing}
|
||||
${collections.map(
|
||||
(collection, index) => html`
|
||||
${collection.titleKey
|
||||
? html`<ha-section-title>
|
||||
? html`<div class="collection-title">
|
||||
${this.hass.localize(collection.titleKey)}
|
||||
</ha-section-title>`
|
||||
</div>`
|
||||
: nothing}
|
||||
${repeat(
|
||||
collection.groups,
|
||||
@@ -849,9 +831,7 @@ class DialogAddAutomationElement
|
||||
.value=${item.key}
|
||||
.index=${index}
|
||||
@click=${this._groupSelected}
|
||||
class=${item.key === this._selectedGroup
|
||||
? "selected"
|
||||
: ""}
|
||||
class=${item.key === this._selectedGroup ? "selected" : ""}
|
||||
>
|
||||
<div slot="headline">${item.name}</div>
|
||||
${item.icon
|
||||
@@ -868,27 +848,18 @@ class DialogAddAutomationElement
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
`}
|
||||
<div
|
||||
class=${classMap({
|
||||
items: true,
|
||||
blank:
|
||||
(this._tab === "groups" &&
|
||||
!this._selectedGroup &&
|
||||
!this._filter) ||
|
||||
(this._tab === "targets" &&
|
||||
!this._selectedTarget &&
|
||||
!this._filter),
|
||||
!this._selectedGroup && !this._filter && this._tab === "groups",
|
||||
"empty-search":
|
||||
!items?.length && !filteredBlockItems?.length && this._filter,
|
||||
hidden:
|
||||
this._narrow &&
|
||||
!this._selectedGroup &&
|
||||
(!this._selectedTarget ||
|
||||
(this._selectedTarget &&
|
||||
!Object.values(this._selectedTarget)[0])) &&
|
||||
!this._filter &&
|
||||
this._tab !== "blocks",
|
||||
this._tab === "groups",
|
||||
})}
|
||||
@scroll=${this._onItemsScroll}
|
||||
>
|
||||
@@ -902,10 +873,6 @@ class DialogAddAutomationElement
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.select`
|
||||
)
|
||||
: this._tab === "targets" && !this._selectedTarget && !this._filter
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.automation.editor.select_target`
|
||||
)
|
||||
: !items?.length &&
|
||||
this._filter &&
|
||||
(!filteredBlockItems || !filteredBlockItems.length)
|
||||
@@ -1018,10 +985,6 @@ class DialogAddAutomationElement
|
||||
}
|
||||
|
||||
private _back() {
|
||||
if (this._selectedTarget) {
|
||||
this._targetPickerElement?.navigateBack();
|
||||
return;
|
||||
}
|
||||
this._selectedGroup = undefined;
|
||||
}
|
||||
|
||||
@@ -1045,21 +1008,6 @@ class DialogAddAutomationElement
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _handleTargetSelected = (
|
||||
ev: CustomEvent<{ value: SingleHassServiceTarget }>
|
||||
) => {
|
||||
this._selectedTarget = ev.detail.value;
|
||||
|
||||
// TODO fix on mobile
|
||||
requestAnimationFrame(() => {
|
||||
if (this._narrow) {
|
||||
this._contentElement?.scrollTo(0, 0);
|
||||
} else {
|
||||
this._itemsListElement?.scrollTo(0, 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private _debounceFilterChanged = debounce(
|
||||
(ev) => this._filterChanged(ev),
|
||||
200
|
||||
@@ -1156,183 +1104,6 @@ class DialogAddAutomationElement
|
||||
this.closeDialog();
|
||||
};
|
||||
|
||||
private _renderHeader() {
|
||||
return html`
|
||||
<ha-dialog-header subtitle-position="above">
|
||||
<span slot="title">${this._getDialogTitle()}</span>
|
||||
|
||||
${this._renderDialogSubtitle()}
|
||||
${this._narrow && (this._selectedGroup || this._selectedTarget)
|
||||
? html`<ha-icon-button-prev
|
||||
slot="navigationIcon"
|
||||
@click=${this._back}
|
||||
></ha-icon-button-prev>`
|
||||
: html`<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
@click=${this._close}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button>`}
|
||||
</ha-dialog-header>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getDialogTitle() {
|
||||
if (this._narrow && this._selectedGroup) {
|
||||
return isService(this._selectedGroup)
|
||||
? domainToName(
|
||||
this.hass.localize,
|
||||
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
|
||||
) ||
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys
|
||||
);
|
||||
}
|
||||
|
||||
if (this._narrow && this._selectedTarget) {
|
||||
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
|
||||
this._selectedTarget
|
||||
);
|
||||
|
||||
if (targetId === undefined && targetType === "floor") {
|
||||
return this.hass.localize("ui.components.area-picker.unassigned_areas");
|
||||
}
|
||||
|
||||
if (targetId) {
|
||||
if (targetType === "floor") {
|
||||
return computeFloorName(this.hass.floors[targetId]) || targetId;
|
||||
}
|
||||
if (targetType === "area") {
|
||||
return computeAreaName(this.hass.areas[targetId]) || targetId;
|
||||
}
|
||||
if (targetType === "device") {
|
||||
return computeDeviceName(this.hass.devices[targetId]) || targetId;
|
||||
}
|
||||
if (targetType === "entity" && this.hass.states[targetId]) {
|
||||
const stateObj = this.hass.states[targetId];
|
||||
const [entityName, deviceName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
return entityName || deviceName || targetId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${this._params!.type}s.add`
|
||||
);
|
||||
}
|
||||
|
||||
private _renderDialogSubtitle() {
|
||||
if (!this._narrow) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._selectedGroup) {
|
||||
return html`<span slot="subtitle"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${this._params!.type}s.add`
|
||||
)}</span
|
||||
>`;
|
||||
}
|
||||
|
||||
if (this._selectedTarget) {
|
||||
let subtitle: string | undefined;
|
||||
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
|
||||
this._selectedTarget
|
||||
);
|
||||
|
||||
if (targetId) {
|
||||
if (targetType === "area" && this.hass.areas[targetId].floor_id) {
|
||||
const floorId = this.hass.areas[targetId].floor_id;
|
||||
subtitle = computeFloorName(this.hass.floors[floorId]) || floorId;
|
||||
}
|
||||
if (targetType === "device" && this.hass.devices[targetId].area_id) {
|
||||
const areaId = this.hass.devices[targetId].area_id;
|
||||
subtitle = computeAreaName(this.hass.areas[areaId]) || areaId;
|
||||
}
|
||||
if (targetType === "entity" && this.hass.states[targetId]) {
|
||||
const stateObj = this.hass.states[targetId];
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
subtitle = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
|
||||
}
|
||||
}
|
||||
|
||||
if (subtitle) {
|
||||
return html`<span slot="subtitle">${subtitle}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _getFloorAreaLookupMemoized = memoizeOne(
|
||||
(areas: HomeAssistant["areas"]) => getFloorAreaLookup(Object.values(areas))
|
||||
);
|
||||
|
||||
private _getAreaDeviceLookupMemoized = memoizeOne(
|
||||
(devices: HomeAssistant["devices"]) =>
|
||||
getAreaDeviceLookup(Object.values(devices))
|
||||
);
|
||||
|
||||
private _getAreaEntityLookupMemoized = memoizeOne(getAreaEntityLookup);
|
||||
|
||||
private _getDeviceEntityLookupMemoized = memoizeOne(getDeviceEntityLookup);
|
||||
|
||||
private _extractTypeAndIdFromTarget = memoizeOne(
|
||||
(target: SingleHassServiceTarget): [string, string | undefined] => {
|
||||
const [targetTypeId, targetId] = Object.entries(target)[0];
|
||||
const targetType = targetTypeId.replace("_id", "");
|
||||
return [targetType, targetId];
|
||||
}
|
||||
);
|
||||
|
||||
private _getAddFromTargetHidden() {
|
||||
if (this._narrow && this._selectedTarget) {
|
||||
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
|
||||
this._selectedTarget
|
||||
);
|
||||
|
||||
if (
|
||||
targetId &&
|
||||
((targetType === "floor" &&
|
||||
this._getFloorAreaLookupMemoized(this.hass.areas)[targetId]
|
||||
?.length === 0) ||
|
||||
(targetType === "area" &&
|
||||
this._getAreaDeviceLookupMemoized(this.hass.devices)[targetId]
|
||||
?.length === 0 &&
|
||||
this._getAreaEntityLookupMemoized(this.fullEntities, true)[targetId]
|
||||
?.length === 0) ||
|
||||
(targetType === "device" &&
|
||||
this._getDeviceEntityLookupMemoized(this.fullEntities, true)[
|
||||
targetId
|
||||
]?.length === 0))
|
||||
) {
|
||||
return "hidden";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
@@ -1386,15 +1157,10 @@ class DialogAddAutomationElement
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.content.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-automation-add-from-target,
|
||||
.groups {
|
||||
overflow: auto;
|
||||
flex: 3;
|
||||
@@ -1402,13 +1168,6 @@ class DialogAddAutomationElement
|
||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
margin: var(--ha-space-3);
|
||||
margin-inline-end: var(--ha-space-0);
|
||||
}
|
||||
|
||||
ha-automation-add-from-target.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.groups {
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-1);
|
||||
@@ -1416,8 +1175,7 @@ class DialogAddAutomationElement
|
||||
--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,
|
||||
ha-bottom-sheet ha-automation-add-from-target {
|
||||
ha-bottom-sheet .groups {
|
||||
margin: var(--ha-space-3);
|
||||
}
|
||||
.groups .selected {
|
||||
@@ -1429,9 +1187,16 @@ class DialogAddAutomationElement
|
||||
color: var(--ha-color-on-primary-normal);
|
||||
}
|
||||
|
||||
ha-section-title {
|
||||
.collection-title {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
top: 0;
|
||||
position: sticky;
|
||||
min-height: var(--ha-space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -1442,11 +1207,6 @@ class DialogAddAutomationElement
|
||||
flex: 7;
|
||||
}
|
||||
|
||||
.content.column ha-automation-add-from-target,
|
||||
.content.column .items {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
ha-wa-dialog .items {
|
||||
margin-top: var(--ha-space-3);
|
||||
}
|
||||
|
||||
@@ -1,810 +0,0 @@
|
||||
import "@home-assistant/webawesome/dist/components/tree-item/tree-item";
|
||||
import "@home-assistant/webawesome/dist/components/tree/tree";
|
||||
import type { WaSelectionChangeEvent } from "@home-assistant/webawesome/dist/events/selection-change";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiSelectionMarker, mdiTextureBox } from "@mdi/js";
|
||||
import type { SingleHassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-floor-icon";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-section-title";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
getAreasNestedInFloors,
|
||||
type AreaFloorValue,
|
||||
type FloorComboBoxItem,
|
||||
type FloorNestedComboBoxItem,
|
||||
type UnassignedAreasFloorComboBoxItem,
|
||||
} from "../../../../data/area_floor";
|
||||
import {
|
||||
getConfigEntries,
|
||||
type ConfigEntry,
|
||||
} from "../../../../data/config_entries";
|
||||
import {
|
||||
areasContext,
|
||||
devicesContext,
|
||||
entitiesContext,
|
||||
floorsContext,
|
||||
labelsContext,
|
||||
localizeContext,
|
||||
statesContext,
|
||||
} from "../../../../data/context";
|
||||
import {
|
||||
getLabels,
|
||||
type LabelRegistryEntry,
|
||||
} from "../../../../data/label_registry";
|
||||
import { extractFromTarget } from "../../../../data/target";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { brandsUrl } from "../../../../util/brands-url";
|
||||
|
||||
const SEPARATOR = "________";
|
||||
|
||||
interface DeviceEntries {
|
||||
open: boolean;
|
||||
entities: string[];
|
||||
}
|
||||
|
||||
interface AreaEntries {
|
||||
open: boolean;
|
||||
devices: Record<string, DeviceEntries>;
|
||||
entities: string[];
|
||||
}
|
||||
|
||||
@customElement("ha-automation-add-from-target")
|
||||
export default class HaAutomationAddFromTarget extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public value?: SingleHassServiceTarget;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
// #region context
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: HomeAssistant["localize"];
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private states!: HomeAssistant["states"];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
private floors!: HomeAssistant["floors"];
|
||||
|
||||
@state()
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
private areas!: HomeAssistant["areas"];
|
||||
|
||||
@state()
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
private devices!: HomeAssistant["devices"];
|
||||
|
||||
@state()
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private entities!: HomeAssistant["entities"];
|
||||
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
private _labelRegistry!: LabelRegistryEntry[];
|
||||
// #endregion context
|
||||
|
||||
@state()
|
||||
private _floorAreas: (
|
||||
| FloorNestedComboBoxItem
|
||||
| UnassignedAreasFloorComboBoxItem
|
||||
)[] = [];
|
||||
|
||||
@state()
|
||||
private _areaEntries: Record<string, AreaEntries> = {};
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._loadConfigEntries();
|
||||
this._getTreeData();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${!this.narrow || !this.value ? this._renderFloors() : nothing}
|
||||
${this.narrow && this.value ? this._renderNarrow(this.value) : nothing}
|
||||
${!this.narrow || !this.value ? this._renderLabels() : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderNarrow(value: SingleHassServiceTarget) {
|
||||
const [valueTypeId, valueId] = Object.entries(value)[0];
|
||||
const valueType = valueTypeId.replace("_id", "");
|
||||
|
||||
if (!valueType || valueType === "label") {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (valueType === "floor") {
|
||||
return this._renderAreas(
|
||||
this._floorAreas.find(
|
||||
(floor) =>
|
||||
(valueId && floor.id === `${valueType}${SEPARATOR}${valueId}`) ||
|
||||
(!valueId && !floor.id)
|
||||
)?.areas
|
||||
);
|
||||
}
|
||||
|
||||
if (valueType === "area") {
|
||||
const { devices, entities } =
|
||||
this._areaEntries[`area${SEPARATOR}${valueId}`];
|
||||
const numberOfDevices = Object.keys(devices).length;
|
||||
|
||||
return html`
|
||||
${numberOfDevices ? this._renderDevices(devices) : nothing}
|
||||
${entities.length ? this._renderEntities(entities) : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
if (valueType === "device" && this.devices[valueId]) {
|
||||
const deviceArea = this.devices[valueId].area_id!;
|
||||
return this._renderEntities(
|
||||
this._areaEntries[`area${SEPARATOR}${deviceArea}`]?.devices[valueId]
|
||||
?.entities
|
||||
);
|
||||
}
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _renderFloors() {
|
||||
return html`<ha-section-title
|
||||
>${this.localize(
|
||||
"ui.panel.config.automation.editor.home"
|
||||
)}</ha-section-title
|
||||
>
|
||||
${!this._floorAreas.length ||
|
||||
(!this._floorAreas[0].id && !this._floorAreas[0].areas.length)
|
||||
? html`<ha-md-list>
|
||||
<ha-md-list-item type="text">
|
||||
<div slot="headline">
|
||||
${this.localize("ui.components.area-picker.no_areas")}
|
||||
</div>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>`
|
||||
: this.narrow
|
||||
? html`<ha-md-list>
|
||||
${this._floorAreas.map((floor, index) =>
|
||||
index === 0 && !floor.id
|
||||
? this._renderAreas(floor.areas)
|
||||
: html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.target=${floor.id || `floor${SEPARATOR}`}
|
||||
@click=${this._selectItem}
|
||||
>
|
||||
${floor.id && (floor as FloorNestedComboBoxItem).floor
|
||||
? html`<ha-floor-icon
|
||||
slot="start"
|
||||
.floor=${(floor as FloorNestedComboBoxItem).floor}
|
||||
></ha-floor-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiSelectionMarker}
|
||||
></ha-svg-icon>`}
|
||||
|
||||
<div slot="headline">
|
||||
${!floor.id
|
||||
? this.localize(
|
||||
"ui.components.area-picker.unassigned_areas"
|
||||
)
|
||||
: floor.primary}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>`
|
||||
)}
|
||||
</ha-md-list>`
|
||||
: html`<wa-tree @wa-selection-change=${this._handleSelectionChange}>
|
||||
${this._floorAreas.map((floor, index) =>
|
||||
index === 0 && !floor.id
|
||||
? this._renderAreas(floor.areas)
|
||||
: html`<wa-tree-item
|
||||
.disabledSelection=${!floor.id}
|
||||
.target=${floor.id}
|
||||
.selected=${!!floor.id &&
|
||||
this._getSelectedTargetId(this.value) === floor.id}
|
||||
>
|
||||
${floor.id && (floor as FloorNestedComboBoxItem).floor
|
||||
? html`<ha-floor-icon
|
||||
.floor=${(floor as FloorNestedComboBoxItem).floor}
|
||||
></ha-floor-icon>`
|
||||
: html`<ha-svg-icon
|
||||
.path=${mdiSelectionMarker}
|
||||
></ha-svg-icon>`}
|
||||
${!floor.id
|
||||
? this.localize(
|
||||
"ui.components.area-picker.unassigned_areas"
|
||||
)
|
||||
: floor.primary}
|
||||
${this._renderAreas(floor.areas)}
|
||||
</wa-tree-item>`
|
||||
)}
|
||||
</wa-tree>`} `;
|
||||
}
|
||||
|
||||
private _renderLabels() {
|
||||
const labels = this._getLabelsMemoized(
|
||||
this.states,
|
||||
this.areas,
|
||||
this.devices,
|
||||
this.entities,
|
||||
this._labelRegistry,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`label${SEPARATOR}`
|
||||
);
|
||||
|
||||
if (!labels.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-section-title
|
||||
>${this.localize("ui.components.label-picker.labels")}</ha-section-title
|
||||
>
|
||||
<ha-md-list>
|
||||
${labels.map(
|
||||
(label) =>
|
||||
html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.target=${label.id}
|
||||
@click=${this._selectItem}
|
||||
class=${this._getSelectedTargetId(this.value) === label.id
|
||||
? "selected"
|
||||
: ""}
|
||||
>${label.icon
|
||||
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
|
||||
: label.icon_path
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${label.icon_path}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<div slot="headline">${label.primary}</div>
|
||||
${this.narrow
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next> `
|
||||
: nothing}
|
||||
</ha-md-list-item>`
|
||||
)}
|
||||
</ha-md-list>`;
|
||||
}
|
||||
|
||||
private _renderAreas(areas: FloorComboBoxItem[] = []) {
|
||||
if (!areas.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this.narrow) {
|
||||
return html`<ha-section-title
|
||||
>${this.localize(
|
||||
"ui.components.target-picker.type.areas"
|
||||
)}</ha-section-title
|
||||
>
|
||||
<ha-md-list>
|
||||
${areas.map(({ id, primary, icon, icon_path }) => {
|
||||
if (!this._areaEntries[id]) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.target=${id}
|
||||
@click=${this._selectItem}
|
||||
>
|
||||
${icon
|
||||
? html`<ha-icon slot="start" .icon=${icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${icon_path || mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
|
||||
<div slot="headline">${primary}</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>`;
|
||||
})}
|
||||
</ha-md-list>`;
|
||||
}
|
||||
|
||||
return areas.map(({ id, primary, icon, icon_path }) => {
|
||||
if (!this._areaEntries[id]) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const { open, devices, entities } = this._areaEntries[id];
|
||||
const numberOfDevices = Object.keys(devices).length;
|
||||
const numberOfItems = numberOfDevices + entities.length;
|
||||
|
||||
return html`<wa-tree-item
|
||||
.target=${id}
|
||||
.selected=${this._getSelectedTargetId(this.value) === id}
|
||||
.lazy=${!open && !!numberOfItems}
|
||||
@wa-lazy-load=${this._expandItem}
|
||||
@wa-collapse=${this._collapseItem}
|
||||
>
|
||||
${icon
|
||||
? html`<ha-icon .icon=${icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
.path=${icon_path || mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${primary}
|
||||
${open
|
||||
? html`
|
||||
${numberOfDevices ? this._renderDevices(devices) : nothing}
|
||||
${entities.length ? this._renderEntities(entities) : nothing}
|
||||
`
|
||||
: nothing}
|
||||
</wa-tree-item>`;
|
||||
});
|
||||
}
|
||||
|
||||
private _renderDevices(devices: Record<string, DeviceEntries>) {
|
||||
const renderedDevices = Object.keys(devices).map((deviceId) => {
|
||||
if (!this.devices[deviceId]) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const device = this.devices[deviceId];
|
||||
const configEntry = device.primary_config_entry
|
||||
? this._configEntryLookup?.[device.primary_config_entry]
|
||||
: undefined;
|
||||
const domain = configEntry?.domain;
|
||||
|
||||
const deviceName = computeDeviceName(device) || deviceId;
|
||||
|
||||
if (this.narrow) {
|
||||
return html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.target=${`device${SEPARATOR}${deviceId}`}
|
||||
@click=${this._selectItem}
|
||||
>
|
||||
${domain
|
||||
? html`
|
||||
<img
|
||||
slot="start"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
<div slot="headline">${deviceName}</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>`;
|
||||
}
|
||||
|
||||
const { open, entities } = devices[deviceId];
|
||||
|
||||
return html`<wa-tree-item
|
||||
.target=${`device${SEPARATOR}${deviceId}`}
|
||||
.selected=${this._getSelectedTargetId(this.value) ===
|
||||
`device${SEPARATOR}${deviceId}`}
|
||||
.lazy=${!open && !!entities.length}
|
||||
@wa-lazy-load=${this._expandItem}
|
||||
@wa-collapse=${this._collapseItem}
|
||||
.title=${deviceName}
|
||||
>
|
||||
${domain
|
||||
? html`
|
||||
<img
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
<span class="item-label">${deviceName}</span>
|
||||
${open ? this._renderEntities(entities) : nothing}
|
||||
</wa-tree-item>`;
|
||||
});
|
||||
|
||||
if (this.narrow) {
|
||||
return html`<ha-section-title
|
||||
>${this.localize(
|
||||
"ui.components.target-picker.type.devices"
|
||||
)}</ha-section-title
|
||||
>
|
||||
<ha-md-list> ${renderedDevices} </ha-md-list>`;
|
||||
}
|
||||
|
||||
return renderedDevices;
|
||||
}
|
||||
|
||||
private _renderEntities(entities: string[] = []) {
|
||||
if (!entities.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const renderedEntites = entities.map((entityId) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
|
||||
const [entityName, deviceName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.entities,
|
||||
this.devices,
|
||||
this.areas,
|
||||
this.floors
|
||||
);
|
||||
|
||||
const label = entityName || deviceName || entityId;
|
||||
|
||||
if (this.narrow) {
|
||||
return html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.target=${`entity${SEPARATOR}${entityId}`}
|
||||
@click=${this._selectItem}
|
||||
>
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
<div slot="headline" class="item-label">${label}</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>`;
|
||||
}
|
||||
|
||||
return html`<wa-tree-item
|
||||
.target=${`entity${SEPARATOR}${entityId}`}
|
||||
.selected=${this._getSelectedTargetId(this.value) ===
|
||||
`entity${SEPARATOR}${entityId}`}
|
||||
.title=${label}
|
||||
>
|
||||
<state-badge .stateObj=${stateObj} .hass=${this.hass}></state-badge>
|
||||
<span class="item-label">${label}</span>
|
||||
</wa-tree-item>`;
|
||||
});
|
||||
|
||||
if (this.narrow) {
|
||||
return html`<ha-section-title
|
||||
>${this.localize(
|
||||
"ui.components.target-picker.type.entities"
|
||||
)}</ha-section-title
|
||||
>
|
||||
<ha-md-list>${renderedEntites}</ha-md-list>`;
|
||||
}
|
||||
|
||||
return renderedEntites;
|
||||
}
|
||||
|
||||
private _getSelectedTargetId = memoizeOne(
|
||||
(value: SingleHassServiceTarget | undefined) =>
|
||||
value && Object.keys(value).length
|
||||
? `${Object.keys(value)[0].replace("_id", "")}${SEPARATOR}${Object.values(value)[0]}`
|
||||
: undefined
|
||||
);
|
||||
|
||||
private _getTreeData() {
|
||||
this._floorAreas = getAreasNestedInFloors(
|
||||
this.states,
|
||||
this.floors,
|
||||
this.areas,
|
||||
this.devices,
|
||||
this.entities,
|
||||
this._formatId,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
for (const floor of this._floorAreas) {
|
||||
for (const area of floor.areas) {
|
||||
this._loadArea(area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _formatId = memoizeOne((value: AreaFloorValue): string =>
|
||||
[value.type, value.id].join(SEPARATOR)
|
||||
);
|
||||
|
||||
private _handleSelectionChange(ev: WaSelectionChangeEvent) {
|
||||
const treeItem = ev.detail.selection[0] as unknown as
|
||||
| { target?: string }
|
||||
| undefined;
|
||||
|
||||
if (treeItem?.target) {
|
||||
this._valueChanged(treeItem.target);
|
||||
}
|
||||
}
|
||||
|
||||
private _selectItem(ev: CustomEvent) {
|
||||
const target = (ev.currentTarget as any).target;
|
||||
|
||||
if (target) {
|
||||
this._valueChanged(target);
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(itemId: string) {
|
||||
const [type, id] = itemId.split(SEPARATOR, 2);
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { [`${type}_id`]: id || undefined },
|
||||
});
|
||||
}
|
||||
|
||||
private async _loadArea(area: FloorComboBoxItem) {
|
||||
try {
|
||||
const [, id] = area.id.split(SEPARATOR, 2);
|
||||
const targetEntries = await extractFromTarget(this.hass, {
|
||||
area_id: id,
|
||||
});
|
||||
|
||||
const devices: Record<string, DeviceEntries> = {};
|
||||
|
||||
targetEntries.referenced_devices.forEach((device_id) => {
|
||||
devices[device_id] = {
|
||||
open: false,
|
||||
entities: [],
|
||||
};
|
||||
});
|
||||
|
||||
const entities: string[] = [];
|
||||
|
||||
targetEntries.referenced_entities.forEach((entity_id) => {
|
||||
const entity = this.hass.entities[entity_id];
|
||||
if (entity.device_id && devices[entity.device_id]) {
|
||||
devices[entity.device_id].entities.push(entity_id);
|
||||
} else {
|
||||
entities.push(entity_id);
|
||||
}
|
||||
});
|
||||
|
||||
this._areaEntries = {
|
||||
...this._areaEntries,
|
||||
[area.id]: {
|
||||
open: false,
|
||||
devices,
|
||||
entities,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to extract target", e);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandItem(ev) {
|
||||
const targetId = ev.target.target;
|
||||
const [type, id] = targetId.split(SEPARATOR, 2);
|
||||
|
||||
if (type === "area") {
|
||||
this._areaEntries = {
|
||||
...this._areaEntries,
|
||||
[targetId]: {
|
||||
...this._areaEntries[targetId],
|
||||
open: true,
|
||||
},
|
||||
};
|
||||
} else if (type === "device") {
|
||||
const areaEntry = Object.values(this._areaEntries).find((area) =>
|
||||
Object.keys(area.devices).includes(id)
|
||||
);
|
||||
if (areaEntry) {
|
||||
areaEntry.devices[id].open = true;
|
||||
this._areaEntries = {
|
||||
...this._areaEntries,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _collapseItem(ev) {
|
||||
const targetId = ev.target.target;
|
||||
const [type, id] = targetId.split(SEPARATOR, 2);
|
||||
|
||||
if (type === "area") {
|
||||
this._areaEntries = {
|
||||
...this._areaEntries,
|
||||
[targetId]: {
|
||||
...this._areaEntries[targetId],
|
||||
open: false,
|
||||
},
|
||||
};
|
||||
} else if (type === "device") {
|
||||
const areaEntry = Object.values(this._areaEntries).find((area) =>
|
||||
Object.keys(area.devices).includes(id)
|
||||
);
|
||||
if (areaEntry) {
|
||||
areaEntry.devices[id].open = false;
|
||||
this._areaEntries = {
|
||||
...this._areaEntries,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
this._configEntryLookup = Object.fromEntries(
|
||||
configEntries.map((entry) => [entry.entry_id, entry])
|
||||
);
|
||||
}
|
||||
|
||||
public navigateBack() {
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueType = Object.keys(this.value)[0].replace("_id", "");
|
||||
const valueId = this.value[`${valueType}_id`];
|
||||
|
||||
if (valueType === "floor" || valueType === "label") {
|
||||
fireEvent(this, "value-changed", { value: undefined });
|
||||
}
|
||||
|
||||
if (valueType === "area") {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { floor_id: this.areas[valueId].floor_id },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (valueType === "device") {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { area_id: this.devices[valueId].area_id },
|
||||
});
|
||||
}
|
||||
|
||||
if (valueType === "entity") {
|
||||
for (const [areaId, areaEntry] of Object.entries(this._areaEntries)) {
|
||||
const entityDeviceId = this.entities[valueId].device_id;
|
||||
if (entityDeviceId && areaEntry.devices[entityDeviceId]) {
|
||||
// Device is also in area -> go back to device
|
||||
break;
|
||||
}
|
||||
|
||||
if (areaEntry.entities.includes(valueId)) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { area_id: areaId.split(SEPARATOR, 2)[1] },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { device_id: this.entities[valueId].device_id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--wa-color-neutral-fill-quiet: var(--ha-color-fill-primary-normal-active);
|
||||
}
|
||||
|
||||
ha-section-title {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
wa-tree-item::part(item) {
|
||||
height: var(--ha-space-10);
|
||||
padding: var(--ha-space-1) var(--ha-space-3);
|
||||
cursor: pointer;
|
||||
border-inline-start: 0;
|
||||
}
|
||||
wa-tree-item::part(label) {
|
||||
gap: var(--ha-space-3);
|
||||
font-family: var(--ha-font-family-heading);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
overflow: hidden;
|
||||
}
|
||||
.item-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
ha-svg-icon,
|
||||
ha-icon,
|
||||
ha-floor-icon {
|
||||
padding: var(--ha-space-1);
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
|
||||
wa-tree-item::part(item):hover {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-hover);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: var(--ha-space-1);
|
||||
}
|
||||
|
||||
state-badge {
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
wa-tree-item[selected],
|
||||
wa-tree-item[selected] > ha-svg-icon,
|
||||
wa-tree-item[selected] > ha-icon,
|
||||
wa-tree-item[selected] > ha-floor-icon {
|
||||
color: var(--ha-color-on-primary-normal);
|
||||
}
|
||||
|
||||
wa-tree-item[selected]::part(item):hover {
|
||||
background-color: var(--ha-color-fill-primary-normal-hover);
|
||||
}
|
||||
|
||||
wa-tree-item::part(base).tree-item-selected .item {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
padding: 0;
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-1);
|
||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||
--md-list-item-one-line-container-height: var(--ha-space-10);
|
||||
}
|
||||
|
||||
ha-md-list-item.selected {
|
||||
background-color: var(--ha-color-fill-primary-normal-active);
|
||||
--md-list-item-label-text-color: var(--ha-color-on-primary-normal);
|
||||
--icon-primary-color: var(--ha-color-on-primary-normal);
|
||||
}
|
||||
|
||||
ha-md-list-item.selected ha-icon,
|
||||
ha-md-list-item.selected ha-svg-icon {
|
||||
color: var(--ha-color-on-primary-normal);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-add-from-target": HaAutomationAddFromTarget;
|
||||
}
|
||||
}
|
||||
@@ -452,10 +452,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -372,14 +372,16 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
clickable
|
||||
id="backup_id"
|
||||
has-filters
|
||||
.filters=${Object.values(this._filters).filter((filter) =>
|
||||
.filters=${
|
||||
Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter)
|
||||
? filter.length
|
||||
: filter &&
|
||||
Object.values(filter).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
).length
|
||||
}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
@@ -421,7 +423,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
|
||||
<div slot="selection-bar">
|
||||
${!this.narrow
|
||||
${
|
||||
!this.narrow
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -442,7 +445,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
class="warning"
|
||||
@click=${this._deleteSelected}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
<ha-filter-states
|
||||
@@ -455,7 +459,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
expanded
|
||||
.narrow=${this.narrow}
|
||||
></ha-filter-states>
|
||||
${!this._needsOnboarding
|
||||
${
|
||||
!this._needsOnboarding
|
||||
? html`
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
@@ -476,7 +481,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
></ha-svg-icon>`}
|
||||
</ha-fab>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-md-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-md-menu-item .clickAction=${this._downloadBackup}>
|
||||
@@ -488,6 +494,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
${this.hass.localize("ui.common.delete")}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-menu>
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -561,15 +569,15 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
navigate(`/config/backup/details/${id}`);
|
||||
}
|
||||
|
||||
private _downloadBackup = async (ev): Promise<void> => {
|
||||
private async _downloadBackup(ev): Promise<void> {
|
||||
const backup = ev.parentElement.anchorElement.backup;
|
||||
if (!backup) {
|
||||
return;
|
||||
}
|
||||
downloadBackup(this.hass, this, backup, this.config);
|
||||
};
|
||||
}
|
||||
|
||||
private _deleteBackup = async (ev): Promise<void> => {
|
||||
private async _deleteBackup(ev): Promise<void> {
|
||||
const backup = ev.parentElement.anchorElement.backup;
|
||||
if (!backup) {
|
||||
return;
|
||||
@@ -601,7 +609,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "ha-refresh-backup-info");
|
||||
};
|
||||
}
|
||||
|
||||
private async _deleteSelected() {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
|
||||
@@ -182,11 +182,13 @@ class HaBlueprintOverview extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
direction: "asc",
|
||||
},
|
||||
path: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.file_name"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
fullpath: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiPlus, mdiTag } from "@mdi/js";
|
||||
import { mdiTag, mdiPlus } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
@@ -194,9 +194,8 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
"ui.components.category-picker.no_categories"
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.category-picker.no_match"
|
||||
)}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
@@ -255,11 +254,6 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.category-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -771,10 +771,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -26,7 +26,6 @@ import type {
|
||||
EnergySource,
|
||||
FlowFromGridSourceEnergyPreference,
|
||||
FlowToGridSourceEnergyPreference,
|
||||
GridPowerSourceEnergyPreference,
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
@@ -48,7 +47,6 @@ import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import {
|
||||
showEnergySettingsGridFlowFromDialog,
|
||||
showEnergySettingsGridFlowToDialog,
|
||||
showEnergySettingsGridPowerDialog,
|
||||
} from "../dialogs/show-dialogs-energy";
|
||||
import "./ha-energy-validation-result";
|
||||
import { energyCardStyles } from "./styles";
|
||||
@@ -228,58 +226,6 @@ export class EnergyGridSettings extends LitElement {
|
||||
>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
${this.hass.localize("ui.panel.config.energy.grid.grid_power")}
|
||||
</h3>
|
||||
${gridSource.power?.map((power) => {
|
||||
const entityState = this.hass.states[power.stat_rate];
|
||||
return html`
|
||||
<div class="row" .source=${power}>
|
||||
${entityState?.attributes.icon
|
||||
? html`<ha-icon
|
||||
.icon=${entityState.attributes.icon}
|
||||
></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
.path=${mdiTransmissionTower}
|
||||
></ha-svg-icon>`}
|
||||
<span class="content"
|
||||
>${getStatisticLabel(
|
||||
this.hass,
|
||||
power.stat_rate,
|
||||
this.statsMetadata?.[power.stat_rate]
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.edit_power"
|
||||
)}
|
||||
@click=${this._editPowerSource}
|
||||
.path=${mdiPencil}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.delete_power"
|
||||
)}
|
||||
@click=${this._deletePowerSource}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
<div class="row border-bottom">
|
||||
<ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon>
|
||||
<ha-button
|
||||
@click=${this._addPowerSource}
|
||||
appearance="filled"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.add_power"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.grid_carbon_footprint"
|
||||
@@ -553,97 +499,6 @@ export class EnergyGridSettings extends LitElement {
|
||||
await this._savePreferences(cleanedPreferences);
|
||||
}
|
||||
|
||||
private _addPowerSource() {
|
||||
const gridSource = this.preferences.energy_sources.find(
|
||||
(src) => src.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
showEnergySettingsGridPowerDialog(this, {
|
||||
grid_source: gridSource,
|
||||
saveCallback: async (power) => {
|
||||
let preferences: EnergyPreferences;
|
||||
if (!gridSource) {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: [
|
||||
...this.preferences.energy_sources,
|
||||
{
|
||||
...emptyGridSourceEnergyPreference(),
|
||||
power: [power],
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
preferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid"
|
||||
? { ...src, power: [...(gridSource.power || []), power] }
|
||||
: src
|
||||
),
|
||||
};
|
||||
}
|
||||
await this._savePreferences(preferences);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _editPowerSource(ev) {
|
||||
const origSource: GridPowerSourceEnergyPreference =
|
||||
ev.currentTarget.closest(".row").source;
|
||||
const gridSource = this.preferences.energy_sources.find(
|
||||
(src) => src.type === "grid"
|
||||
) as GridSourceTypeEnergyPreference | undefined;
|
||||
showEnergySettingsGridPowerDialog(this, {
|
||||
source: { ...origSource },
|
||||
grid_source: gridSource,
|
||||
saveCallback: async (source) => {
|
||||
const power =
|
||||
energySourcesByType(this.preferences).grid![0].power || [];
|
||||
|
||||
const preferences: EnergyPreferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((src) =>
|
||||
src.type === "grid"
|
||||
? {
|
||||
...src,
|
||||
power: power.map((p) => (p === origSource ? source : p)),
|
||||
}
|
||||
: src
|
||||
),
|
||||
};
|
||||
await this._savePreferences(preferences);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _deletePowerSource(ev) {
|
||||
const sourceToDelete: GridPowerSourceEnergyPreference =
|
||||
ev.currentTarget.closest(".row").source;
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const power =
|
||||
energySourcesByType(this.preferences).grid![0].power?.filter(
|
||||
(p) => p !== sourceToDelete
|
||||
) || [];
|
||||
|
||||
const preferences: EnergyPreferences = {
|
||||
...this.preferences,
|
||||
energy_sources: this.preferences.energy_sources.map((source) =>
|
||||
source.type === "grid" ? { ...source, power } : source
|
||||
),
|
||||
};
|
||||
|
||||
const cleanedPreferences = this._removeEmptySources(preferences);
|
||||
await this._savePreferences(cleanedPreferences);
|
||||
}
|
||||
|
||||
private _removeEmptySources(preferences: EnergyPreferences) {
|
||||
// Check if grid sources became an empty type and remove if so
|
||||
preferences.energy_sources = preferences.energy_sources.reduce<
|
||||
@@ -652,8 +507,7 @@ export class EnergyGridSettings extends LitElement {
|
||||
if (
|
||||
source.type !== "grid" ||
|
||||
source.flow_from.length > 0 ||
|
||||
source.flow_to.length > 0 ||
|
||||
(source.power && source.power.length > 0)
|
||||
source.flow_to.length > 0
|
||||
) {
|
||||
acc.push(source);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
const powerUnitClasses = ["power"];
|
||||
|
||||
@customElement("dialog-energy-battery-settings")
|
||||
export class DialogEnergyBatterySettings
|
||||
@@ -33,14 +32,10 @@ export class DialogEnergyBatterySettings
|
||||
|
||||
@state() private _energy_units?: string[];
|
||||
|
||||
@state() private _power_units?: string[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _excludeList?: string[];
|
||||
|
||||
private _excludeListPower?: string[];
|
||||
|
||||
public async showDialog(
|
||||
params: EnergySettingsBatteryDialogParams
|
||||
): Promise<void> {
|
||||
@@ -51,9 +46,6 @@ export class DialogEnergyBatterySettings
|
||||
this._energy_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||
).units;
|
||||
this._power_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
||||
).units;
|
||||
const allSources: string[] = [];
|
||||
this._params.battery_sources.forEach((entry) => {
|
||||
allSources.push(entry.stat_energy_from);
|
||||
@@ -64,9 +56,6 @@ export class DialogEnergyBatterySettings
|
||||
id !== this._source?.stat_energy_from &&
|
||||
id !== this._source?.stat_energy_to
|
||||
);
|
||||
this._excludeListPower = this._params.battery_sources
|
||||
.map((entry) => entry.stat_rate)
|
||||
.filter((id) => id && id !== this._source?.stat_rate) as string[];
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -83,6 +72,8 @@ export class DialogEnergyBatterySettings
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const pickableUnit = this._energy_units?.join(", ") || "";
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@@ -94,6 +85,12 @@ export class DialogEnergyBatterySettings
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.entity_para",
|
||||
{ unit: pickableUnit }
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
@@ -108,10 +105,6 @@ export class DialogEnergyBatterySettings
|
||||
this._source.stat_energy_from,
|
||||
]}
|
||||
@value-changed=${this._statisticToChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.energy_helper_into",
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
dialogInitialFocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
@@ -128,25 +121,6 @@ export class DialogEnergyBatterySettings
|
||||
this._source.stat_energy_to,
|
||||
]}
|
||||
@value-changed=${this._statisticFromChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.energy_helper_out",
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._source.stat_rate}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.power"
|
||||
)}
|
||||
.excludeStatistics=${this._excludeListPower}
|
||||
@value-changed=${this._powerChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.power_helper",
|
||||
{ unit: this._power_units?.join(", ") || "" }
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-button
|
||||
@@ -176,10 +150,6 @@ export class DialogEnergyBatterySettings
|
||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||
}
|
||||
|
||||
private _powerChanged(ev: CustomEvent<{ value: string }>) {
|
||||
this._source = { ...this._source!, stat_rate: ev.detail.value };
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
await this._params!.saveCallback(this._source!);
|
||||
@@ -198,11 +168,7 @@ export class DialogEnergyBatterySettings
|
||||
--mdc-dialog-max-width: 430px;
|
||||
}
|
||||
ha-statistic-picker {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-statistic-picker:last-of-type {
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -21,7 +21,6 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
const powerUnitClasses = ["power"];
|
||||
|
||||
@customElement("dialog-energy-device-settings")
|
||||
export class DialogEnergyDeviceSettings
|
||||
@@ -36,14 +35,10 @@ export class DialogEnergyDeviceSettings
|
||||
|
||||
@state() private _energy_units?: string[];
|
||||
|
||||
@state() private _power_units?: string[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _excludeList?: string[];
|
||||
|
||||
private _excludeListPower?: string[];
|
||||
|
||||
private _possibleParents: DeviceConsumptionEnergyPreference[] = [];
|
||||
|
||||
public async showDialog(
|
||||
@@ -55,15 +50,9 @@ export class DialogEnergyDeviceSettings
|
||||
this._energy_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||
).units;
|
||||
this._power_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
||||
).units;
|
||||
this._excludeList = this._params.device_consumptions
|
||||
.map((entry) => entry.stat_consumption)
|
||||
.filter((id) => id !== this._device?.stat_consumption);
|
||||
this._excludeListPower = this._params.device_consumptions
|
||||
.map((entry) => entry.stat_rate)
|
||||
.filter((id) => id && id !== this._device?.stat_rate) as string[];
|
||||
}
|
||||
|
||||
private _computePossibleParents() {
|
||||
@@ -104,6 +93,8 @@ export class DialogEnergyDeviceSettings
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const pickableUnit = this._energy_units?.join(", ") || "";
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@@ -117,6 +108,12 @@ export class DialogEnergyDeviceSettings
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
|
||||
{ unit: pickableUnit }
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
@@ -128,28 +125,9 @@ export class DialogEnergyDeviceSettings
|
||||
)}
|
||||
.excludeStatistics=${this._excludeList}
|
||||
@value-changed=${this._statisticChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
dialogInitialFocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._device?.stat_rate}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.device_consumption_power"
|
||||
)}
|
||||
.excludeStatistics=${this._excludeListPower}
|
||||
@value-changed=${this._powerStatisticChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
|
||||
{ unit: this._power_units?.join(", ") || "" }
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.display_name"
|
||||
@@ -232,20 +210,6 @@ export class DialogEnergyDeviceSettings
|
||||
this._computePossibleParents();
|
||||
}
|
||||
|
||||
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
|
||||
if (!this._device) {
|
||||
return;
|
||||
}
|
||||
const newDevice = {
|
||||
...this._device,
|
||||
stat_rate: ev.detail.value,
|
||||
} as DeviceConsumptionEnergyPreference;
|
||||
if (!newDevice.stat_rate) {
|
||||
delete newDevice.stat_rate;
|
||||
}
|
||||
this._device = newDevice;
|
||||
}
|
||||
|
||||
private _nameChanged(ev) {
|
||||
const newDevice = {
|
||||
...this._device!,
|
||||
@@ -281,19 +245,15 @@ export class DialogEnergyDeviceSettings
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-statistic-picker {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-statistic-picker {
|
||||
width: 100%;
|
||||
}
|
||||
ha-select {
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-textfield {
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -104,6 +104,8 @@ export class DialogEnergyGridFlowSettings
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const pickableUnit = this._energy_units?.join(", ") || "";
|
||||
|
||||
const unitPriceFixed = `${this.hass.config.currency}/kWh`;
|
||||
|
||||
const externalSource =
|
||||
@@ -133,11 +135,19 @@ export class DialogEnergyGridFlowSettings
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
<div>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph`
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`,
|
||||
{ unit: pickableUnit }
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
@@ -153,10 +163,6 @@ export class DialogEnergyGridFlowSettings
|
||||
)}
|
||||
.excludeStatistics=${this._excludeList}
|
||||
@value-changed=${this._statisticChanged}
|
||||
.helper=${this.hass.localize(
|
||||
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`,
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
dialogInitialFocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
@@ -355,10 +361,6 @@ export class DialogEnergyGridFlowSettings
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 430px;
|
||||
}
|
||||
ha-statistic-picker {
|
||||
display: block;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
import { mdiTransmissionTower } from "@mdi/js";
|
||||
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/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-button";
|
||||
import type { GridPowerSourceEnergyPreference } from "../../../../data/energy";
|
||||
import { energyStatisticHelpUrl } from "../../../../data/energy";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { EnergySettingsGridPowerDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const powerUnitClasses = ["power"];
|
||||
|
||||
@customElement("dialog-energy-grid-power-settings")
|
||||
export class DialogEnergyGridPowerSettings
|
||||
extends LitElement
|
||||
implements HassDialog<EnergySettingsGridPowerDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: EnergySettingsGridPowerDialogParams;
|
||||
|
||||
@state() private _source?: GridPowerSourceEnergyPreference;
|
||||
|
||||
@state() private _power_units?: string[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _excludeListPower?: string[];
|
||||
|
||||
public async showDialog(
|
||||
params: EnergySettingsGridPowerDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._source = params.source ? { ...params.source } : { stat_rate: "" };
|
||||
|
||||
const initialSourceIdPower = this._source.stat_rate;
|
||||
|
||||
this._power_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
||||
).units;
|
||||
|
||||
this._excludeListPower = [
|
||||
...(this._params.grid_source?.power?.map((entry) => entry.stat_rate) ||
|
||||
[]),
|
||||
].filter((id) => id && id !== initialSourceIdPower) as string[];
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
this._source = undefined;
|
||||
this._error = undefined;
|
||||
this._excludeListPower = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._source) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
.heading=${html`<ha-svg-icon
|
||||
.path=${mdiTransmissionTower}
|
||||
style="--mdc-icon-size: 32px;"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.header"
|
||||
)}`}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._source.stat_rate}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.power_stat"
|
||||
)}
|
||||
.excludeStatistics=${this._excludeListPower}
|
||||
@value-changed=${this._powerStatisticChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.power_dialog.power_helper",
|
||||
{ unit: this._power_units?.join(", ") || "" }
|
||||
)}
|
||||
dialogInitialFocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._source.stat_rate}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
stat_rate: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
await this._params!.saveCallback(this._source!);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 430px;
|
||||
}
|
||||
ha-statistic-picker {
|
||||
display: block;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-energy-grid-power-settings": DialogEnergyGridPowerSettings;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import { brandsUrl } from "../../../../util/brands-url";
|
||||
import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
const powerUnitClasses = ["power"];
|
||||
|
||||
@customElement("dialog-energy-solar-settings")
|
||||
export class DialogEnergySolarSettings
|
||||
@@ -47,14 +46,10 @@ export class DialogEnergySolarSettings
|
||||
|
||||
@state() private _energy_units?: string[];
|
||||
|
||||
@state() private _power_units?: string[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _excludeList?: string[];
|
||||
|
||||
private _excludeListPower?: string[];
|
||||
|
||||
public async showDialog(
|
||||
params: EnergySettingsSolarDialogParams
|
||||
): Promise<void> {
|
||||
@@ -67,15 +62,9 @@ export class DialogEnergySolarSettings
|
||||
this._energy_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||
).units;
|
||||
this._power_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
|
||||
).units;
|
||||
this._excludeList = this._params.solar_sources
|
||||
.map((entry) => entry.stat_energy_from)
|
||||
.filter((id) => id !== this._source?.stat_energy_from);
|
||||
this._excludeListPower = this._params.solar_sources
|
||||
.map((entry) => entry.stat_rate)
|
||||
.filter((id) => id && id !== this._source?.stat_rate) as string[];
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -92,6 +81,8 @@ export class DialogEnergySolarSettings
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const pickableUnit = this._energy_units?.join(", ") || "";
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@@ -103,6 +94,12 @@ export class DialogEnergySolarSettings
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.entity_para",
|
||||
{ unit: pickableUnit }
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
@@ -114,28 +111,9 @@ export class DialogEnergySolarSettings
|
||||
)}
|
||||
.excludeStatistics=${this._excludeList}
|
||||
@value-changed=${this._statisticChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.entity_para",
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
dialogInitialFocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeUnitClass=${powerUnitClasses}
|
||||
.value=${this._source.stat_rate}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.solar_production_power"
|
||||
)}
|
||||
.excludeStatistics=${this._excludeListPower}
|
||||
@value-changed=${this._powerStatisticChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.entity_para",
|
||||
{ unit: this._power_units?.join(", ") || "" }
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.solar_production_forecast"
|
||||
@@ -289,10 +267,6 @@ export class DialogEnergySolarSettings
|
||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||
}
|
||||
|
||||
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
|
||||
this._source = { ...this._source!, stat_rate: ev.detail.value };
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
if (!this._forecast) {
|
||||
@@ -313,10 +287,6 @@ export class DialogEnergySolarSettings
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 430px;
|
||||
}
|
||||
ha-statistic-picker {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
img {
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
FlowFromGridSourceEnergyPreference,
|
||||
FlowToGridSourceEnergyPreference,
|
||||
GasSourceTypeEnergyPreference,
|
||||
GridPowerSourceEnergyPreference,
|
||||
GridSourceTypeEnergyPreference,
|
||||
SolarSourceTypeEnergyPreference,
|
||||
WaterSourceTypeEnergyPreference,
|
||||
@@ -42,12 +41,6 @@ export interface EnergySettingsGridFlowToDialogParams {
|
||||
saveCallback: (source: FlowToGridSourceEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface EnergySettingsGridPowerDialogParams {
|
||||
source?: GridPowerSourceEnergyPreference;
|
||||
grid_source?: GridSourceTypeEnergyPreference;
|
||||
saveCallback: (source: GridPowerSourceEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface EnergySettingsSolarDialogParams {
|
||||
info: EnergyInfo;
|
||||
source?: SolarSourceTypeEnergyPreference;
|
||||
@@ -159,14 +152,3 @@ export const showEnergySettingsGridFlowToDialog = (
|
||||
dialogParams: { ...dialogParams, direction: "to" },
|
||||
});
|
||||
};
|
||||
|
||||
export const showEnergySettingsGridPowerDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: EnergySettingsGridPowerDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-energy-grid-power-settings",
|
||||
dialogImport: () => import("./dialog-energy-grid-power-settings"),
|
||||
dialogParams: dialogParams,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -792,10 +792,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -634,10 +634,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { relativeTime } from "../../../../../common/datetime/relative_time";
|
||||
import { getDeviceContext } from "../../../../../common/entity/context/get_device_context";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { throttle } from "../../../../../common/util/throttle";
|
||||
import "../../../../../components/chart/ha-network-graph";
|
||||
@@ -195,17 +194,11 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
];
|
||||
const links: NetworkLink[] = [];
|
||||
Object.values(scanners).forEach((scanner) => {
|
||||
const scannerDevice = this._sourceDevices[scanner.source] as
|
||||
| DeviceRegistryEntry
|
||||
| undefined;
|
||||
const area = scannerDevice
|
||||
? getDeviceContext(scannerDevice, this.hass).area
|
||||
: undefined;
|
||||
const scannerDevice = this._sourceDevices[scanner.source];
|
||||
nodes.push({
|
||||
id: scanner.source,
|
||||
name:
|
||||
scannerDevice?.name_by_user || scannerDevice?.name || scanner.name,
|
||||
context: area?.name,
|
||||
category: 1,
|
||||
value: 5,
|
||||
symbol: "circle",
|
||||
@@ -238,16 +231,10 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const device = this._sourceDevices[node.address] as
|
||||
| DeviceRegistryEntry
|
||||
| undefined;
|
||||
const area = device
|
||||
? getDeviceContext(device, this.hass).area
|
||||
: undefined;
|
||||
const device = this._sourceDevices[node.address];
|
||||
nodes.push({
|
||||
id: node.address,
|
||||
name: this._getBluetoothDeviceName(node.address),
|
||||
context: area?.name,
|
||||
value: device ? 1 : 0,
|
||||
category: device ? 2 : 3,
|
||||
symbolSize: 20,
|
||||
@@ -307,24 +294,19 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
const btDevice = this._data.find((d) => d.address === address);
|
||||
if (btDevice) {
|
||||
tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}<br><b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${btDevice.rssi}<br><b>${this.hass.localize("ui.panel.config.bluetooth.source")}:</b> ${btDevice.source}<br><b>${this.hass.localize("ui.panel.config.bluetooth.updated")}:</b> ${relativeTime(new Date(btDevice.time * 1000), this.hass.locale)}`;
|
||||
const device = this._sourceDevices[address];
|
||||
if (device) {
|
||||
const area = getDeviceContext(device, this.hass).area;
|
||||
if (area) {
|
||||
tooltipText += `<br><b>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b>${area.name}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const device = this._sourceDevices[address];
|
||||
if (device) {
|
||||
tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}`;
|
||||
const area = getDeviceContext(device, this.hass).area;
|
||||
if (device.area_id) {
|
||||
const area = this.hass.areas[device.area_id];
|
||||
if (area) {
|
||||
tooltipText += `<br><b>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b>${area.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tooltipText;
|
||||
};
|
||||
|
||||
|
||||
@@ -426,10 +426,6 @@ class DialogZHAReconfigureDevice extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr 2fr;
|
||||
|
||||
@@ -19,8 +19,6 @@ import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { formatAsPaddedHex } from "./functions";
|
||||
import { zhaTabs } from "./zha-config-dashboard";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device_registry";
|
||||
import { getDeviceContext } from "../../../../../common/entity/context/get_device_context";
|
||||
|
||||
@customElement("zha-network-visualization-page")
|
||||
export class ZHANetworkVisualizationPage extends LitElement {
|
||||
@@ -119,11 +117,8 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
} else {
|
||||
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.device_not_in_db")}</b>`;
|
||||
}
|
||||
const haDevice = this.hass.devices[device.device_reg_id] as
|
||||
| DeviceRegistryEntry
|
||||
| undefined;
|
||||
if (haDevice) {
|
||||
const area = getDeviceContext(haDevice, this.hass).area;
|
||||
if (device.area_id) {
|
||||
const area = this.hass.areas[device.area_id];
|
||||
if (area) {
|
||||
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.area")}: </b>${area.name}`;
|
||||
}
|
||||
@@ -209,17 +204,10 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
category = 2; // End Device
|
||||
}
|
||||
|
||||
const haDevice = this.hass.devices[device.device_reg_id] as
|
||||
| DeviceRegistryEntry
|
||||
| undefined;
|
||||
const area = haDevice
|
||||
? getDeviceContext(haDevice, this.hass).area
|
||||
: undefined;
|
||||
// Create node
|
||||
nodes.push({
|
||||
id: device.ieee,
|
||||
name: device.user_given_name || device.name || device.ieee,
|
||||
context: area?.name,
|
||||
category,
|
||||
value: isCoordinator ? 3 : device.device_type === "Router" ? 2 : 1,
|
||||
symbolSize: isCoordinator
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getDeviceContext } from "../../../../../common/entity/context/get_device_context";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { debounce } from "../../../../../common/util/debounce";
|
||||
import "../../../../../components/chart/ha-network-graph";
|
||||
@@ -125,7 +124,7 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
return tip;
|
||||
}
|
||||
const { id, name } = data as any;
|
||||
const device = this._devices[id] as DeviceRegistryEntry | undefined;
|
||||
const device = this._devices[id];
|
||||
const nodeStatus = this._nodeStatuses[id];
|
||||
let tip = `${(params as any).marker} ${name}`;
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.node_id")}:</b> ${id}`;
|
||||
@@ -139,12 +138,6 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
tip += `<br><b>Z-Wave Plus:</b> ${this.hass.localize("ui.panel.config.zwave_js.visualization.version")} ${nodeStatus.zwave_plus_version}`;
|
||||
}
|
||||
}
|
||||
if (device) {
|
||||
const area = getDeviceContext(device, this.hass).area;
|
||||
if (area) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.area")}:</b> ${area.name}`;
|
||||
}
|
||||
}
|
||||
return tip;
|
||||
};
|
||||
|
||||
@@ -204,16 +197,10 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
if (node.is_controller_node) {
|
||||
controllerNode = node.node_id;
|
||||
}
|
||||
const device = this._devices[node.node_id] as
|
||||
| DeviceRegistryEntry
|
||||
| undefined;
|
||||
const area = device
|
||||
? getDeviceContext(device, this.hass).area
|
||||
: undefined;
|
||||
const device = this._devices[node.node_id];
|
||||
nodes.push({
|
||||
id: String(node.node_id),
|
||||
name: device?.name_by_user ?? device?.name ?? String(node.node_id),
|
||||
context: area?.name,
|
||||
value: node.is_controller_node ? 3 : node.is_routing ? 2 : 1,
|
||||
category:
|
||||
node.status === NodeStatus.Dead
|
||||
|
||||
@@ -320,9 +320,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
if (this.hass.panels.light) {
|
||||
result.push({
|
||||
icon: this.hass.panels.light.icon || "mdi:lamps",
|
||||
icon: "mdi:lamps",
|
||||
title: this.hass.localize("panel.light"),
|
||||
show_in_sidebar: true,
|
||||
show_in_sidebar: false,
|
||||
mode: "storage",
|
||||
url_path: "light",
|
||||
filename: "",
|
||||
@@ -334,9 +334,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
if (this.hass.panels.security) {
|
||||
result.push({
|
||||
icon: this.hass.panels.security.icon || "mdi:security",
|
||||
icon: "mdi:security",
|
||||
title: this.hass.localize("panel.security"),
|
||||
show_in_sidebar: true,
|
||||
show_in_sidebar: false,
|
||||
mode: "storage",
|
||||
url_path: "security",
|
||||
filename: "",
|
||||
@@ -348,9 +348,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
if (this.hass.panels.climate) {
|
||||
result.push({
|
||||
icon: this.hass.panels.climate.icon || "mdi:home-thermometer",
|
||||
icon: "mdi:home-thermometer",
|
||||
title: this.hass.localize("panel.climate"),
|
||||
show_in_sidebar: true,
|
||||
show_in_sidebar: false,
|
||||
mode: "storage",
|
||||
url_path: "climate",
|
||||
filename: "",
|
||||
@@ -360,20 +360,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.home) {
|
||||
result.push({
|
||||
icon: this.hass.panels.home.icon || "mdi:home",
|
||||
title: this.hass.localize("panel.home"),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "home",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
|
||||
result.push(
|
||||
...dashboards
|
||||
.sort((a, b) =>
|
||||
@@ -484,18 +470,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
}
|
||||
|
||||
private _canDelete(urlPath: string) {
|
||||
return ![
|
||||
"lovelace",
|
||||
"energy",
|
||||
"light",
|
||||
"security",
|
||||
"climate",
|
||||
"home",
|
||||
].includes(urlPath);
|
||||
return !["lovelace", "energy", "light", "security", "climate"].includes(
|
||||
urlPath
|
||||
);
|
||||
}
|
||||
|
||||
private _canEdit(urlPath: string) {
|
||||
return !["light", "security", "climate", "home"].includes(urlPath);
|
||||
return !["light", "security", "climate"].includes(urlPath);
|
||||
}
|
||||
|
||||
private _handleDelete = async (item: DataTableItem) => {
|
||||
|
||||
@@ -41,6 +41,7 @@ class DialogRepairsIssueSubtitle extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
font-size: var(--ha-font-size-m);
|
||||
margin-bottom: 8px;
|
||||
color: var(--secondary-text-color);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import { mdiClose, mdiOpenInNew } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../../../common/dom/is-navigation-click";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../components/ha-md-dialog";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-dialog-header";
|
||||
import "./dialog-repairs-issue-subtitle";
|
||||
import "../../../components/ha-markdown";
|
||||
import type { RepairsIssue } from "../../../data/repairs";
|
||||
@@ -25,12 +26,11 @@ class DialogRepairsIssue extends LitElement {
|
||||
|
||||
@state() private _params?: RepairsIssueDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public showDialog(params: RepairsIssueDialogParams): void {
|
||||
this._params = params;
|
||||
this._issue = this._params.issue;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
@@ -44,7 +44,7 @@ class DialogRepairsIssue extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -62,19 +62,32 @@ class DialogRepairsIssue extends LitElement {
|
||||
) || this.hass!.localize("ui.panel.config.repairs.dialog.title");
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${dialogTitle}
|
||||
aria-describedby="dialog-repairs-issue-description"
|
||||
<ha-md-dialog
|
||||
open
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-repairs-issue-title"
|
||||
aria-describedby="dialog-repairs-issue-description"
|
||||
>
|
||||
<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"
|
||||
id="dialog-repairs-issue-title"
|
||||
.title=${dialogTitle}
|
||||
>${dialogTitle}</span
|
||||
>
|
||||
<dialog-repairs-issue-subtitle
|
||||
slot="headerSubtitle"
|
||||
slot="subtitle"
|
||||
.hass=${this.hass}
|
||||
.issue=${this._issue}
|
||||
></dialog-repairs-issue-subtitle>
|
||||
<div class="dialog-content">
|
||||
</ha-dialog-header>
|
||||
<div slot="content" class="dialog-content">
|
||||
${this._issue.breaks_in_ha_version
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
@@ -109,12 +122,8 @@ class DialogRepairsIssue extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._ignoreIssue}
|
||||
>
|
||||
<div slot="actions">
|
||||
<ha-button appearance="plain" @click=${this._ignoreIssue}>
|
||||
${this._issue!.ignored
|
||||
? this.hass!.localize("ui.panel.config.repairs.dialog.unignore")
|
||||
: this.hass!.localize("ui.panel.config.repairs.dialog.ignore")}
|
||||
@@ -122,7 +131,6 @@ class DialogRepairsIssue extends LitElement {
|
||||
${this._issue.learn_more_url
|
||||
? html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
appearance="filled"
|
||||
rel="noopener noreferrer"
|
||||
href=${learnMoreUrlIsHomeAssistant
|
||||
@@ -141,8 +149,8 @@ class DialogRepairsIssue extends LitElement {
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -164,7 +172,7 @@ class DialogRepairsIssue extends LitElement {
|
||||
padding-top: 0;
|
||||
}
|
||||
ha-alert {
|
||||
margin-bottom: var(--ha-space-4);
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
.dismissed {
|
||||
|
||||
@@ -473,10 +473,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -65,15 +65,13 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
private _selectorRowElement?: HaAutomationRow;
|
||||
|
||||
protected render() {
|
||||
const hasSelector =
|
||||
this.field.selector && typeof this.field.selector === "object";
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-automation-row
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._toggleSidebar}
|
||||
.selected=${this._selected}
|
||||
.leftChevron=${hasSelector}
|
||||
left-chevron
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
.collapsed=${this._collapsed}
|
||||
.highlight=${this.highlight}
|
||||
@@ -142,8 +140,6 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
</ha-automation-row>
|
||||
</ha-card>
|
||||
${hasSelector
|
||||
? html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"selector-row": true,
|
||||
@@ -207,10 +203,7 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
class="warning"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete"
|
||||
@@ -258,8 +251,6 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -458,10 +458,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -371,11 +371,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
const actionType = getActionType(config);
|
||||
if (
|
||||
!["sequence", "unknown"].includes(actionType) ||
|
||||
(actionType === "sequence" && "metadata" in config)
|
||||
) {
|
||||
if (!["sequence", "unknown"].includes(getActionType(config))) {
|
||||
config = { sequence: [config] };
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export class HaConfigUsers extends LitElement {
|
||||
title: localize("ui.panel.config.users.picker.headers.username"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
template: (user) => html`${user.username || "—"}`,
|
||||
},
|
||||
group: {
|
||||
@@ -97,6 +98,7 @@ export class HaConfigUsers extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
direction: "asc",
|
||||
},
|
||||
is_active: {
|
||||
title: this.hass.localize(
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../../types";
|
||||
import "../lovelace/hui-root";
|
||||
import { generateLovelaceDashboardStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import { showAlertDialog } from "../lovelace/custom-card-helpers";
|
||||
|
||||
const HOME_LOVELACE_CONFIG: LovelaceDashboardStrategyConfig = {
|
||||
strategy: {
|
||||
type: "home",
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("ha-panel-home")
|
||||
class PanelHome extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route?: Route;
|
||||
|
||||
@property({ attribute: false }) public panel?: PanelInfo;
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && this.hass) {
|
||||
// If the entity registry changed, ask the user if they want to refresh the config
|
||||
if (
|
||||
oldHass.entities !== this.hass.entities ||
|
||||
oldHass.devices !== this.hass.devices ||
|
||||
oldHass.areas !== this.hass.areas ||
|
||||
oldHass.floors !== this.hass.floors
|
||||
) {
|
||||
if (this.hass.config.state === "RUNNING") {
|
||||
this._debounceRegistriesChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If ha started, refresh the config
|
||||
if (
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
oldHass.config.state !== "RUNNING"
|
||||
) {
|
||||
this._setLovelace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
);
|
||||
|
||||
private _registriesChanged = async () => {
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this._lovelace) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hui-root
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.route=${this.route}
|
||||
.panel=${this.panel}
|
||||
></hui-root>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLovelace() {
|
||||
const config = await generateLovelaceDashboardStrategy(
|
||||
HOME_LOVELACE_CONFIG,
|
||||
this.hass
|
||||
);
|
||||
|
||||
if (deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: config,
|
||||
editMode: false,
|
||||
urlPath: "home",
|
||||
mode: "generated",
|
||||
locale: this.hass.locale,
|
||||
enableFullEditMode: () => undefined,
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: this._setEditMode,
|
||||
showToast: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private _setEditMode = () => {
|
||||
// For now, we just show an alert that edit mode is not supported.
|
||||
// This will be expanded in the future.
|
||||
showAlertDialog(this, {
|
||||
title: "Edit mode not available",
|
||||
text: "The Home panel does not support edit mode.",
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
});
|
||||
};
|
||||
|
||||
static readonly styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-panel-home": PanelHome;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||
import {
|
||||
ConditionalListenerMixin,
|
||||
setupMediaQueryListeners,
|
||||
} from "../../../mixins/conditional-listener-mixin";
|
||||
import { checkConditionsMet } from "../common/validate-condition";
|
||||
import { createBadgeElement } from "../create-element/create-badge-element";
|
||||
import { createErrorBadgeConfig } from "../create-element/create-element-base";
|
||||
@@ -19,9 +22,7 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement("hui-badge")
|
||||
export class HuiBadge extends ConditionalListenerMixin<LovelaceBadgeConfig>(
|
||||
ReactiveElement
|
||||
) {
|
||||
export class HuiBadge extends ConditionalListenerMixin(ReactiveElement) {
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceBadgeConfig;
|
||||
@@ -52,7 +53,7 @@ export class HuiBadge extends ConditionalListenerMixin<LovelaceBadgeConfig>(
|
||||
this._updateVisibility();
|
||||
}
|
||||
|
||||
protected _updateElement(config: LovelaceBadgeConfig) {
|
||||
private _updateElement(config: LovelaceBadgeConfig) {
|
||||
if (!this._element) {
|
||||
return;
|
||||
}
|
||||
@@ -132,7 +133,22 @@ export class HuiBadge extends ConditionalListenerMixin<LovelaceBadgeConfig>(
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateVisibility(conditionsMet?: boolean) {
|
||||
protected setupConditionalListeners() {
|
||||
if (!this.config?.visibility || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupMediaQueryListeners(
|
||||
this.config.visibility,
|
||||
this.hass,
|
||||
(unsub) => this.addConditionalListener(unsub),
|
||||
(conditionsMet) => {
|
||||
this._updateVisibility(conditionsMet);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _updateVisibility(ignoreConditions?: boolean) {
|
||||
if (!this._element || !this.hass) {
|
||||
return;
|
||||
}
|
||||
@@ -153,9 +169,9 @@ export class HuiBadge extends ConditionalListenerMixin<LovelaceBadgeConfig>(
|
||||
}
|
||||
|
||||
const visible =
|
||||
conditionsMet ??
|
||||
(!this.config?.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass));
|
||||
ignoreConditions ||
|
||||
!this.config?.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass);
|
||||
this._setElementVisibility(visible);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import memoizeOne from "memoize-one";
|
||||
import type { BarSeriesOption, PieSeriesOption } from "echarts/charts";
|
||||
import { PieChart } from "echarts/charts";
|
||||
import type { ECElementEvent } from "echarts/types/dist/shared";
|
||||
import type { PieDataItemOption } from "echarts/types/src/chart/pie/PieSeries";
|
||||
import { filterXSS } from "../../../../common/util/xss";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
@@ -388,7 +387,6 @@ export class HuiEnergyDevicesGraphCard
|
||||
});
|
||||
|
||||
if (this._chartType === "pie") {
|
||||
const pieChartData = chartData as NonNullable<PieSeriesOption["data"]>;
|
||||
const { summedData, compareSummedData } = getSummedData(energyData);
|
||||
const { consumption, compareConsumption } = computeConsumptionData(
|
||||
summedData,
|
||||
@@ -401,10 +399,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
"from_battery" in summedData;
|
||||
const untracked = showUntracked
|
||||
? totalUsed -
|
||||
pieChartData.reduce(
|
||||
(acc: number, d) => acc + (d as PieDataItemOption).value![0],
|
||||
0
|
||||
)
|
||||
chartData.reduce((acc: number, d: any) => acc + d.value[0], 0)
|
||||
: 0;
|
||||
if (untracked > 0) {
|
||||
const color = getEnergyColor(
|
||||
@@ -414,7 +409,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
false,
|
||||
"--history-unknown-color"
|
||||
);
|
||||
pieChartData.push({
|
||||
chartData.push({
|
||||
id: "untracked",
|
||||
value: [untracked, "untracked"] as any,
|
||||
name: this.hass.localize(
|
||||
@@ -447,20 +442,13 @@ export class HuiEnergyDevicesGraphCard
|
||||
}
|
||||
}
|
||||
}
|
||||
const totalChart = pieChartData.reduce(
|
||||
(acc: number, d) =>
|
||||
this._hiddenStats.includes((d as PieDataItemOption).id as string)
|
||||
? acc
|
||||
: acc + (d as PieDataItemOption).value![0],
|
||||
0
|
||||
);
|
||||
datasets.push({
|
||||
type: "pie",
|
||||
radius: ["0%", compareData ? "30%" : "40%"],
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_graph.total_energy_usage"
|
||||
),
|
||||
data: [totalChart],
|
||||
data: [totalUsed],
|
||||
label: {
|
||||
show: true,
|
||||
position: "center",
|
||||
@@ -468,7 +456,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
fontSize: computedStyle.getPropertyValue("--ha-font-size-l"),
|
||||
lineHeight: 24,
|
||||
fontWeight: "bold",
|
||||
formatter: `{a}\n${formatNumber(totalChart, this.hass.locale)} kWh`,
|
||||
formatter: `{a}\n${formatNumber(totalUsed, this.hass.locale)} kWh`,
|
||||
},
|
||||
cursor: "default",
|
||||
itemStyle: {
|
||||
|
||||
@@ -5,7 +5,10 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||
import {
|
||||
ConditionalListenerMixin,
|
||||
setupMediaQueryListeners,
|
||||
} from "../../../mixins/conditional-listener-mixin";
|
||||
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { checkConditionsMet } from "../common/validate-condition";
|
||||
@@ -21,9 +24,7 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement("hui-card")
|
||||
export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
||||
ReactiveElement
|
||||
) {
|
||||
export class HuiCard extends ConditionalListenerMixin(ReactiveElement) {
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceCardConfig;
|
||||
@@ -120,7 +121,7 @@ export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
||||
return {};
|
||||
}
|
||||
|
||||
protected _updateElement(config: LovelaceCardConfig) {
|
||||
private _updateElement(config: LovelaceCardConfig) {
|
||||
if (!this._element) {
|
||||
return;
|
||||
}
|
||||
@@ -246,7 +247,22 @@ export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateVisibility(conditionsMet?: boolean) {
|
||||
protected setupConditionalListeners() {
|
||||
if (!this.config?.visibility || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupMediaQueryListeners(
|
||||
this.config.visibility,
|
||||
this.hass,
|
||||
(unsub) => this.addConditionalListener(unsub),
|
||||
(conditionsMet) => {
|
||||
this._updateVisibility(conditionsMet);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _updateVisibility(ignoreConditions?: boolean) {
|
||||
if (!this._element || !this.hass) {
|
||||
return;
|
||||
}
|
||||
@@ -267,9 +283,9 @@ export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
||||
}
|
||||
|
||||
const visible =
|
||||
conditionsMet ??
|
||||
(!this.config?.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass));
|
||||
ignoreConditions ||
|
||||
!this.config?.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass);
|
||||
this._setElementVisibility(visible);
|
||||
}
|
||||
|
||||
|
||||
@@ -175,9 +175,9 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
this._names = {};
|
||||
this._entities.forEach((config) => {
|
||||
const stateObj = this.hass!.states[config.entity];
|
||||
this._names[config.entity] =
|
||||
computeLovelaceEntityName(this.hass!, stateObj, config.name) ||
|
||||
config.entity;
|
||||
this._names[config.entity] = stateObj
|
||||
? computeLovelaceEntityName(this.hass!, stateObj, config.name)
|
||||
: config.entity;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,7 @@ export const coordinates = (
|
||||
width: number,
|
||||
height: number,
|
||||
maxDetails: number,
|
||||
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number },
|
||||
useMean = false
|
||||
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }
|
||||
) => {
|
||||
history = history.filter((item) => !Number.isNaN(item[1]));
|
||||
|
||||
@@ -59,8 +58,7 @@ export const coordinates = (
|
||||
history,
|
||||
maxDetails,
|
||||
limits?.minX,
|
||||
limits?.maxX,
|
||||
useMean
|
||||
limits?.maxX
|
||||
);
|
||||
return calcPoints(sampledData, width, height, limits);
|
||||
};
|
||||
@@ -70,8 +68,7 @@ export const coordinatesMinimalResponseCompressedState = (
|
||||
width: number,
|
||||
height: number,
|
||||
maxDetails: number,
|
||||
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number },
|
||||
useMean = false
|
||||
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }
|
||||
) => {
|
||||
if (!history?.length) {
|
||||
return { points: [], yAxisOrigin: 0 };
|
||||
@@ -83,5 +80,5 @@ export const coordinatesMinimalResponseCompressedState = (
|
||||
item.lu * 1000,
|
||||
Number(item.s),
|
||||
]);
|
||||
return coordinates(mappedHistory, width, height, maxDetails, limits, useMean);
|
||||
return coordinates(mappedHistory, width, height, maxDetails, limits);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiAmpersand,
|
||||
mdiCalendarClock,
|
||||
mdiGateOr,
|
||||
mdiMapMarker,
|
||||
mdiNotEqualVariant,
|
||||
@@ -16,7 +15,6 @@ export const ICON_CONDITION: Record<Condition["condition"], string> = {
|
||||
numeric_state: mdiNumeric,
|
||||
state: mdiStateMachine,
|
||||
screen: mdiResponsive,
|
||||
time: mdiCalendarClock,
|
||||
user: mdiAccount,
|
||||
and: mdiAmpersand,
|
||||
not: mdiNotEqualVariant,
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { UNKNOWN } from "../../../data/entity";
|
||||
import { getUserPerson } from "../../../data/person";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import {
|
||||
checkTimeInRange,
|
||||
isValidTimeString,
|
||||
} from "../../../common/datetime/check_time";
|
||||
import {
|
||||
WEEKDAYS_SHORT,
|
||||
type WeekdayShort,
|
||||
} from "../../../common/datetime/weekday";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
export type Condition =
|
||||
| LocationCondition
|
||||
| NumericStateCondition
|
||||
| StateCondition
|
||||
| ScreenCondition
|
||||
| TimeCondition
|
||||
| UserCondition
|
||||
| OrCondition
|
||||
| AndCondition
|
||||
@@ -58,13 +50,6 @@ export interface ScreenCondition extends BaseCondition {
|
||||
media_query?: string;
|
||||
}
|
||||
|
||||
export interface TimeCondition extends BaseCondition {
|
||||
condition: "time";
|
||||
after?: string;
|
||||
before?: string;
|
||||
weekdays?: WeekdayShort[];
|
||||
}
|
||||
|
||||
export interface UserCondition extends BaseCondition {
|
||||
condition: "user";
|
||||
users?: string[];
|
||||
@@ -165,13 +150,6 @@ function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) {
|
||||
: false;
|
||||
}
|
||||
|
||||
function checkTimeCondition(
|
||||
condition: Omit<TimeCondition, "condition">,
|
||||
hass: HomeAssistant
|
||||
) {
|
||||
return checkTimeInRange(hass, condition);
|
||||
}
|
||||
|
||||
function checkLocationCondition(
|
||||
condition: LocationCondition,
|
||||
hass: HomeAssistant
|
||||
@@ -217,8 +195,6 @@ export function checkConditionsMet(
|
||||
return conditions.every((c) => {
|
||||
if ("condition" in c) {
|
||||
switch (c.condition) {
|
||||
case "time":
|
||||
return checkTimeCondition(c, hass);
|
||||
case "screen":
|
||||
return checkScreenCondition(c, hass);
|
||||
case "user":
|
||||
@@ -295,35 +271,6 @@ function validateScreenCondition(condition: ScreenCondition) {
|
||||
return condition.media_query != null;
|
||||
}
|
||||
|
||||
function validateTimeCondition(condition: TimeCondition) {
|
||||
// Check if time strings are present and non-empty
|
||||
const hasAfter = condition.after != null && condition.after !== "";
|
||||
const hasBefore = condition.before != null && condition.before !== "";
|
||||
const hasTime = hasAfter || hasBefore;
|
||||
|
||||
const hasWeekdays =
|
||||
condition.weekdays != null && condition.weekdays.length > 0;
|
||||
const weekdaysValid =
|
||||
!hasWeekdays ||
|
||||
condition.weekdays!.every((w: WeekdayShort) => WEEKDAYS_SHORT.includes(w));
|
||||
|
||||
// Validate time string formats if present
|
||||
const timeStringsValid =
|
||||
(!hasAfter || isValidTimeString(condition.after!)) &&
|
||||
(!hasBefore || isValidTimeString(condition.before!));
|
||||
|
||||
// Prevent after and before being identical (creates zero-length interval)
|
||||
const timeRangeValid =
|
||||
!hasAfter || !hasBefore || condition.after !== condition.before;
|
||||
|
||||
return (
|
||||
(hasTime || hasWeekdays) &&
|
||||
weekdaysValid &&
|
||||
timeStringsValid &&
|
||||
timeRangeValid
|
||||
);
|
||||
}
|
||||
|
||||
function validateUserCondition(condition: UserCondition) {
|
||||
return condition.users != null;
|
||||
}
|
||||
@@ -363,8 +310,6 @@ export function validateConditionalConfig(
|
||||
switch (c.condition) {
|
||||
case "screen":
|
||||
return validateScreenCondition(c);
|
||||
case "time":
|
||||
return validateTimeCondition(c);
|
||||
case "user":
|
||||
return validateUserCondition(c);
|
||||
case "location":
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||
import {
|
||||
ConditionalListenerMixin,
|
||||
setupMediaQueryListeners,
|
||||
} from "../../../mixins/conditional-listener-mixin";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import type { ConditionalCardConfig } from "../cards/types";
|
||||
import type { Condition } from "../common/validate-condition";
|
||||
@@ -19,9 +22,9 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement("hui-conditional-base")
|
||||
export class HuiConditionalBase extends ConditionalListenerMixin<
|
||||
ConditionalCardConfig | ConditionalRowConfig
|
||||
>(ReactiveElement) {
|
||||
export class HuiConditionalBase extends ConditionalListenerMixin(
|
||||
ReactiveElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
@@ -70,13 +73,18 @@ export class HuiConditionalBase extends ConditionalListenerMixin<
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter to supported conditions (those with 'condition' property)
|
||||
const supportedConditions = this._config.conditions.filter(
|
||||
(c) => "condition" in c
|
||||
) as Condition[];
|
||||
|
||||
// Pass filtered conditions to parent implementation
|
||||
super.setupConditionalListeners(supportedConditions);
|
||||
setupMediaQueryListeners(
|
||||
supportedConditions,
|
||||
this.hass,
|
||||
(unsub) => this.addConditionalListener(unsub),
|
||||
(conditionsMet) => {
|
||||
this.setVisibility(conditionsMet);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected update(changed: PropertyValues): void {
|
||||
@@ -94,15 +102,17 @@ export class HuiConditionalBase extends ConditionalListenerMixin<
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateVisibility(conditionsMet?: boolean) {
|
||||
private _updateVisibility() {
|
||||
if (!this._element || !this.hass || !this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._element.preview = this.preview;
|
||||
|
||||
const conditionMet =
|
||||
conditionsMet ?? checkConditionsMet(this._config.conditions, this.hass);
|
||||
const conditionMet = checkConditionsMet(
|
||||
this._config!.conditions,
|
||||
this.hass!
|
||||
);
|
||||
|
||||
this.setVisibility(conditionMet);
|
||||
}
|
||||
|
||||
@@ -39,23 +39,21 @@ export class HuiEntityEditor extends LitElement {
|
||||
private _renderItem(item: EntityConfig, index: number) {
|
||||
const stateObj = this.hass.states[item.entity];
|
||||
|
||||
const useDeviceName =
|
||||
stateObj &&
|
||||
entityUseDeviceName(stateObj, this.hass.entities, this.hass.devices);
|
||||
const useDeviceName = entityUseDeviceName(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const primary =
|
||||
(stateObj &&
|
||||
this.hass.formatEntityName(
|
||||
stateObj,
|
||||
useDeviceName ? { type: "device" } : { type: "entity" }
|
||||
)) ||
|
||||
item.entity;
|
||||
) || item.entity;
|
||||
|
||||
const secondary =
|
||||
stateObj &&
|
||||
this.hass.formatEntityName(
|
||||
const secondary = this.hass.formatEntityName(
|
||||
stateObj,
|
||||
useDeviceName
|
||||
? [{ type: "area" }]
|
||||
|
||||
@@ -4,14 +4,11 @@ import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DOMAINS_INPUT_ROW } from "../../../common/const";
|
||||
import { uid } from "../../../common/util/uid";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { EntitiesCardEntityConfig } from "../cards/types";
|
||||
@@ -39,8 +36,6 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
@property({ attribute: "catch-interaction", type: Boolean })
|
||||
public catchInteraction?;
|
||||
|
||||
private _secondaryInfoElementId = "-" + uid();
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.config) {
|
||||
return nothing;
|
||||
@@ -105,19 +100,7 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
? stateObj.entity_id
|
||||
: this.config.secondary_info === "last-changed"
|
||||
? html`
|
||||
<ha-tooltip
|
||||
for="last-changed${this
|
||||
._secondaryInfoElementId}"
|
||||
placement="right"
|
||||
>
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(stateObj.last_changed),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</ha-tooltip>
|
||||
<ha-relative-time
|
||||
id="last-changed${this._secondaryInfoElementId}"
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_changed}
|
||||
capitalize
|
||||
@@ -125,20 +108,7 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
`
|
||||
: this.config.secondary_info === "last-updated"
|
||||
? html`
|
||||
<ha-tooltip
|
||||
for="last-updated${this
|
||||
._secondaryInfoElementId}"
|
||||
placement="right"
|
||||
>
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(stateObj.last_updated),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</ha-tooltip>
|
||||
<ha-relative-time
|
||||
id="last-updated${this
|
||||
._secondaryInfoElementId}"
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_updated}
|
||||
capitalize
|
||||
@@ -147,22 +117,7 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
: this.config.secondary_info === "last-triggered"
|
||||
? stateObj.attributes.last_triggered
|
||||
? html`
|
||||
<ha-tooltip
|
||||
for="last-triggered${this
|
||||
._secondaryInfoElementId}"
|
||||
placement="right"
|
||||
>
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(
|
||||
stateObj.attributes.last_triggered
|
||||
),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</ha-tooltip>
|
||||
<ha-relative-time
|
||||
id="last-triggered${this
|
||||
._secondaryInfoElementId}"
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.attributes
|
||||
.last_triggered}
|
||||
|
||||
@@ -25,7 +25,6 @@ import "./types/ha-card-condition-numeric_state";
|
||||
import "./types/ha-card-condition-or";
|
||||
import "./types/ha-card-condition-screen";
|
||||
import "./types/ha-card-condition-state";
|
||||
import "./types/ha-card-condition-time";
|
||||
import "./types/ha-card-condition-user";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
@@ -34,7 +33,6 @@ const UI_CONDITION = [
|
||||
"numeric_state",
|
||||
"state",
|
||||
"screen",
|
||||
"time",
|
||||
"user",
|
||||
"and",
|
||||
"not",
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
literal,
|
||||
array,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
assert,
|
||||
enums,
|
||||
} from "superstruct";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import {
|
||||
WEEKDAY_SHORT_TO_LONG,
|
||||
WEEKDAYS_SHORT,
|
||||
} from "../../../../../common/datetime/weekday";
|
||||
import type { TimeCondition } from "../../../common/validate-condition";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../../components/ha-form/types";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
|
||||
const timeConditionStruct = object({
|
||||
condition: literal("time"),
|
||||
after: optional(string()),
|
||||
before: optional(string()),
|
||||
weekdays: optional(array(enums(WEEKDAYS_SHORT))),
|
||||
});
|
||||
|
||||
@customElement("ha-card-condition-time")
|
||||
export class HaCardConditionTime extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public condition!: TimeCondition;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public static get defaultConfig(): TimeCondition {
|
||||
return { condition: "time", after: "08:00", before: "17:00" };
|
||||
}
|
||||
|
||||
protected static validateUIConfig(condition: TimeCondition) {
|
||||
return assert(condition, timeConditionStruct);
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{ name: "after", selector: { time: { no_second: true } } },
|
||||
{ name: "before", selector: { time: { no_second: true } } },
|
||||
{
|
||||
name: "weekdays",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "list",
|
||||
multiple: true,
|
||||
options: WEEKDAYS_SHORT.map((day) => ({
|
||||
value: day,
|
||||
label: localize(`ui.weekdays.${WEEKDAY_SHORT_TO_LONG[day]}`),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.condition}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.schema=${this._schema(this.hass.localize)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const data = ev.detail.value as TimeCondition;
|
||||
fireEvent(this, "value-changed", { value: data });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.time.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-card-condition-time": HaCardConditionTime;
|
||||
}
|
||||
}
|
||||
@@ -155,20 +155,16 @@ export class HuiGraphHeaderFooter
|
||||
}
|
||||
const width = this.clientWidth || this.offsetWidth;
|
||||
// sample to 1 point per hour or 1 point per 5 pixels
|
||||
const maxDetails = Math.max(
|
||||
10,
|
||||
const maxDetails =
|
||||
this._config.detail! > 1
|
||||
? Math.max(width / 5, this._config.hours_to_show!)
|
||||
: this._config.hours_to_show!
|
||||
);
|
||||
const useMean = this._config.detail !== 2;
|
||||
: this._config.hours_to_show!;
|
||||
const { points } = coordinatesMinimalResponseCompressedState(
|
||||
combinedHistory[this._config.entity],
|
||||
width,
|
||||
width / 5,
|
||||
maxDetails,
|
||||
{ minY: this._config.limits?.min, maxY: this._config.limits?.max },
|
||||
useMean
|
||||
{ minY: this._config.limits?.min, maxY: this._config.limits?.max }
|
||||
);
|
||||
this._coordinates = points;
|
||||
},
|
||||
|
||||
@@ -4,7 +4,10 @@ import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||
import {
|
||||
ConditionalListenerMixin,
|
||||
setupMediaQueryListeners,
|
||||
} from "../../../mixins/conditional-listener-mixin";
|
||||
import { checkConditionsMet } from "../common/validate-condition";
|
||||
import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
|
||||
import type { LovelaceHeadingBadge } from "../types";
|
||||
@@ -18,9 +21,7 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement("hui-heading-badge")
|
||||
export class HuiHeadingBadge extends ConditionalListenerMixin<LovelaceHeadingBadgeConfig>(
|
||||
ReactiveElement
|
||||
) {
|
||||
export class HuiHeadingBadge extends ConditionalListenerMixin(ReactiveElement) {
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceHeadingBadgeConfig;
|
||||
@@ -51,7 +52,7 @@ export class HuiHeadingBadge extends ConditionalListenerMixin<LovelaceHeadingBad
|
||||
this._updateVisibility();
|
||||
}
|
||||
|
||||
protected _updateElement(config: LovelaceHeadingBadgeConfig) {
|
||||
private _updateElement(config: LovelaceHeadingBadgeConfig) {
|
||||
if (!this._element) {
|
||||
return;
|
||||
}
|
||||
@@ -132,7 +133,22 @@ export class HuiHeadingBadge extends ConditionalListenerMixin<LovelaceHeadingBad
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateVisibility(conditionsMet?: boolean) {
|
||||
protected setupConditionalListeners() {
|
||||
if (!this.config?.visibility || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupMediaQueryListeners(
|
||||
this.config.visibility,
|
||||
this.hass,
|
||||
(unsub) => this.addConditionalListener(unsub),
|
||||
(conditionsMet) => {
|
||||
this._updateVisibility(conditionsMet);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _updateVisibility(forceVisible?: boolean) {
|
||||
if (!this._element || !this.hass) {
|
||||
return;
|
||||
}
|
||||
@@ -142,20 +158,11 @@ export class HuiHeadingBadge extends ConditionalListenerMixin<LovelaceHeadingBad
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.preview) {
|
||||
this._setElementVisibility(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.config?.disabled) {
|
||||
this._setElementVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const visible =
|
||||
conditionsMet ??
|
||||
(!this.config?.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass));
|
||||
forceVisible ||
|
||||
this.preview ||
|
||||
!this.config?.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass);
|
||||
this._setElementVisibility(visible);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ import type {
|
||||
} from "../../../data/lovelace/config/section";
|
||||
import { isStrategySection } from "../../../data/lovelace/config/section";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||
import {
|
||||
ConditionalListenerMixin,
|
||||
setupMediaQueryListeners,
|
||||
} from "../../../mixins/conditional-listener-mixin";
|
||||
import "../cards/hui-card";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { checkConditionsMet } from "../common/validate-condition";
|
||||
@@ -34,9 +37,7 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement("hui-section")
|
||||
export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
ReactiveElement
|
||||
) {
|
||||
export class HuiSection extends ConditionalListenerMixin(ReactiveElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
|
||||
@@ -58,6 +59,8 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
|
||||
private _layoutElement?: LovelaceSectionElement;
|
||||
|
||||
private _config: LovelaceSectionConfig | undefined;
|
||||
|
||||
@storage({
|
||||
key: "dashboardCardClipboard",
|
||||
state: false,
|
||||
@@ -113,7 +116,7 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._updateVisibility();
|
||||
this._updateElement();
|
||||
}
|
||||
|
||||
protected update(changedProperties) {
|
||||
@@ -144,11 +147,26 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
this._layoutElement.cards = this._cards;
|
||||
}
|
||||
if (changedProperties.has("hass") || changedProperties.has("preview")) {
|
||||
this._updateVisibility();
|
||||
this._updateElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected setupConditionalListeners() {
|
||||
if (!this._config?.visibility || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupMediaQueryListeners(
|
||||
this._config.visibility,
|
||||
this.hass,
|
||||
(unsub) => this.addConditionalListener(unsub),
|
||||
(conditionsMet) => {
|
||||
this._updateElement(conditionsMet);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _initializeConfig() {
|
||||
let sectionConfig = { ...this.config };
|
||||
let isStrategy = false;
|
||||
@@ -190,11 +208,11 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
while (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this._updateVisibility();
|
||||
this._updateElement();
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateVisibility(conditionsMet?: boolean) {
|
||||
private _updateElement(ignoreConditions?: boolean) {
|
||||
if (!this._layoutElement || !this._config) {
|
||||
return;
|
||||
}
|
||||
@@ -210,9 +228,9 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
}
|
||||
|
||||
const visible =
|
||||
conditionsMet ??
|
||||
(!this._config.visibility ||
|
||||
checkConditionsMet(this._config.visibility, this.hass));
|
||||
ignoreConditions ||
|
||||
!this._config.visibility ||
|
||||
checkConditionsMet(this._config.visibility, this.hass);
|
||||
|
||||
this._setElementVisibility(visible);
|
||||
}
|
||||
|
||||
@@ -168,27 +168,6 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
|
||||
const summaryEntities = Object.values(entitiesBySummary).flat();
|
||||
|
||||
// Scenes section
|
||||
const sceneFilter = generateEntityFilter(hass, {
|
||||
domain: "scene",
|
||||
entity_category: "none",
|
||||
});
|
||||
const scenes = areaEntities.filter(sceneFilter);
|
||||
|
||||
if (scenes.length > 0) {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
computeHeadingCard(
|
||||
hass.localize("ui.panel.lovelace.strategy.home.scenes"),
|
||||
"mdi:palette",
|
||||
"/config/scene/dashboard"
|
||||
),
|
||||
...scenes.map(computeTileCard),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Automations section
|
||||
const automationFilter = generateEntityFilter(hass, {
|
||||
domain: "automation",
|
||||
@@ -199,9 +178,7 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
// Rest of entities grouped by device
|
||||
const otherEntities = areaEntities.filter(
|
||||
(entityId) =>
|
||||
!summaryEntities.includes(entityId) &&
|
||||
!scenes.includes(entityId) &&
|
||||
!automations.includes(entityId)
|
||||
!summaryEntities.includes(entityId) && !automations.includes(entityId)
|
||||
);
|
||||
|
||||
const entitiesByDevice: Record<string, string[]> = {};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user