Compare commits

..

1 Commits

Author SHA1 Message Date
Paulus Schoutsen 9ecb41accd App info page no longer links to itself 2026-03-29 10:45:00 -04:00
350 changed files with 4143 additions and 5247 deletions
+3 -3
View File
@@ -41,14 +41,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -62,4 +62,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
+48 -91
View File
@@ -99,44 +99,6 @@ const lokaliseProjects = {
frontend: "3420425759f6d6d241f598.13594006",
};
const POLL_INTERVAL_MS = 1000;
/* eslint-disable no-await-in-loop */
async function pollProcess(lokaliseApi, projectId, processId) {
while (true) {
const process = await lokaliseApi
.queuedProcesses()
.get(processId, { project_id: projectId });
const project =
projectId === lokaliseProjects.backend ? "backend" : "frontend";
if (process.status === "finished") {
console.log(`Lokalise export process for ${project} finished`);
return process;
}
if (process.status === "failed" || process.status === "cancelled") {
throw new Error(
`Lokalise export process for ${project} ${process.status}: ${process.message}`
);
}
console.log(
`Lokalise export process for ${project} in progress...`,
process.status,
process.details?.items_to_process
? `${Math.round(((process.details.items_processed || 0) / process.details.items_to_process) * 100)}% (${process.details.items_processed}/${process.details.items_to_process})`
: ""
);
await new Promise((resolve) => {
setTimeout(resolve, POLL_INTERVAL_MS);
});
}
}
/* eslint-enable no-await-in-loop */
gulp.task("fetch-lokalise", async function () {
let apiKey;
try {
@@ -156,60 +118,55 @@ gulp.task("fetch-lokalise", async function () {
]);
await Promise.all(
Object.entries(lokaliseProjects).map(async ([project, projectId]) => {
try {
const exportProcess = await lokaliseApi
.files()
.async_download(projectId, {
format: "json",
original_filenames: false,
replace_breaks: false,
json_unescaped_slashes: true,
export_empty_as: "skip",
filter_data: ["verified"],
});
const finishedProcess = await pollProcess(
lokaliseApi,
projectId,
exportProcess.process_id
);
const bundleUrl = finishedProcess.details.download_url;
console.log(`Downloading translations from: ${bundleUrl}`);
const response = await fetch(bundleUrl);
if (response.status !== 200 && response.status !== 0) {
Object.entries(lokaliseProjects).map(([project, projectId]) =>
lokaliseApi
.files()
.download(projectId, {
format: "json",
original_filenames: false,
replace_breaks: false,
json_unescaped_slashes: true,
export_empty_as: "skip",
filter_data: ["verified"],
})
.then((download) => fetch(download.bundle_url))
.then((response) => {
if (response.status === 200 || response.status === 0) {
return response.arrayBuffer();
}
throw new Error(response.statusText);
}
console.log(`Extracting translations...`);
const contents = await JSZip.loadAsync(await response.arrayBuffer());
await mkdirPromise;
await Promise.all(
Object.keys(contents.files).map(async (filename) => {
const file = contents.file(filename);
if (!file) {
// no file, probably a directory
return;
}
const content = await file.async("nodebuffer");
await fs.writeFile(
path.join(inDir, project, filename.split("/").splice(-1)[0]),
content,
{ flag: "w", encoding }
);
})
);
} catch (err) {
console.error(err);
throw err;
}
})
})
.then(JSZip.loadAsync)
.then(async (contents) => {
await mkdirPromise;
return Promise.all(
Object.keys(contents.files).map(async (filename) => {
const file = contents.file(filename);
if (!file) {
// no file, probably a directory
return Promise.resolve();
}
return file
.async("nodebuffer")
.then((content) =>
fs.writeFile(
path.join(
inDir,
project,
filename.split("/").splice(-1)[0]
),
content,
{ flag: "w", encoding }
)
);
})
);
})
.catch((err) => {
console.error(err);
throw err;
})
)
);
});
+5 -5
View File
@@ -26,7 +26,7 @@ import "../../../../src/components/ha-svg-icon";
import "../../../../src/layouts/hass-loading-screen";
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
import "./hc-layout";
import "../../../../src/components/input/ha-input";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-button";
const seeFAQ = (qid) => html`
@@ -123,11 +123,11 @@ export class HcConnect extends LitElement {
To get started, enter your Home Assistant URL and click authorize.
If you want a preview instead, click the show demo button.
</p>
<ha-input
<ha-textfield
label="Home Assistant URL"
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
@keydown=${this._handleInputKeyDown}
></ha-input>
></ha-textfield>
${this.error ? html` <p class="error">${this.error}</p> ` : ""}
</div>
<div class="card-actions">
@@ -204,7 +204,7 @@ export class HcConnect extends LitElement {
}
private async _handleConnect() {
const inputEl = this.shadowRoot!.querySelector("ha-input")!;
const inputEl = this.shadowRoot!.querySelector("ha-textfield")!;
const value = inputEl.value || "";
this.error = undefined;
@@ -319,7 +319,7 @@ export class HcConnect extends LitElement {
flex: 1;
}
ha-input {
ha-textfield {
width: 100%;
}
`;
-1
View File
@@ -1,4 +1,3 @@
/// <reference types="chromecast-caf-sender" />
import { mdiTelevision } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
+2 -6
View File
@@ -1,5 +1,5 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
@@ -692,11 +692,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
([key, value]) => html`
<ha-settings-row narrow slot=${slot}>
<span slot="heading">${value?.name || key}</span>
${value?.description
? html`<span slot="description"
>${value?.description}</span
>`
: nothing}
<span slot="description">${value?.description}</span>
<ha-selector
.hass=${this.hass}
.selector=${value!.selector}
-15
View File
@@ -134,21 +134,6 @@ const CONFIGS = [
entity: sensor.not_working
`,
},
{
heading: "Lower minimum",
config: `
- type: gauge
entity: sensor.brightness_high
needle: true
severity:
green: 0
yellow: 0.45
red: 0.9
min: -0.05
name: " "
max: 1.9
unit: GBP/h`,
},
];
@customElement("demo-lovelace-gauge-card")
+1
View File
@@ -422,6 +422,7 @@ export class DemoEntityState extends LitElement {
return html`
<ha-data-table
.hass=${this.hass}
.columns=${this._columns(this.hass)}
.data=${this._rows()}
auto-height
+5 -5
View File
@@ -82,7 +82,7 @@
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.20",
"@swc/helpers": "0.5.19",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
@@ -149,7 +149,7 @@
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.6",
"@rsdoctor/rspack-plugin": "1.5.5",
"@rspack/core": "1.7.10",
"@rspack/dev-server": "1.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -169,7 +169,7 @@
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.1.2",
"@vitest/coverage-v8": "4.1.1",
"babel-loader": "10.1.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -207,10 +207,10 @@
"tar": "7.5.13",
"terser-webpack-plugin": "5.4.0",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.57.2",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.2",
"vitest": "4.1.1",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.0#~/.yarn/patches/workbox-build-npm-7.4.0-c84561662c.patch"
-1
View File
@@ -1,5 +1,4 @@
/* eslint-disable no-console */
/// <reference types="chromecast-caf-sender" />
import type { Auth } from "home-assistant-js-websocket";
import { castApiAvailable } from "./cast_framework";
-28
View File
@@ -32,12 +32,6 @@ const YAML_ONLY_THEMES_COLORS = new Set([
"disabled",
]);
/**
* Compose a CSS variable out of a theme color
* @param color - Theme color (examples: `red`, `primary-text`)
* @returns CSS variable in `--xxx-color` format;
* initial color if not found in theme colors
*/
export function computeCssVariableName(color: string): string {
if (THEME_COLORS.has(color) || YAML_ONLY_THEMES_COLORS.has(color)) {
return `--${color}-color`;
@@ -45,12 +39,6 @@ export function computeCssVariableName(color: string): string {
return color;
}
/**
* Compose a CSS variable out of a theme color & then resolve it
* @param color - Theme color (examples: `red`, `primary-text`)
* @returns Resolved CSS variable in `var(--xxx-color)` format;
* initial color if not found in theme colors
*/
export function computeCssColor(color: string): string {
const cssVarName = computeCssVariableName(color);
if (cssVarName !== color) {
@@ -59,22 +47,6 @@ export function computeCssColor(color: string): string {
return color;
}
/**
* Get a color from document's styles
* @param color - Named theme color (examples: `red`, `primary-text`)
* @returns Resolved color; initial color if not found in document's styles
*/
export function resolveThemeColor(color: string): string {
const cssColor = computeCssVariableName(color);
if (cssColor.startsWith("--")) {
const resolved = getComputedStyle(document.body)
.getPropertyValue(cssColor)
.trim();
return resolved || color;
}
return cssColor;
}
/**
* Validates if a string is a valid color.
* Accepts: hex colors (#xxx, #xxxxxx), theme colors, and valid CSS color names.
+12 -30
View File
@@ -1,6 +1,5 @@
import colors from "color-name";
import { expandHex } from "./hex";
import { resolveThemeColor } from "./compute-color";
const rgb_hex = (component: number): string => {
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
@@ -131,43 +130,26 @@ export const rgb2hs = (rgb: [number, number, number]): [number, number] =>
export const hs2rgb = (hs: [number, number]): [number, number, number] =>
hsv2rgb([hs[0], hs[1], 255]);
/**
* Attempt to get a HEX color from a color defined in different formats:
* HEX, rgb/rgba, named color
* @param color - Color (HEX, rgb/rgba, named color) to be converted to HEX
* @returns HEX color
*/
export function theme2hex(color: string): string {
// Attempting to find a HEX pattern in the input string
if (color.startsWith("#")) {
if (color.length === 4 || color.length === 5) {
const c = color;
export function theme2hex(themeColor: string): string {
if (themeColor.startsWith("#")) {
if (themeColor.length === 4 || themeColor.length === 5) {
const c = themeColor;
// Convert short-form hex (#abc) to 6 digit (#aabbcc). Ignore alpha channel.
return `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
}
if (color.length === 9) {
if (themeColor.length === 9) {
// Ignore alpha channel.
return color.substring(0, 7);
return themeColor.substring(0, 7);
}
return color;
return themeColor;
}
// Attempting to find a match in a HA Frontend theme colors
const themeColor = resolveThemeColor(color.toLowerCase());
if (themeColor !== color.toLowerCase()) {
// theme color is recognized, now re-attempt
return theme2hex(themeColor);
const rgbFromColorName = colors[themeColor.toLowerCase()];
if (rgbFromColorName) {
return rgb2hex(rgbFromColorName);
}
// Attempting to find a match in a web colors array
const rgbFromWebColor = colors[color.toLowerCase()];
if (rgbFromWebColor) {
// HEX color is recognized for the input named color
return rgb2hex(rgbFromWebColor);
}
// Attempting to find an RGB pattern in the input string
const rgbMatch = color.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (rgbMatch) {
const [, r, g, b] = rgbMatch.map(Number);
return rgb2hex([r, g, b]);
@@ -176,5 +158,5 @@ export function theme2hex(color: string): string {
// We have a named color, and there's nothing in the table,
// so nothing further we can do with it.
// Compare/border/background color will all be the same.
return color;
return themeColor;
}
-11
View File
@@ -1,5 +1,4 @@
import { wcagLuminance, wcagContrast } from "culori";
import { theme2hex } from "./convert-color";
/**
* Calculates the luminosity of an RGB color.
@@ -49,13 +48,3 @@ export const getRGBContrastRatio = (
rgb1: [number, number, number],
rgb2: [number, number, number]
) => Math.round((rgbContrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
/**
* Returns a contrasted color (black or white) based on the luminance of another color
* @param color - Color (HEX, rgb/rgba, named color) to calculate a contrasted color
* @returns HEX color ("#000000" for dark backgrounds, "#ffffff" for light backgrounds)
*/
export const getContrastedColorHex = (color: string): string => {
const lum = wcagLuminance(theme2hex(color));
return lum > 0.5 ? "#000000" : "#ffffff";
};
+5 -26
View File
@@ -1,9 +1,6 @@
import { listenMediaQuery } from "../dom/media_query";
import type { HomeAssistant } from "../../types";
import type {
Condition,
ConditionContext,
} from "../../panels/lovelace/common/validate-condition";
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";
@@ -22,8 +19,7 @@ export function setupMediaQueryListeners(
conditions: Condition[],
hass: HomeAssistant,
addListener: (unsub: () => void) => void,
onUpdate: (conditionsMet: boolean) => void,
getContext?: () => ConditionContext
onUpdate: (conditionsMet: boolean) => void
): void {
const mediaQueries = extractMediaQueries(conditions);
@@ -40,8 +36,7 @@ export function setupMediaQueryListeners(
if (hasOnlyMediaQuery) {
onUpdate(matches);
} else {
const context = getContext?.() ?? {};
const conditionsMet = checkConditionsMet(conditions, hass, context);
const conditionsMet = checkConditionsMet(conditions, hass);
onUpdate(conditionsMet);
}
});
@@ -56,8 +51,7 @@ export function setupTimeListeners(
conditions: Condition[],
hass: HomeAssistant,
addListener: (unsub: () => void) => void,
onUpdate: (conditionsMet: boolean) => void,
getContext?: () => ConditionContext
onUpdate: (conditionsMet: boolean) => void
): void {
const timeConditions = extractTimeConditions(conditions);
@@ -76,8 +70,7 @@ export function setupTimeListeners(
timeoutId = setTimeout(() => {
if (delay <= MAX_TIMEOUT_DELAY) {
const context = getContext?.() ?? {};
const conditionsMet = checkConditionsMet(conditions, hass, context);
const conditionsMet = checkConditionsMet(conditions, hass);
onUpdate(conditionsMet);
}
scheduleUpdate();
@@ -94,17 +87,3 @@ export function setupTimeListeners(
scheduleUpdate();
});
}
/**
* Sets up all condition listeners (media query, time) for conditional visibility.
*/
export function setupConditionListeners(
conditions: Condition[],
hass: HomeAssistant,
addListener: (unsub: () => void) => void,
onUpdate: (conditionsMet: boolean) => void,
getContext?: () => ConditionContext
): void {
setupMediaQueryListeners(conditions, hass, addListener, onUpdate, getContext);
setupTimeListeners(conditions, hass, addListener, onUpdate, getContext);
}
+2 -2
View File
@@ -14,7 +14,7 @@ export const isLoadedIntegration = (
) =>
!page.component ||
ensureArray(page.component).some((integration) =>
isComponentLoaded(hass.config, integration)
isComponentLoaded(hass, integration)
);
export const isNotLoadedIntegration = (
@@ -23,7 +23,7 @@ export const isNotLoadedIntegration = (
) =>
!page.not_component ||
!ensureArray(page.not_component).some((integration) =>
isComponentLoaded(hass.config, integration)
isComponentLoaded(hass, integration)
);
export const isCore = (page: PageNavigation) => page.core;
+2 -2
View File
@@ -2,6 +2,6 @@ import type { HomeAssistant } from "../../types";
/** Return if a component is loaded. */
export const isComponentLoaded = (
hassConfig: HomeAssistant["config"],
hass: HomeAssistant,
component: string
): boolean => hassConfig && hassConfig.components.includes(component);
): boolean => hass && hass.config.components.includes(component);
+6 -7
View File
@@ -14,25 +14,24 @@ export const computeDeviceName = (
export const computeDeviceNameDisplay = (
device: DeviceRegistryEntry,
localize: HomeAssistant["localize"],
hassStates: HomeAssistant["states"],
hass: HomeAssistant,
entities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) =>
computeDeviceName(device) ||
(entities && fallbackDeviceName(hassStates, entities)) ||
localize("ui.panel.config.devices.unnamed_device", {
type: localize(
(entities && fallbackDeviceName(hass, entities)) ||
hass.localize("ui.panel.config.devices.unnamed_device", {
type: hass.localize(
`ui.panel.config.devices.type.${device.entry_type || "device"}`
),
});
export const fallbackDeviceName = (
hassStates: HomeAssistant["states"],
hass: HomeAssistant,
entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) => {
for (const entity of entities || []) {
const entityId = typeof entity === "string" ? entity : entity.entity_id;
const stateObj = hassStates[entityId];
const stateObj = hass.states[entityId];
if (stateObj) {
return computeStateName(stateObj);
}
@@ -1,11 +1,26 @@
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";
export const getDeviceArea = (
interface DeviceContext {
device: DeviceRegistryEntry;
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}
export const getDeviceContext = (
device: DeviceRegistryEntry,
areas: HomeAssistant["areas"]
): AreaRegistryEntry | undefined => {
hass: HomeAssistant
): DeviceContext => {
const areaId = device.area_id;
return areaId ? areas[areaId] : undefined;
const area = areaId ? hass.areas[areaId] : undefined;
const floorId = area?.floor_id;
const floor = floorId ? hass.floors[floorId] : undefined;
return {
device: device,
area: area || null,
floor: floor || null,
};
};
+2 -2
View File
@@ -27,7 +27,7 @@ export const isDeletableEntity = (
const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id);
if (isHelperDomain(domain)) {
return !!(
isComponentLoaded(hass.config, domain) &&
isComponentLoaded(hass, domain) &&
entityRegEntry &&
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)
);
@@ -56,7 +56,7 @@ export const deleteEntity = (
const domain = computeDomain(entity_id);
const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id);
if (isHelperDomain(domain)) {
if (isComponentLoaded(hass.config, domain)) {
if (isComponentLoaded(hass, domain)) {
if (
entityRegEntry &&
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)
@@ -1,8 +0,0 @@
/**
* Indicates whether the current browser has native ElementInternals support.
*/
export const nativeElementInternalsSupported =
Boolean(globalThis.ElementInternals) &&
globalThis.HTMLElement?.prototype.attachInternals
?.toString()
.includes("[native code]");
@@ -41,7 +41,7 @@ export const protocolIntegrationPicked = async (
).filter((e) => !e.disabled_by);
if (
!isComponentLoaded(hass.config, "zwave_js") ||
!isComponentLoaded(hass, "zwave_js") ||
(!options?.config_entry && !entries?.length)
) {
// If the component isn't loaded, ask them to load the integration first
@@ -90,7 +90,7 @@ export const protocolIntegrationPicked = async (
).filter((e) => !e.disabled_by);
if (
!isComponentLoaded(hass.config, "zha") ||
!isComponentLoaded(hass, "zha") ||
(!options?.config_entry && !entries?.length)
) {
// If the component isn't loaded, ask them to load the integration first
@@ -139,7 +139,7 @@ export const protocolIntegrationPicked = async (
})
).filter((e) => !e.disabled_by);
if (
!isComponentLoaded(hass.config, domain) ||
!isComponentLoaded(hass, domain) ||
(!options?.config_entry && !entries?.length)
) {
// If the component isn't loaded, ask them to load the integration first
-29
View File
@@ -5,41 +5,12 @@ import {
formatDateMonthYear,
formatDateVeryShort,
formatDateWeekdayShort,
formatDateYear,
} from "../../common/datetime/format_date";
import {
formatTime,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
export function getPeriodicAxisLabelConfig(
period: string,
locale: FrontendLocaleData,
config: HassConfig
):
| {
formatter: (value: number) => string;
}
| undefined {
if (period === "month") {
return {
formatter: (value: number) => {
const date = new Date(value);
return date.getMonth() === 0
? `{bold|${formatDateMonthYear(date, locale, config)}}`
: formatDateMonth(date, locale, config);
},
};
}
if (period === "year") {
return {
formatter: (value: number) =>
formatDateYear(new Date(value), locale, config),
};
}
return undefined;
}
export function formatTimeLabel(
value: number | Date,
locale: FrontendLocaleData,
+12 -140
View File
@@ -91,10 +91,6 @@ export class HaChartBase extends LitElement {
private _lastTapTime?: number;
private _longPressTimer?: ReturnType<typeof setTimeout>;
private _longPressTriggered = false;
private _shouldResizeChart = false;
private _resizeAnimationDuration?: number;
@@ -132,7 +128,6 @@ export class HaChartBase extends LitElement {
public disconnectedCallback() {
super.disconnectedCallback();
this._legendPointerCancel();
this._pendingSetup = false;
while (this._listeners.length) {
this._listeners.pop()!();
@@ -307,31 +302,22 @@ export class HaChartBase extends LitElement {
`;
}
private _getLegendItems() {
private _renderLegend() {
if (!this.options?.legend || !this.data) {
return undefined;
return nothing;
}
const legend = ensureArray(this.options.legend).find(
(l) => l.show && l.type === "custom"
) as CustomLegendOption | undefined;
if (!legend) {
return undefined;
return nothing;
}
const datasets = ensureArray(this.data);
return (
const items =
legend.data ||
datasets
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
.map((d) => ({ id: d.id, name: d.name }))
);
}
private _renderLegend() {
const items = this._getLegendItems();
if (!items) {
return nothing;
}
const datasets = ensureArray(this.data!);
.map((d) => ({ id: d.id, name: d.name }));
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
@@ -376,11 +362,6 @@ export class HaChartBase extends LitElement {
return html`<li
.id=${id}
@click=${this._legendClick}
@pointerdown=${this._legendPointerDown}
@pointerup=${this._legendPointerCancel}
@pointerleave=${this._legendPointerCancel}
@pointercancel=${this._legendPointerCancel}
@contextmenu=${this._legendContextMenu}
class=${classMap({ hidden: this._hiddenDatasets.has(id) })}
.title=${name}
>
@@ -606,7 +587,10 @@ export class HaChartBase extends LitElement {
id: "dataZoom",
type: "inside",
orient: "horizontal",
filterMode: this._getDataZoomFilterMode() as any,
// "boundaryFilter" is a custom mode added via axis-proxy-patch.ts.
// It rescales the Y-axis to the visible data while keeping one point
// just outside each boundary to avoid line gaps at the zoom edges.
filterMode: "boundaryFilter" as any,
xAxisIndex: 0,
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
@@ -614,23 +598,6 @@ export class HaChartBase extends LitElement {
};
}
// "boundaryFilter" is a custom mode added via axis-proxy-patch.ts.
// It rescales the Y-axis to the visible data while keeping one point
// just outside each boundary to avoid line gaps at the zoom edges.
// Use "filter" for bar charts since boundaryFilter causes rendering issues.
// Use "weakFilter" for other types (e.g. custom/timeline) so bars
// spanning the visible range boundary are kept.
private _getDataZoomFilterMode(): string {
const series = ensureArray(this.data);
if (series.every((s) => s.type === "line")) {
return "boundaryFilter";
}
if (series.some((s) => s.type === "bar")) {
return "filter";
}
return "weakFilter";
}
private _createOptions(): ECOption {
let xAxis = this.options?.xAxis;
if (xAxis) {
@@ -665,7 +632,7 @@ export class HaChartBase extends LitElement {
hideOverlap: true,
...axis.axisLabel,
},
minInterval: axis.minInterval ?? minInterval,
minInterval,
} as XAXisOption;
});
}
@@ -1055,52 +1022,11 @@ export class HaChartBase extends LitElement {
fireEvent(this, "chart-zoom", { start, end });
}
// Long-press to solo on touch/pen devices (500ms, consistent with action-handler-directive)
private _legendPointerDown(ev: PointerEvent) {
// Mouse uses Ctrl/Cmd+click instead
if (ev.pointerType === "mouse") {
return;
}
const id = (ev.currentTarget as HTMLElement)?.id;
if (!id) {
return;
}
this._longPressTriggered = false;
this._longPressTimer = setTimeout(() => {
this._longPressTriggered = true;
this._longPressTimer = undefined;
this._soloLegend(id);
}, 500);
}
private _legendPointerCancel() {
if (this._longPressTimer) {
clearTimeout(this._longPressTimer);
this._longPressTimer = undefined;
}
}
private _legendContextMenu(ev: Event) {
if (this._longPressTimer || this._longPressTriggered) {
ev.preventDefault();
}
}
private _legendClick(ev: MouseEvent) {
private _legendClick(ev: any) {
if (!this.chart) {
return;
}
if (this._longPressTriggered) {
this._longPressTriggered = false;
return;
}
const id = (ev.currentTarget as HTMLElement)?.id;
// Cmd+click on Mac (Ctrl+click is right-click there), Ctrl+click elsewhere
const soloModifier = isMac ? ev.metaKey : ev.ctrlKey;
if (soloModifier) {
this._soloLegend(id);
return;
}
const id = ev.currentTarget?.id;
if (this._hiddenDatasets.has(id)) {
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
this._hiddenDatasets.delete(i)
@@ -1115,60 +1041,6 @@ export class HaChartBase extends LitElement {
this.requestUpdate("_hiddenDatasets");
}
private _soloLegend(id: string) {
const allIds = this._getAllLegendIds();
const clickedIds = this._getAllIdsFromLegend(this.options, id);
const otherIds = allIds.filter((i) => !clickedIds.includes(i));
const clickedIsOnlyVisible =
clickedIds.every((i) => !this._hiddenDatasets.has(i)) &&
otherIds.every((i) => this._hiddenDatasets.has(i));
if (clickedIsOnlyVisible) {
// Already solo'd on this item — restore all series to visible
for (const hiddenId of [...this._hiddenDatasets]) {
this._hiddenDatasets.delete(hiddenId);
fireEvent(this, "dataset-unhidden", { id: hiddenId });
}
} else {
// Solo: hide every other series, unhide clicked if it was hidden
for (const otherId of otherIds) {
if (!this._hiddenDatasets.has(otherId)) {
this._hiddenDatasets.add(otherId);
fireEvent(this, "dataset-hidden", { id: otherId });
}
}
for (const clickedId of clickedIds) {
if (this._hiddenDatasets.has(clickedId)) {
this._hiddenDatasets.delete(clickedId);
fireEvent(this, "dataset-unhidden", { id: clickedId });
}
}
}
this.requestUpdate("_hiddenDatasets");
}
private _getAllLegendIds(): string[] {
const items = this._getLegendItems();
if (!items) {
return [];
}
const allIds = new Set<string>();
for (const item of items) {
const primaryId =
typeof item === "string"
? item
: ((item.id as string) ?? (item.name as string) ?? "");
for (const expandedId of this._getAllIdsFromLegend(
this.options,
primaryId
)) {
allIds.add(expandedId);
}
}
return [...allIds];
}
private _toggleExpandedLegend() {
this.expandLegend = !this.expandLegend;
setTimeout(() => {
+2 -12
View File
@@ -65,8 +65,6 @@ export interface NetworkData {
categories?: { name: string; symbol: string }[];
}
const PHYSICS_DISABLE_THRESHOLD = 512;
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
let GraphChart: typeof import("echarts/lib/chart/graph/install");
@@ -96,7 +94,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
@state() private _reducedMotion = false;
@state() private _physicsEnabled?: boolean;
@state() private _physicsEnabled = true;
@state() private _showLabels = true;
@@ -124,14 +122,6 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
];
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (this._physicsEnabled === undefined && this.data?.nodes?.length > 1) {
this._physicsEnabled =
this.data.nodes.length <= PHYSICS_DISABLE_THRESHOLD;
}
}
protected render() {
if (!GraphChart || !this.data.nodes?.length) {
return nothing;
@@ -148,7 +138,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.data=${this._getSeries(
this.data,
this._physicsEnabled ?? false,
this._physicsEnabled,
this._reducedMotion,
this._showLabels,
isMobile,
+1 -1
View File
@@ -105,7 +105,7 @@ export class StateHistoryCharts extends LitElement {
@restoreScroll(".container") private _savedScrollPos?: number;
protected render() {
if (!isComponentLoaded(this.hass.config, "history")) {
if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info">
${this.hass.localize("ui.components.history_charts.history_disabled")}
</div>`;
+1 -18
View File
@@ -32,7 +32,6 @@ import {
} from "../../data/recorder";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant } from "../../types";
import { getPeriodicAxisLabelConfig } from "./axis-label";
import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base";
@@ -149,7 +148,7 @@ export class StatisticsChart extends LitElement {
}
protected render(): TemplateResult {
if (!isComponentLoaded(this.hass.config, "history")) {
if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info">
${this.hass.localize("ui.components.history_charts.history_disabled")}
</div>`;
@@ -294,22 +293,6 @@ export class StatisticsChart extends LitElement {
type: "time",
min: startTime,
max: this.endTime,
...(this.period === "month" && {
minInterval: 28 * 24 * 3600 * 1000,
axisLabel: getPeriodicAxisLabelConfig(
"month",
this.hass.locale,
this.hass.config
),
}),
...(this.period === "year" && {
minInterval: 365 * 24 * 3600 * 1000,
axisLabel: getPeriodicAxisLabelConfig(
"year",
this.hass.locale,
this.hass.config
),
}),
},
{
id: "hiddenAxis",
@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { computeCssColor } from "../../common/color/compute-color";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { stringCompare } from "../../common/string/compare";
@@ -52,15 +53,16 @@ class HaDataTableLabels extends LitElement {
}
private _renderLabel(label: LabelRegistryEntry, clickAction: boolean) {
const color = label?.color ? computeCssColor(label.color) : undefined;
return html`
<ha-label
dense
role="button"
tabindex="0"
.color=${label.color}
.item=${label}
@click=${clickAction ? this._labelClicked : undefined}
@keydown=${clickAction ? this._labelClicked : undefined}
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label?.icon
@@ -100,6 +102,10 @@ class HaDataTableLabels extends LitElement {
position: fixed;
flex-wrap: nowrap;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
.plus {
--ha-label-background-color: transparent;
border: 1px solid var(--divider-color);
+19 -24
View File
@@ -1,4 +1,3 @@
import { consume, type ContextType } from "@lit/context";
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
@@ -22,10 +21,9 @@ import type { LocalizeFunc } from "../../common/translations/localize";
import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status";
import { localeContext, localizeContext } from "../../data/context";
import type { FrontendLocaleData } from "../../data/translation";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon";
@@ -106,13 +104,9 @@ const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
@customElement("ha-data-table")
export class HaDataTable extends LitElement {
@state()
@consume({ context: localizeContext, subscribe: true })
private _localize?: ContextType<typeof localizeContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: localeContext, subscribe: true })
private _locale?: ContextType<typeof localeContext>;
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
@property({ type: Boolean }) public narrow = false;
@@ -384,6 +378,8 @@ export class HaDataTable extends LitElement {
);
protected render() {
const localize = this.localizeFunc || this.hass.localize;
const columns = this._sortedColumns(this.columns, this.columnOrder);
const renderRow = (row: DataTableRowData, index: number) =>
@@ -507,8 +503,7 @@ export class HaDataTable extends LitElement {
<div class="mdc-data-table__row" role="row">
<div class="mdc-data-table__cell grows center" role="cell">
${this.noDataText ||
this._localize?.("ui.components.data-table.no-data") ||
"No data"}
localize("ui.components.data-table.no-data")}
</div>
</div>
</div>
@@ -520,8 +515,7 @@ export class HaDataTable extends LitElement {
@scroll=${this._saveScrollPos}
.items=${this._groupData(
this._filteredData,
this._localize,
this._locale,
localize,
this.appendRow,
this.groupColumn,
this.groupOrder,
@@ -691,7 +685,7 @@ export class HaDataTable extends LitElement {
this._sortColumns[this.sortColumn],
this.sortDirection,
this.sortColumn,
this._locale?.language
this.hass.locale.language
)
: filteredData;
@@ -717,8 +711,7 @@ export class HaDataTable extends LitElement {
private _groupData = memoizeOne(
(
data: DataTableRowData[],
localize: LocalizeFunc | undefined,
locale: FrontendLocaleData | undefined,
localize: LocalizeFunc,
appendRow,
groupColumn: string | undefined,
groupOrder: string[] | undefined,
@@ -742,7 +735,11 @@ export class HaDataTable extends LitElement {
)
.sort((a, b) => {
if (!groupOrder && isGroupSortColumn) {
const comparison = stringCompare(a, b, locale?.language);
const comparison = stringCompare(
a,
b,
this.hass.locale.language
);
if (sortDirection === "asc") {
return comparison;
}
@@ -763,7 +760,7 @@ export class HaDataTable extends LitElement {
return stringCompare(
["", "-", "—"].includes(a) ? "zzz" : a,
["", "-", "—"].includes(b) ? "zzz" : b,
locale?.language
this.hass.locale.language
);
})
.reduce(
@@ -790,15 +787,14 @@ export class HaDataTable extends LitElement {
>
<ha-icon-button
.path=${mdiChevronUp}
.label=${localize?.(
.label=${this.hass.localize(
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
) || (collapsed ? "Expand" : "Collapse")}
)}
class=${collapsed ? "collapsed" : ""}
>
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
? localize?.("ui.components.data-table.ungrouped") ||
"Ungrouped"
? localize("ui.components.data-table.ungrouped")
: groupName || ""}
</div>`,
});
@@ -867,8 +863,7 @@ export class HaDataTable extends LitElement {
const groupedData = this._groupData(
this._filteredData,
this._localize,
this._locale,
this.localizeFunc || this.hass.localize,
this.appendRow,
this.groupColumn,
this.groupOrder,
+26 -95
View File
@@ -4,7 +4,7 @@ import type { ActionDetail } from "@material/mwc-list";
import { mdiCalendarToday } from "@mdi/js";
import "cally";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import {
formatCallyDateRange,
@@ -19,12 +19,7 @@ import {
localizeContext,
} from "../../data/context";
import { TimeZone } from "../../data/translation";
import { MobileAwareMixin } from "../../mixins/mobile-aware-mixin";
import { haStyleScrollbar } from "../../resources/styles";
import type { ValueChangedEvent } from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-filter-chip";
import type { HaFilterChip } from "../chips/ha-filter-chip";
import type { HaBaseTimeInput } from "../ha-base-time-input";
import "../ha-icon-button";
import "../ha-icon-button-next";
@@ -32,12 +27,11 @@ import "../ha-icon-button-prev";
import "../ha-list";
import "../ha-list-item";
import "../ha-time-input";
import type { HaTimeInput } from "../ha-time-input";
import type { DateRangePickerRanges } from "./ha-date-range-picker";
import { datePickerStyles, dateRangePickerStyles } from "./styles";
@customElement("date-range-picker")
export class DateRangePicker extends MobileAwareMixin(LitElement) {
export class DateRangePicker extends LitElement {
@property({ attribute: false }) public ranges?: DateRangePickerRanges | false;
@property({ attribute: false }) public startDate?: Date;
@@ -75,8 +69,6 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
to: { hours: 23, minutes: 59 },
};
@queryAll("ha-time-input") private _timeInputs?: NodeListOf<HaTimeInput>;
public connectedCallback() {
super.connectedCallback();
@@ -108,38 +100,16 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
}
}
private _renderRanges() {
if (this._isMobileSize) {
return html`
<ha-chip-set class="ha-scrollbar">
${Object.entries(this.ranges!).map(
([name, range], index) => html`
<ha-filter-chip
.index=${index}
.range=${range}
@click=${this._clickDateRangeChip}
>
${name}
</ha-filter-chip>
`
)}
</ha-chip-set>
`;
}
return html`
<ha-list @action=${this._setDateRange} activatable>
${Object.keys(this.ranges!).map(
(name) => html`<ha-list-item>${name}</ha-list-item>`
)}
</ha-list>
`;
}
render() {
return html`<div class="picker">
${this.ranges !== false && this.ranges
? html`<div class="date-range-ranges">${this._renderRanges()}</div>`
? html`<div class="date-range-ranges">
<ha-list @action=${this._setDateRange} activatable>
${Object.keys(this.ranges).map(
(name) => html`<ha-list-item>${name}</ha-list-item>`
)}
</ha-list>
</div>`
: nothing}
<div class="range">
<calendar-range
@@ -183,7 +153,6 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
)}
id="from"
placeholder-labels
auto-validate
></ha-time-input>
<ha-time-input
.value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
@@ -194,7 +163,6 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
)}
id="to"
placeholder-labels
auto-validate
></ha-time-input>
</div>
`
@@ -232,14 +200,6 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
let endDate = new Date(`${dates[1]}T23:59:00`);
if (this.timePicker) {
const timeInputs = this._timeInputs;
if (
timeInputs &&
![...timeInputs].every((input) => input.reportValidity())
) {
// If we have time inputs, and they don't all report valid, don't save
return;
}
startDate.setHours(this._timeValue.from.hours);
startDate.setMinutes(this._timeValue.from.minutes);
endDate.setHours(this._timeValue.to.hours);
@@ -297,38 +257,31 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
this._focusDate = undefined;
}
private _clickDateRangeChip(ev: Event) {
const chip = ev.target as HaFilterChip & {
index: number;
range: [Date, Date];
};
this._saveDateRangePreset(chip.range, chip.index);
}
private _setDateRange(ev: CustomEvent<ActionDetail>) {
const dateRange: [Date, Date] = Object.values(this.ranges!)[
ev.detail.index
];
this._saveDateRangePreset(dateRange, ev.detail.index);
}
private _saveDateRangePreset(range: [Date, Date], index: number) {
this._dateValue = formatCallyDateRange(
dateRange[0],
dateRange[1],
this.locale,
this.hassConfig
);
fireEvent(this, "value-changed", {
value: {
startDate: range[0],
endDate: range[1],
startDate: dateRange[0],
endDate: dateRange[1],
},
});
fireEvent(this, "preset-selected", {
index,
index: ev.detail.index,
});
}
private _handleChangeTime(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const time = ev.detail.value;
const target = ev.target as HaBaseTimeInput;
const type = target.id;
const type = (ev.target as HaBaseTimeInput).id;
if (time) {
if (!this._timeValue) {
this._timeValue = {
@@ -345,48 +298,20 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
static styles = [
datePickerStyles,
dateRangePickerStyles,
haStyleScrollbar,
css`
.picker {
display: flex;
flex-direction: row;
}
.date-range-ranges {
border-right: var(--ha-border-width-sm) solid var(--divider-color);
min-width: 140px;
flex: 0 1 30%;
border-right: 1px solid var(--divider-color);
}
.range {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
padding: var(--ha-space-3);
overflow-x: hidden;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.picker {
flex-direction: column;
}
.date-range-ranges {
border-bottom: 1px solid var(--divider-color);
margin-top: var(--ha-space-5);
overflow: visible;
}
ha-chip-set {
padding: var(--ha-space-3);
flex-wrap: nowrap;
overflow-x: auto;
}
.range {
flex-basis: fit-content;
}
}
.times {
@@ -401,6 +326,12 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
padding: var(--ha-space-2);
border-top: 1px solid var(--divider-color);
}
@media only screen and (max-width: 500px) {
.date-range-ranges {
max-width: 30%;
}
}
`,
];
}
+27
View File
@@ -80,6 +80,33 @@ export const datePickerStyles = css`
text-align: center;
margin-left: 48px;
}
@media only screen and (max-width: 500px) {
calendar-month {
min-height: calc(34px * 7);
}
calendar-month::part(day) {
font-size: var(--ha-font-size-s);
}
calendar-month::part(button) {
height: 26px;
width: 26px;
}
calendar-month::part(range-inner),
calendar-month::part(range-start),
calendar-month::part(range-end),
calendar-month::part(selected),
calendar-month::part(selected):hover {
height: 34px;
width: 34px;
}
.heading {
font-size: var(--ha-font-size-s);
}
.month-year {
margin-left: 40px;
}
}
`;
export const dateRangePickerStyles = css`
+3 -3
View File
@@ -6,7 +6,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { getDeviceArea } from "../../common/entity/context/get_device_context";
import { getDeviceContext } from "../../common/entity/context/get_device_context";
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
import {
deviceComboBoxKeys,
@@ -14,11 +14,11 @@ import {
type DevicePickerItem,
} from "../../data/device/device_picker";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
@@ -154,7 +154,7 @@ export class HaDevicePicker extends LitElement {
return html`<span slot="headline">${deviceId}</span>`;
}
const area = getDeviceArea(device, this.hass.areas);
const { area } = getDeviceContext(device, this.hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
+1 -1
View File
@@ -94,7 +94,7 @@ class HaAddonPicker extends LitElement {
private async _getApps() {
try {
if (isComponentLoaded(this.hass.config, "hassio")) {
if (isComponentLoaded(this.hass, "hassio")) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = addonsInfo.addons
.filter((addon) => addon.version)
+1 -4
View File
@@ -8,7 +8,6 @@ import {
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { LocalizeFunc } from "../common/translations/localize";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-icon-button";
import "./ha-svg-icon";
@@ -39,8 +38,6 @@ class HaAlert extends LitElement {
@property({ type: Boolean }) public dismissable = false;
@property({ attribute: false }) public localize?: LocalizeFunc;
@property({ type: Boolean }) public narrow = false;
public render() {
@@ -68,7 +65,7 @@ class HaAlert extends LitElement {
${this.dismissable
? html`<ha-icon-button
@click=${this._dismissClicked}
.label=${this.localize!("ui.common.dismiss_alert")}
label="Dismiss alert"
.path=${mdiClose}
></ha-icon-button>`
: nothing}
@@ -267,6 +267,7 @@ export class HaAreaControlsPicker extends LitElement {
: item.domain
? html`<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.domain}
.deviceClass=${item.deviceClass}
></ha-domain-icon>`
+3 -5
View File
@@ -137,7 +137,7 @@ export class HaBaseTimeInput extends LitElement {
@property({ attribute: "placeholder-labels", type: Boolean })
public placeholderLabels = false;
@queryAll("ha-input") private _inputs?: NodeListOf<HaInput>;
@queryAll("ha-input") private _inputs?: HaInput[];
static shadowRootOptions = {
...LitElement.shadowRootOptions,
@@ -145,9 +145,7 @@ export class HaBaseTimeInput extends LitElement {
};
public reportValidity(): boolean {
const inputs = this._inputs;
if (!inputs) return true;
return [...inputs].every((input) => input.reportValidity());
return this._inputs?.every((input) => input.reportValidity()) ?? true;
}
protected render(): TemplateResult {
@@ -401,7 +399,7 @@ export class HaBaseTimeInput extends LitElement {
.time-separator,
ha-icon-button {
background-color: var(--ha-color-form-background);
background-color: var(--ha-color-fill-neutral-quiet-resting);
color: var(--ha-color-text-secondary);
border-bottom: 1px solid var(--ha-color-border-neutral-loud);
box-sizing: border-box;
+2
View File
@@ -79,6 +79,7 @@ class HaConfigEntryPicker extends LitElement {
<span slot="supporting-text">${item.secondary}</span>
<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.icon!}
brand-fallback
></ha-domain-icon>
@@ -114,6 +115,7 @@ class HaConfigEntryPicker extends LitElement {
slot="headline"
>${item?.icon
? html`<ha-domain-icon
.hass=${this.hass}
.domain=${item.icon!}
brand-fallback
></ha-domain-icon>`
-3
View File
@@ -123,9 +123,6 @@ export class HaDateInput extends LitElement {
}
static styles = css`
:host {
min-width: 0px;
}
ha-svg-icon {
color: var(--secondary-text-color);
}
+8 -29
View File
@@ -1,23 +1,19 @@
import { consume, type ContextType } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import {
authContext,
configContext,
connectionContext,
themesContext,
} from "../data/context";
import {
DEFAULT_DOMAIN_ICON,
domainIcon,
FALLBACK_DOMAIN_ICONS,
} from "../data/icons";
import type { HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import "./ha-icon";
@customElement("ha-domain-icon")
export class HaDomainIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public domain?: string;
@property({ attribute: false }) public deviceClass?: string;
@@ -29,22 +25,6 @@ export class HaDomainIcon extends LitElement {
@property({ attribute: "brand-fallback", type: Boolean })
public brandFallback?: boolean;
@state()
@consume({ context: configContext, subscribe: true })
private _hassConfig?: ContextType<typeof configContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: ContextType<typeof connectionContext>;
@state()
@consume({ context: themesContext, subscribe: true })
private _themes?: ContextType<typeof themesContext>;
@state()
@consume({ context: authContext, subscribe: true })
private _auth?: ContextType<typeof authContext>;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -54,13 +34,12 @@ export class HaDomainIcon extends LitElement {
return nothing;
}
if (!this._connection || !this._hassConfig) {
if (!this.hass) {
return this._renderFallback();
}
const icon = domainIcon(
this._connection,
this._hassConfig,
this.hass,
this.domain,
this.deviceClass,
this.state
@@ -86,9 +65,9 @@ export class HaDomainIcon extends LitElement {
{
domain: this.domain!,
type: "icon",
darkOptimized: this._themes?.darkMode,
darkOptimized: this.hass.themes?.darkMode,
},
this._auth?.data.hassUrl
this.hass.auth.data.hassUrl
);
return html`
<img
-3
View File
@@ -100,9 +100,6 @@ export class HaDropdown extends Dropdown {
#menu {
padding: var(--ha-space-1);
}
wa-popup::part(popup) {
z-index: 200;
}
`,
];
}
+4 -12
View File
@@ -101,11 +101,7 @@ export class HaFilterDevices extends LitElement {
.value=${device.id}
.selected=${this.value?.includes(device.id) ?? false}
>
${computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)}
${computeDeviceNameDisplay(device, this.hass)}
</ha-check-list-item>`;
private _handleItemClick(ev) {
@@ -155,18 +151,14 @@ export class HaFilterDevices extends LitElement {
.filter(
(device) =>
!filter ||
computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)
computeDeviceNameDisplay(device, this.hass)
.toLowerCase()
.includes(filter)
)
.sort((a, b) =>
stringCompare(
computeDeviceNameDisplay(a, this.hass.localize, this.hass.states),
computeDeviceNameDisplay(b, this.hass.localize, this.hass.states),
computeDeviceNameDisplay(a, this.hass),
computeDeviceNameDisplay(b, this.hass),
this.hass.locale.language
)
);
+1
View File
@@ -72,6 +72,7 @@ export class HaFilterDomains extends LitElement {
>
<ha-domain-icon
slot="graphic"
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>
+1
View File
@@ -82,6 +82,7 @@ export class HaFilterIntegrations extends LitElement {
>
<ha-domain-icon
slot="graphic"
.hass=${this.hass}
.domain=${integration.domain}
brand-fallback
></ha-domain-icon>
+13 -4
View File
@@ -6,6 +6,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { navigate } from "../common/navigate";
import { stringCompare } from "../common/string/compare";
@@ -97,14 +98,17 @@ export class HaFilterLabels extends LitElement {
this.value
),
(label) => label.label_id,
(label) =>
html`<ha-check-list-item
(label) => {
const color = label.color
? computeCssColor(label.color)
: undefined;
return html`<ha-check-list-item
.value=${label.label_id}
.selected=${(this.value || []).includes(label.label_id)}
hasMeta
>
<ha-label
.color=${label.color}
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon
@@ -115,7 +119,8 @@ export class HaFilterLabels extends LitElement {
: nothing}
${label.name}
</ha-label>
</ha-check-list-item>`
</ha-check-list-item>`;
}
)}
</ha-list> `
: nothing}
@@ -248,6 +253,10 @@ export class HaFilterLabels extends LitElement {
.warning {
color: var(--error-color);
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
.add {
position: absolute;
bottom: 0;
+1 -3
View File
@@ -100,9 +100,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
inputMode="numeric"
.label=${this.label}
.hint=${this.helper}
.value=${this.data !== undefined && this.data !== null
? this.data.toString()
: ""}
.value=${this.data !== undefined ? this.data.toString() : ""}
.disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
+78 -108
View File
@@ -1,4 +1,4 @@
import type { PropertyValues } from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, LitElement, svg } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
@@ -54,7 +54,6 @@ export class HaGauge extends LitElement {
this._angle = getAngle(this.value, this.min, this.max);
}
this._segment_label = this._getSegmentLabel();
this._rescaleSvg();
});
}
@@ -71,7 +70,6 @@ export class HaGauge extends LitElement {
}
this._angle = getAngle(this.value, this.min, this.max);
this._segment_label = this._getSegmentLabel();
this._rescaleSvg();
}
protected render() {
@@ -90,91 +88,87 @@ export class HaGauge extends LitElement {
/>
${
this.levels
? (() => {
const sortedLevels = [...this.levels].sort(
(a, b) => a.level - b.level
);
${
this.levels
? [...this.levels]
.sort((a, b) => a.level - b.level)
.map((level, i, arr) => {
const startLevel = i === 0 ? this.min : arr[i].level;
const endLevel = i + 1 < arr.length ? arr[i + 1].level : this.max;
if (
sortedLevels.length > 0 &&
sortedLevels[0].level !== this.min
) {
sortedLevels.unshift({
level: this.min,
stroke: "var(--info-color)",
});
}
const startAngle = getAngle(startLevel, this.min, this.max);
const endAngle = getAngle(endLevel, this.min, this.max);
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
return sortedLevels.map((level, i, arr) => {
const startLevel = level.level;
const endLevel =
i + 1 < arr.length ? arr[i + 1].level : this.max;
const x1 = -arcRadius * Math.cos((startAngle * Math.PI) / 180);
const y1 = -arcRadius * Math.sin((startAngle * Math.PI) / 180);
const x2 = -arcRadius * Math.cos((endAngle * Math.PI) / 180);
const y2 = -arcRadius * Math.sin((endAngle * Math.PI) / 180);
const startAngle = getAngle(startLevel, this.min, this.max);
const endAngle = getAngle(endLevel, this.min, this.max);
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
const firstSegment = i === 0;
const lastSegment = i === arr.length - 1;
const x1 =
-arcRadius * Math.cos((startAngle * Math.PI) / 180);
const y1 =
-arcRadius * Math.sin((startAngle * Math.PI) / 180);
const x2 = -arcRadius * Math.cos((endAngle * Math.PI) / 180);
const y2 = -arcRadius * Math.sin((endAngle * Math.PI) / 180);
const paths: TemplateResult[] = [];
const isFirst = i === 0;
const isLast = i === arr.length - 1;
if (firstSegment) {
paths.push(svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: round"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
/>
`);
} else if (lastSegment) {
const offsetAngle = 0.5;
const midAngle = endAngle - offsetAngle;
const xm = -arcRadius * Math.cos((midAngle * Math.PI) / 180);
const ym = -arcRadius * Math.sin((midAngle * Math.PI) / 180);
if (isFirst) {
return svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: butt"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
/>
`;
}
paths.push(svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: butt"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${xm} ${ym}"
/>
`);
if (isLast) {
const offsetAngle = 0.5;
const midAngle = endAngle - offsetAngle;
const xm =
-arcRadius * Math.cos((midAngle * Math.PI) / 180);
const ym =
-arcRadius * Math.sin((midAngle * Math.PI) / 180);
paths.push(svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: round"
d="M ${xm} ${ym} A ${arcRadius} ${arcRadius} 0 0 1 ${x2} ${y2}"
/>
`);
} else {
paths.push(svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: butt"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
/>
`);
}
return svg`
<path class="level" stroke="${level.stroke}" style="stroke-linecap: butt"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${xm} ${ym}" />
<path class="level" stroke="${level.stroke}" style="stroke-linecap: butt"
d="M ${xm} ${ym} A ${arcRadius} ${arcRadius} 0 0 1 ${x2} ${y2}" />
`;
}
return svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: butt"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
></path>
`;
});
})()
: ""
}
return paths;
})
: ""
}
${
this.needle
? svg`
<path
class="needle"
d="M -36,-2 L -44,-1 A 1,1,0,0,0,-44,1 L -36,2 A 2,2,0,0,0,-36,-2 Z"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
/>
<line
class="needle"
x1="-35.0"
y1="0"
x2="-45.0"
y2="0"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
/>
`
: svg`
<path
@@ -185,8 +179,7 @@ export class HaGauge extends LitElement {
/>
`
}
</svg>
<svg class="text">
<text
class="value-text"
x="0"
@@ -211,18 +204,6 @@ export class HaGauge extends LitElement {
`;
}
private _rescaleSvg() {
// Set the viewbox of the SVG containing the value to perfectly
// fit the text
// That way it will auto-scale correctly
const svgRoot = this.shadowRoot!.querySelector(".text")!;
const box = svgRoot.querySelector("text")!.getBBox()!;
svgRoot.setAttribute(
"viewBox",
`${box.x} ${box.y} ${box.width} ${box.height}`
);
}
private _getSegmentLabel() {
if (this.levels) {
[...this.levels].sort((a, b) => a.level - b.level);
@@ -243,43 +224,32 @@ export class HaGauge extends LitElement {
.levels-base {
fill: none;
stroke: var(--primary-background-color);
stroke-width: 6;
stroke-linecap: butt;
stroke-width: 8;
stroke-linecap: round;
}
.level {
fill: none;
stroke-width: 6;
stroke-width: 8;
stroke-linecap: butt;
}
.value {
fill: none;
stroke-width: 6;
stroke-width: 8;
stroke: var(--gauge-color);
stroke-linecap: butt;
stroke-linecap: round;
transition: stroke-dashoffset 1s ease 0s;
}
.needle {
fill: var(--primary-text-color);
stroke: var(--card-background-color);
color: var(--primary-text-color);
stroke-width: 1;
stroke: var(--primary-text-color);
stroke-width: 2;
stroke-linecap: round;
transform-origin: 0 0;
transition: all 1s ease 0s;
}
.text {
position: absolute;
max-height: 40%;
max-width: 55%;
left: 50%;
bottom: 10%;
transform: translate(-50%, 0%);
}
.value-text {
font-size: var(--ha-font-size-l);
fill: var(--primary-text-color);
+1 -1
View File
@@ -140,7 +140,7 @@ class HaHLSPlayer extends LitElement {
this._cleanUp();
this._resetError();
if (!isComponentLoaded(this.hass.config, "stream")) {
if (!isComponentLoaded(this.hass!, "stream")) {
this._setFatalError("Streaming component is not loaded.");
return;
}
+5 -27
View File
@@ -1,44 +1,18 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { computeCssColor } from "../common/color/compute-color";
import { getContrastedColorHex } from "../common/color/rgb";
import { uid } from "../common/util/uid";
import "./ha-tooltip";
/**
* Returns CSS styles for a label's background & icon/text
* @param color Label color defined in HEX format
* @returns CSS styles
*/
export const getLabelColorStyle = (labelColor: string | undefined | null) => {
const color = labelColor ? computeCssColor(labelColor) : undefined;
return color
? `--ha-label-background-color: ${color};
--primary-text-color: ${getContrastedColorHex(labelColor!)};`
: `--ha-label-background-color: rgba(var(--rgb-primary-text-color), 0.15);`;
};
@customElement("ha-label")
class HaLabel extends LitElement {
@property({ type: Boolean, reflect: true }) dense = false;
@property()
public color?: string;
@property({ attribute: "description" })
public description?: string;
private _elementId = "label-" + uid();
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
if (!changedProps.has("color")) {
return;
}
this.style.cssText = getLabelColorStyle(this.color);
}
protected render(): TemplateResult {
return html`
<ha-tooltip
@@ -62,6 +36,10 @@ class HaLabel extends LitElement {
:host {
--ha-label-text-color: var(--primary-text-color);
--ha-label-icon-color: var(--primary-text-color);
--ha-label-background-color: rgba(
var(--rgb-primary-text-color),
0.15
);
--ha-label-background-opacity: 1;
border: 1px solid var(--outline-color);
position: relative;
+7 -11
View File
@@ -6,6 +6,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import { labelsContext } from "../data/context";
@@ -16,7 +17,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./chips/ha-chip-set";
import "./chips/ha-input-chip";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import { getLabelColorStyle } from "./ha-label";
import "./ha-label-picker";
import type { HaLabelPicker } from "./ha-label-picker";
import "./ha-tooltip";
@@ -106,14 +106,9 @@ export class HaLabelsPicker extends LitElement {
labels?.find((label) => label.label_id === id) || {
label_id: id,
name: id,
color: "rgba(var(--rgb-primary-text-color), 0.15)",
}
)
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
.map((label) => ({
...label,
style: getLabelColorStyle(label.color),
}))
);
protected render(): TemplateResult {
@@ -140,6 +135,9 @@ export class HaLabelsPicker extends LitElement {
(label) => label?.label_id,
(label) => {
if (!label) return nothing;
const color = label.color
? computeCssColor(label.color)
: undefined;
const elementId = "label-" + label.label_id;
return html`
<ha-tooltip
@@ -156,7 +154,7 @@ export class HaLabelsPicker extends LitElement {
.disabled=${this.disabled}
.label=${label.name}
selected
style=${label.style}
style=${color ? `--color: ${color}` : ""}
>
${label.icon
? html`<ha-icon
@@ -241,10 +239,8 @@ export class HaLabelsPicker extends LitElement {
height: var(--ha-space-8);
}
ha-input-chip {
--md-input-chip-selected-container-color: var(
--ha-label-background-color,
var(--grey-color)
);
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
--ha-input-chip-selected-container-opacity: 0.5;
--md-input-chip-selected-outline-width: 1px;
}
label {
+2 -2
View File
@@ -14,9 +14,9 @@ import {
} from "../data/supervisor/mounts";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-list-item";
import "./ha-select";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
const _BACKUP_DATA_DISK_ = "/backup";
@@ -129,7 +129,7 @@ class HaMountPicker extends LitElement {
private async _getMounts() {
try {
if (isComponentLoaded(this.hass.config, "hassio")) {
if (isComponentLoaded(this.hass, "hassio")) {
this._mounts = await fetchSupervisorMounts(this.hass);
if (this.usage === SupervisorMountUsage.BACKUP && !this.value) {
this.value = this._mounts.default_backup_mount || _BACKUP_DATA_DISK_;
+2
View File
@@ -132,6 +132,7 @@ export class HaNavigationPicker extends LitElement {
? html`
<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.domain}
brand-fallback
></ha-domain-icon>
@@ -157,6 +158,7 @@ export class HaNavigationPicker extends LitElement {
? html`
<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.domain}
brand-fallback
></ha-domain-icon>
+8 -4
View File
@@ -798,11 +798,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
}
ha-input-search {
padding: 0 var(--ha-space-3) var(--ha-space-3);
padding: 0 var(--ha-space-3);
}
:host([mode="dialog"]) ha-input-search {
padding: 0 var(--ha-space-4) var(--ha-space-3);
padding: 0 var(--ha-space-4);
}
ha-combo-box-item {
@@ -873,12 +873,12 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
display: flex;
flex-wrap: nowrap;
gap: var(--ha-space-2);
padding: 0 var(--ha-space-3) var(--ha-space-3);
padding: var(--ha-space-3) var(--ha-space-3);
overflow: auto;
}
:host([mode="dialog"]) .sections {
padding: 0 var(--ha-space-4) var(--ha-space-3);
padding: var(--ha-space-3) var(--ha-space-4);
}
.sections ha-filter-chip {
@@ -915,6 +915,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
padding: var(--ha-space-1) var(--ha-space-4);
}
:host([mode="dialog"]) ha-input-search {
padding: 0 var(--ha-space-4);
}
.section-title-wrapper {
height: 0;
position: relative;
+4 -1
View File
@@ -153,7 +153,10 @@ export class HaPickerField extends PickerMixin(LitElement) {
right: 0;
height: 1px;
width: 100%;
background-color: var(--ha-color-border-neutral-loud);
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
@@ -287,9 +287,7 @@ export class HaNumericThresholdSelector extends LitElement {
const numberSelector = {
number: {
...this.selector.numeric_threshold?.number,
...(!showUnit && effectiveUnit
? { unit_of_measurement: effectiveUnit }
: {}),
...(effectiveUnit ? { unit_of_measurement: effectiveUnit } : {}),
},
};
const entitySelector = {
@@ -1,144 +0,0 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { PeriodKey, PeriodSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import { deepEqual } from "../../common/util/deep-equal";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-form/ha-form";
const PERIODS = {
none: undefined,
today: { calendar: { period: "day" } },
yesterday: { calendar: { period: "day", offset: -1 } },
tomorrow: { calendar: { period: "day", offset: 1 } },
this_week: { calendar: { period: "week" } },
last_week: { calendar: { period: "week", offset: -1 } },
next_week: { calendar: { period: "week", offset: 1 } },
this_month: { calendar: { period: "month" } },
last_month: { calendar: { period: "month", offset: -1 } },
next_month: { calendar: { period: "month", offset: 1 } },
this_year: { calendar: { period: "year" } },
last_year: { calendar: { period: "year", offset: -1 } },
next_7d: { calendar: { period: "day", offset: 7 } },
next_30d: { calendar: { period: "day", offset: 30 } },
} as const;
@customElement("ha-selector-period")
export class HaPeriodSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: PeriodSelector;
@property({ attribute: false }) public value?: unknown;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
private _schema = memoizeOne(
(
selectedPeriodKey: PeriodKey | undefined,
selector: PeriodSelector,
localize: LocalizeFunc
) =>
[
{
name: "period",
required: this.required,
selector:
selectedPeriodKey && selectedPeriodKey in this._periods(selector)
? {
select: {
multiple: false,
options: Object.keys(this._periods(selector)).map(
(periodKey) => ({
value: periodKey,
label:
localize(
`ui.components.selectors.period.periods.${periodKey as PeriodKey}`
) || periodKey,
})
),
},
}
: { object: {} },
},
] as const
);
protected render() {
const data = this._data(this.value, this.selector);
const schema = this._schema(
typeof data.period === "string" ? (data.period as PeriodKey) : undefined,
this.selector,
this.hass.localize
);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeHelper=${this._computeHelperCallback}
.computeLabel=${this._computeLabelCallback}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _periods = memoizeOne((selector: PeriodSelector) =>
Object.fromEntries(
Object.entries(PERIODS).filter(([key]) =>
selector.period?.options?.includes(key as any)
)
)
);
private _data = memoizeOne((value: unknown, selector: PeriodSelector) => {
for (const [periodKey, period] of Object.entries(this._periods(selector))) {
if (deepEqual(period, value)) {
return { period: periodKey };
}
}
return { period: value };
});
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (typeof newValue.period === "string") {
const periods = this._periods(this.selector);
if (newValue.period in periods) {
const period = this._periods(this.selector)[newValue.period];
fireEvent(this, "value-changed", { value: period });
}
} else {
fireEvent(this, "value-changed", { value: newValue.period });
}
}
private _computeHelperCallback = () => this.helper;
private _computeLabelCallback = () => this.label;
static styles = css`
:host {
position: relative;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-period": HaPeriodSelector;
}
}
@@ -41,7 +41,6 @@ const LOAD_ELEMENTS = {
number: () => import("./ha-selector-number"),
numeric_threshold: () => import("./ha-selector-numeric-threshold"),
object: () => import("./ha-selector-object"),
period: () => import("./ha-selector-period"),
qr_code: () => import("./ha-selector-qr-code"),
select: () => import("./ha-selector-select"),
selector: () => import("./ha-selector-selector"),
+8 -1
View File
@@ -516,10 +516,17 @@ export class HaServiceControl extends LitElement {
`}
${serviceData && "target" in serviceData
? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""}
<span slot="heading"
>${this.hass.localize("ui.components.service-control.target")}</span
>
<ha-selector
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(
serviceData.target as TargetSelector,
+3 -15
View File
@@ -1,6 +1,5 @@
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-settings-row")
@@ -17,28 +16,17 @@ export class HaSettingsRow extends LitElement {
@property({ type: Boolean, reflect: true }) public empty = false;
private readonly _hasSlotController = new HasSlotController(
this,
"description"
);
protected render(): TemplateResult {
const hasDescription = this._hasSlotController.test("description");
return html`
<div class="prefix-wrap">
<slot name="prefix"></slot>
<div
class="body"
?two-line=${!this.threeLine && hasDescription}
?two-line=${!this.threeLine}
?three-line=${this.threeLine}
>
<slot name="heading"></slot>
${hasDescription
? html`<span class="secondary"
><slot name="description"></slot
></span>`
: nothing}
<div class="secondary"><slot name="description"></slot></div>
</div>
</div>
<div class="content">
+7 -7
View File
@@ -1,19 +1,19 @@
import { mdiStarFourPoints } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { html, css, LitElement, nothing } from "lit";
import { mdiStarFourPoints } from "@mdi/js";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { customElement, state, property } from "lit/decorators";
import type {
AITaskPreferences,
GenDataTask,
GenDataTaskResult,
} from "../data/ai_task";
import { fetchAITaskPreferences, generateDataAITask } from "../data/ai_task";
import type { HomeAssistant } from "../types";
import "./chips/ha-assist-chip";
import "./ha-svg-icon";
import type { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import { isComponentLoaded } from "../common/config/is_component_loaded";
declare global {
interface HASSDomEvents {
@@ -56,7 +56,7 @@ export class HaSuggestWithAIButton extends LitElement {
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
if (!this.hass || !isComponentLoaded(this.hass.config, "ai_task")) {
if (!this.hass || !isComponentLoaded(this.hass, "ai_task")) {
return;
}
fetchAITaskPreferences(this.hass).then((prefs) => {
+294
View File
@@ -0,0 +1,294 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import "./input/ha-input";
import type { HaInput } from "./input/ha-input";
/**
* Legacy wrapper around ha-input that preserves the mwc-textfield API.
* New code should use ha-input directly.
* @deprecated Use ha-input instead.
*/
@customElement("ha-textfield")
export class HaTextField extends LitElement {
@property({ type: String })
public value = "";
@property({ type: String })
public type:
| "text"
| "search"
| "tel"
| "url"
| "email"
| "password"
| "date"
| "month"
| "week"
| "time"
| "datetime-local"
| "number"
| "color" = "text";
@property({ type: String })
public label = "";
@property({ type: String })
public placeholder = "";
@property({ type: String })
public prefix = "";
@property({ type: String })
public suffix = "";
@property({ type: Boolean })
// @ts-ignore
public icon = false;
@property({ type: Boolean })
// @ts-ignore
// eslint-disable-next-line lit/attribute-names
public iconTrailing = false;
@property({ type: Boolean })
public disabled = false;
@property({ type: Boolean })
public required = false;
@property({ type: Number, attribute: "minlength" })
public minLength = -1;
@property({ type: Number, attribute: "maxlength" })
public maxLength = -1;
@property({ type: Boolean, reflect: true })
public outlined = false;
@property({ type: String })
public helper = "";
@property({ type: Boolean, attribute: "validateoninitialrender" })
public validateOnInitialRender = false;
@property({ type: String, attribute: "validationmessage" })
public validationMessage = "";
@property({ type: Boolean, attribute: "autovalidate" })
public autoValidate = false;
@property({ type: String })
public pattern = "";
@property()
public min: number | string = "";
@property()
public max: number | string = "";
@property()
public step: number | "any" | null = null;
@property({ type: Number })
public size: number | null = null;
@property({ type: Boolean, attribute: "helperpersistent" })
public helperPersistent = false;
@property({ attribute: "charcounter" })
public charCounter: boolean | "external" | "internal" = false;
@property({ type: Boolean, attribute: "endaligned" })
public endAligned = false;
@property({ type: String, attribute: "inputmode" })
public inputMode = "";
@property({ type: Boolean, reflect: true, attribute: "readonly" })
public readOnly = false;
@property({ type: String })
public name = "";
@property({ type: String })
// eslint-disable-next-line lit/no-native-attributes
public autocapitalize = "";
// --- ha-textfield-specific properties ---
@property({ type: Boolean })
public invalid = false;
@property({ attribute: "error-message" })
public errorMessage?: string;
@property()
public autocomplete?: string;
@property({ type: Boolean })
public autocorrect = true;
@property({ attribute: "input-spellcheck" })
public inputSpellcheck?: string;
@query("ha-input")
private _haInput?: HaInput;
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
public get formElement(): HTMLInputElement | undefined {
return (this._haInput as any)?._input?.input;
}
public select(): void {
this._haInput?.select();
}
public setSelectionRange(
selectionStart: number,
selectionEnd: number,
selectionDirection?: "forward" | "backward" | "none"
): void {
this._haInput?.setSelectionRange(
selectionStart,
selectionEnd,
selectionDirection
);
}
public setRangeText(
replacement: string,
start?: number,
end?: number,
selectMode?: "select" | "start" | "end" | "preserve"
): void {
this._haInput?.setRangeText(replacement, start, end, selectMode);
}
public checkValidity(): boolean {
return this._haInput?.checkValidity() ?? true;
}
public reportValidity(): boolean {
return this._haInput?.reportValidity() ?? true;
}
public setCustomValidity(message: string): void {
this.validationMessage = message;
this.invalid = !!message;
}
/** No-op. Preserved for backward compatibility. */
public layout(): void {
// no-op — mwc-textfield needed this for notched outline recalculation
}
protected override firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.validateOnInitialRender) {
this.reportValidity();
}
}
protected override updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("invalid") && this._haInput) {
if (
this.invalid ||
(changedProperties.get("invalid") !== undefined && !this.invalid)
) {
this.reportValidity();
}
}
}
protected override render(): TemplateResult {
const errorMsg = this.errorMessage || this.validationMessage;
return html`
<ha-input
.type=${this.type}
.value=${this.value || undefined}
.label=${this.label}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.required=${this.required}
.readonly=${this.readOnly}
.pattern=${this.pattern || undefined}
.minlength=${this.minLength > 0 ? this.minLength : undefined}
.maxlength=${this.maxLength > 0 ? this.maxLength : undefined}
.min=${this.min !== "" ? this.min : undefined}
.max=${this.max !== "" ? this.max : undefined}
.step=${this.step ?? undefined}
.name=${this.name || undefined}
.autocomplete=${this.autocomplete}
.autocorrect=${this.autocorrect}
.spellcheck=${this.inputSpellcheck === "true"}
.inputmode=${this.inputMode}
.autocapitalize=${this.autocapitalize || ""}
.invalid=${this.invalid}
.validationMessage=${errorMsg || ""}
.autoValidate=${this.autoValidate}
.hint=${this.helper}
.withoutSpinButtons=${this.type === "number"}
.insetLabel=${this.prefix}
@input=${this._onInput}
@change=${this._onChange}
>
${this.icon
? html`<slot name="leadingIcon" slot="start"></slot>`
: nothing}
${this.prefix
? html`<span class="prefix" slot="start">${this.prefix}</span>`
: nothing}
${this.suffix
? html`<span class="suffix" slot="end">${this.suffix}</span>`
: nothing}
${this.iconTrailing
? html`<slot name="trailingIcon" slot="end"></slot>`
: nothing}
</ha-input>
`;
}
private _onInput(): void {
this.value = this._haInput?.value ?? "";
}
private _onChange(): void {
this.value = this._haInput?.value ?? "";
}
static override styles = css`
:host {
display: inline-flex;
flex-direction: column;
outline: none;
}
ha-input {
--ha-input-padding-bottom: 0;
width: 100%;
}
.prefix,
.suffix {
color: var(--secondary-text-color);
}
.prefix {
padding-top: var(--ha-space-3);
margin-inline-end: var(--text-field-prefix-padding-right);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-textfield": HaTextField;
}
}
-17
View File
@@ -21,8 +21,6 @@ export class HaTimeInput extends LitElement {
@property({ type: Boolean }) public required = false;
@property({ attribute: "auto-validate", type: Boolean }) autoValidate = false;
@property({ type: Boolean, attribute: "enable-second" })
public enableSecond = false;
@@ -73,7 +71,6 @@ export class HaTimeInput extends LitElement {
.clearable=${this.clearable && this.value !== undefined}
.helper=${this.helper}
.placeholderLabels=${this.placeholderLabels}
.autoValidate=${this.autoValidate}
day-label="dd"
hour-label="hh"
min-label="mm"
@@ -89,7 +86,6 @@ export class HaTimeInput extends LitElement {
const useAMPM = useAmPm(this.locale);
let value: string | undefined;
let updateHours = 0;
// An undefined eventValue means the time selector is being cleared,
// the `value` variable will (intentionally) be left undefined.
@@ -101,8 +97,6 @@ export class HaTimeInput extends LitElement {
) {
let hours = eventValue.hours || 0;
if (eventValue && useAMPM) {
updateHours =
hours >= 12 && hours < 24 ? hours - 12 : hours === 0 ? 12 : 0;
if (eventValue.amPm === "PM" && hours < 12) {
hours += 12;
}
@@ -121,17 +115,6 @@ export class HaTimeInput extends LitElement {
}`;
}
if (updateHours) {
// If the user entered a 24hr time in a 12hr input, we need to refresh the
// input to ensure it resets back to the 12hr equivalent.
this.updateComplete.then(() => {
const input = this._input;
if (input) {
input.hours = updateHours;
}
});
}
if (value === this.value) {
return;
}
+19 -27
View File
@@ -1,12 +1,6 @@
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import {
customElement,
property,
query,
queryAssignedElements,
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { popoverSupported } from "../common/feature-detect/support-popover";
@@ -31,12 +25,6 @@ export class HaToast extends LitElement {
@query(".toast")
private _toast?: HTMLDivElement;
@queryAssignedElements({ slot: "action", flatten: true })
private _actionElements?: Element[];
@queryAssignedElements({ slot: "dismiss", flatten: true })
private _dismissElements?: Element[];
@state() private _active = false;
@state() private _visible = false;
@@ -175,10 +163,6 @@ export class HaToast extends LitElement {
}
protected render() {
const hasAction =
(this._actionElements?.length ?? 0) > 0 ||
(this._dismissElements?.length ?? 0) > 0;
return html`
<div
class=${classMap({
@@ -191,7 +175,7 @@ export class HaToast extends LitElement {
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
>
<span class="message">${this.labelText}</span>
<div class=${classMap({ actions: true, "has-action": hasAction })}>
<div class="actions">
<slot name="action"></slot>
<slot name="dismiss"></slot>
</div>
@@ -214,13 +198,21 @@ export class HaToast extends LitElement {
border: none;
overflow: hidden;
box-sizing: border-box;
min-width: min(350px, calc(var(--safe-width) - var(--ha-space-4)));
max-width: min(650px, var(--safe-width));
min-width: min(
350px,
calc(
100vw - var(--ha-space-4) - var(--safe-area-inset-left, 0px) - var(
--safe-area-inset-right,
0px
)
)
);
max-width: 650px;
min-height: 48px;
display: flex;
align-items: center;
gap: var(--ha-space-2);
padding: var(--ha-space-3) var(--ha-space-4);
padding: var(--ha-space-3);
color: var(--ha-color-on-neutral-loud);
background-color: var(--ha-color-neutral-10);
border-radius: var(--ha-border-radius-sm);
@@ -253,14 +245,14 @@ export class HaToast extends LitElement {
color: var(--ha-color-on-neutral-loud);
}
.actions:not(.has-action) {
display: none;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.toast {
min-width: var(--safe-width);
max-width: var(--safe-width);
min-width: calc(
100vw - var(--safe-area-inset-left, 0px) - var(
--safe-area-inset-right,
0px
)
);
border-radius: var(--ha-border-radius-square);
}
}
+4 -13
View File
@@ -19,7 +19,6 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
import "../ha-icon-button";
import "../ha-svg-icon";
import "../ha-tooltip";
import { nativeElementInternalsSupported } from "../../common/feature-detect/support-native-element-internals";
export type InputType =
| "date"
@@ -236,8 +235,7 @@ export class HaInput extends LitElement {
this,
"label",
"hint",
"input",
"start"
"input"
);
static shadowRootOptions: ShadowRootInit = {
@@ -289,9 +287,7 @@ export class HaInput extends LitElement {
}
public checkValidity(): boolean {
return nativeElementInternalsSupported
? (this._input?.checkValidity() ?? true)
: true;
return this._input?.checkValidity() ?? true;
}
public reportValidity(): boolean {
@@ -322,8 +318,6 @@ export class HaInput extends LitElement {
? false
: this._hasSlotController.test("hint");
const hasStartSlot = this._hasSlotController.test("start");
return html`
<wa-input
.type=${this.type}
@@ -354,8 +348,7 @@ export class HaInput extends LitElement {
invalid: this.invalid || this._invalid,
"label-raised":
(this.value !== undefined && this.value !== "") ||
(this.label && this.placeholder) ||
(hasStartSlot && this.insetLabel),
(this.label && this.placeholder),
"no-label": !this.label,
"hint-hidden":
!this.hint &&
@@ -596,7 +589,6 @@ export class HaInput extends LitElement {
}
:host([type="color"]) wa-input::part(input) {
padding-top: var(--ha-space-6);
padding-bottom: 2px;
cursor: pointer;
}
:host([type="color"]) wa-input.no-label::part(input) {
@@ -633,7 +625,7 @@ export class HaInput extends LitElement {
}
wa-input::part(hint) {
min-height: var(--ha-space-5);
height: var(--ha-space-5);
margin-block-start: 0;
margin-inline-start: var(--ha-space-3);
font-size: var(--ha-font-size-xs);
@@ -644,7 +636,6 @@ export class HaInput extends LitElement {
wa-input.hint-hidden::part(hint) {
height: 0;
min-height: 0;
}
.error {
@@ -1,10 +1,10 @@
import { animate } from "@lit-labs/motion";
import {
mdiCheckboxBlankOutline,
mdiCheckboxMarkedOutline,
mdiClose,
mdiDelete,
mdiCheckboxBlankOutline,
mdiCheckboxMarkedOutline,
} from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
@@ -26,8 +26,8 @@ import type { HomeAssistant } from "../../types";
import "../ha-button";
import "../ha-check-list-item";
import "../ha-dialog";
import "../ha-dialog-footer";
import "../ha-dialog-header";
import "../ha-dialog-footer";
import "../ha-icon-button";
import "../ha-list";
import "../ha-spinner";
@@ -227,7 +227,7 @@ class DialogMediaManage extends LitElement {
)}
</ha-list>
`}
${isComponentLoaded(this.hass.config, "hassio")
${isComponentLoaded(this.hass, "hassio")
? html`<ha-tip .hass=${this.hass}>
${this.hass.localize(
"ui.components.media-browser.file_management.tip_media_storage",
@@ -524,13 +524,7 @@ export class HaTargetPickerItemRow extends LitElement {
}
return {
name: device
? computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)
: item,
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
fallbackIconPath: mdiDevices,
notFound: !device,
@@ -161,13 +161,7 @@ export class HaTargetPickerValueChip extends LitElement {
}
return {
name: device
? computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)
: itemId,
name: device ? computeDeviceNameDisplay(device, this.hass) : itemId,
fallbackIconPath: mdiDevices,
};
}
+3 -25
View File
@@ -1,4 +1,3 @@
import "@home-assistant/webawesome/dist/components/skeleton/skeleton";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
@@ -13,8 +12,6 @@ import { customElement, property } from "lit/decorators";
* @slot primary - The primary text container.
* @slot secondary - The secondary text container.
*
* @property {boolean} secondaryLoading - Whether the secondary text is loading. Shows a skeleton placeholder.
*
* @cssprop --ha-tile-info-primary-font-size - The font size of the primary text. defaults to `var(--ha-font-size-m)`.
* @cssprop --ha-tile-info-primary-font-weight - The font weight of the primary text. defaults to `var(--ha-font-weight-medium)`.
* @cssprop --ha-tile-info-primary-line-height - The line height of the primary text. defaults to `var(--ha-line-height-normal)`.
@@ -32,31 +29,21 @@ export class HaTileInfo extends LitElement {
@property() public secondary?: string;
@property({ type: Boolean, attribute: "secondary-loading" })
public secondaryLoading = false;
protected render() {
return html`
<div class="info">
<slot name="primary" class="primary">
<span>${this.primary}</span>
</slot>
${this.secondaryLoading
? html`<div class="secondary">
<wa-skeleton class="placeholder" effect="pulse"></wa-skeleton>
</div>`
: html`<slot name="secondary" class="secondary">
<span>${this.secondary}</span>
</slot>`}
<slot name="secondary" class="secondary">
<span>${this.secondary}</span>
</slot>
</div>
`;
}
static styles = css`
:host {
display: block;
width: 100%;
min-width: 0;
--tile-info-primary-font-size: var(
--ha-tile-info-primary-font-size,
var(--ha-font-size-m)
@@ -125,15 +112,6 @@ export class HaTileInfo extends LitElement {
line-height: var(--tile-info-secondary-line-height);
letter-spacing: var(--tile-info-secondary-letter-spacing);
color: var(--tile-info-secondary-color);
width: 100%;
}
.placeholder {
width: 140px;
max-width: 100%;
height: var(--tile-info-secondary-font-size);
--wa-border-radius-pill: var(--ha-border-radius-sm);
--color: var(--ha-color-fill-neutral-normal-resting);
--sheen-color: var(--ha-color-fill-neutral-loud-resting);
}
`;
}
+1 -3
View File
@@ -32,9 +32,7 @@ export class HaTraceLogbook extends LitElement {
></hat-logbook-note>
`
: html`<div class="padded-box">
${this.hass.localize(
"ui.panel.config.automation.trace.path.no_logbook_entries"
)}
No Logbook entries found for this step.
</div>`;
}
+13 -26
View File
@@ -78,19 +78,6 @@ const localizeTimeString = (
}
};
const formatNumericLimitValue = (
hass: HomeAssistant,
value?: number | string
) => {
if (typeof value !== "string" || !isValidEntityId(value)) {
return value;
}
return hass.states[value]
? computeStateName(hass.states[value]) || value
: value;
};
export const describeTrigger = (
trigger: Trigger,
hass: HomeAssistant,
@@ -246,8 +233,8 @@ const describeLegacyTrigger = (
attribute: attribute,
entity: formatListWithOrs(hass.locale, entities),
numberOfEntities: entities.length,
above: formatNumericLimitValue(hass, trigger.above),
below: formatNumericLimitValue(hass, trigger.below),
above: trigger.above,
below: trigger.below,
duration: duration,
}
);
@@ -259,7 +246,7 @@ const describeLegacyTrigger = (
attribute: attribute,
entity: formatListWithOrs(hass.locale, entities),
numberOfEntities: entities.length,
above: formatNumericLimitValue(hass, trigger.above),
above: trigger.above,
duration: duration,
}
);
@@ -271,7 +258,7 @@ const describeLegacyTrigger = (
attribute: attribute,
entity: formatListWithOrs(hass.locale, entities),
numberOfEntities: entities.length,
below: formatNumericLimitValue(hass, trigger.below),
below: trigger.below,
duration: duration,
}
);
@@ -826,16 +813,16 @@ const describeLegacyTrigger = (
: trigger.entity_id;
let offsetChoice = "other";
let offset = "";
let offset: string | string[] = "";
if (trigger.offset) {
offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
const parts = trigger.offset.startsWith("-")
offset = trigger.offset.startsWith("-")
? trigger.offset.substring(1).split(":")
: trigger.offset.split(":");
const duration = {
hours: parts.length > 0 ? +parts[0] : 0,
minutes: parts.length > 1 ? +parts[1] : 0,
seconds: parts.length > 2 ? +parts[2] : 0,
hours: offset.length > 0 ? +offset[0] : 0,
minutes: offset.length > 1 ? +offset[1] : 0,
seconds: offset.length > 2 ? +offset[2] : 0,
};
offset = formatDurationLong(hass.locale, duration);
if (offset === "") {
@@ -1129,8 +1116,8 @@ const describeLegacyCondition = (
attribute,
entity,
numberOfEntities: entity_ids.length,
above: formatNumericLimitValue(hass, condition.above),
below: formatNumericLimitValue(hass, condition.below),
above: condition.above,
below: condition.below,
}
);
}
@@ -1141,7 +1128,7 @@ const describeLegacyCondition = (
attribute,
entity,
numberOfEntities: entity_ids.length,
above: formatNumericLimitValue(hass, condition.above),
above: condition.above,
}
);
}
@@ -1152,7 +1139,7 @@ const describeLegacyCondition = (
attribute,
entity,
numberOfEntities: entity_ids.length,
below: formatNumericLimitValue(hass, condition.below),
below: condition.below,
}
);
}
+3 -4
View File
@@ -1,7 +1,7 @@
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { getDeviceArea } from "../../common/entity/context/get_device_context";
import { getDeviceContext } from "../../common/entity/context/get_device_context";
import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
@@ -144,12 +144,11 @@ export const getDevices = (
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
const deviceName = computeDeviceNameDisplay(
device,
hass.localize,
hass.states,
hass,
deviceEntityLookup[device.id]
);
const area = getDeviceArea(device, hass.areas);
const { area } = getDeviceContext(device, hass);
const areaName = area ? computeAreaName(area) : undefined;
+1 -1
View File
@@ -13,7 +13,7 @@ export const fetchErrorLog = (hass: HomeAssistant) =>
hass.callApi<string>("GET", "error_log");
export const getErrorLogDownloadUrl = (hass: HomeAssistant) =>
isComponentLoaded(hass.config, "hassio") &&
isComponentLoaded(hass, "hassio") &&
atLeastVersion(hass.config.version, 2025, 10)
? "/api/hassio/core/logs/latest"
: "/api/error_log";
-1
View File
@@ -20,7 +20,6 @@ export interface CoreFrontendSystemData {
export interface HomeFrontendSystemData {
favorite_entities?: string[];
welcome_banner_dismissed?: boolean;
hidden_summaries?: string[];
}
declare global {
+30 -77
View File
@@ -3,10 +3,8 @@ import {
mdiAirFilter,
mdiAlert,
mdiAppleSafari,
mdiBattery,
mdiBell,
mdiBookmark,
mdiBrightness6,
mdiBullhorn,
mdiButtonPointer,
mdiCalendar,
@@ -18,18 +16,13 @@ import {
mdiCog,
mdiCommentAlert,
mdiCounter,
mdiDoorOpen,
mdiEye,
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
mdiFormTextbox,
mdiForumOutline,
mdiGarageOpen,
mdiGate,
mdiGoogleAssistant,
mdiGoogleCirclesCommunities,
mdiHomeAccount,
mdiHomeAutomation,
mdiImage,
mdiImageFilterFrames,
@@ -37,7 +30,6 @@ import {
mdiLightbulb,
mdiMapMarkerRadius,
mdiMicrophoneMessage,
mdiMotionSensor,
mdiPalette,
mdiRayVertex,
mdiRemote,
@@ -48,17 +40,13 @@ import {
mdiScriptText,
mdiSpeakerMessage,
mdiStarFourPoints,
mdiThermometer,
mdiThermostat,
mdiTimerOutline,
mdiToggleSwitch,
mdiWater,
mdiWaterPercent,
mdiWeatherPartlyCloudy,
mdiWhiteBalanceSunny,
mdiWindowClosed,
} from "@mdi/js";
import type { Connection, HassEntity } from "home-assistant-js-websocket";
import type { HassEntity } from "home-assistant-js-websocket";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { atLeastVersion } from "../common/config/version";
import { computeDomain } from "../common/entity/compute_domain";
@@ -72,7 +60,6 @@ import type {
} from "./entity/entity_registry";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import { callWS } from "../util/websocket";
import { getConditionDomain, getConditionObjectId } from "./condition";
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
@@ -88,7 +75,6 @@ export const FALLBACK_DOMAIN_ICONS = {
air_quality: mdiAirFilter,
alert: mdiAlert,
automation: mdiRobot,
battery: mdiBattery,
calendar: mdiCalendar,
climate: mdiThermostat,
configurator: mdiCog,
@@ -98,15 +84,10 @@ export const FALLBACK_DOMAIN_ICONS = {
datetime: mdiCalendarClock,
demo: mdiHomeAssistant,
device_tracker: mdiAccount,
door: mdiDoorOpen,
garage_door: mdiGarageOpen,
gate: mdiGate,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
homeassistant: mdiHomeAssistant,
homekit: mdiHomeAutomation,
humidity: mdiWaterPercent,
illuminance: mdiBrightness6,
image_processing: mdiImageFilterFrames,
image: mdiImage,
infrared: mdiLedOn,
@@ -118,15 +99,11 @@ export const FALLBACK_DOMAIN_ICONS = {
input_text: mdiFormTextbox,
lawn_mower: mdiRobotMower,
light: mdiLightbulb,
moisture: mdiWater,
motion: mdiMotionSensor,
notify: mdiCommentAlert,
number: mdiRayVertex,
occupancy: mdiHomeAccount,
persistent_notification: mdiBell,
person: mdiAccount,
plant: mdiFlower,
power: mdiFlash,
proximity: mdiAppleSafari,
remote: mdiRemote,
scene: mdiPalette,
@@ -138,7 +115,6 @@ export const FALLBACK_DOMAIN_ICONS = {
siren: mdiBullhorn,
stt: mdiMicrophoneMessage,
sun: mdiWhiteBalanceSunny,
temperature: mdiThermometer,
text: mdiFormTextbox,
time: mdiClock,
timer: mdiTimerOutline,
@@ -148,7 +124,6 @@ export const FALLBACK_DOMAIN_ICONS = {
vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep,
weather: mdiWeatherPartlyCloudy,
window: mdiWindowClosed,
zone: mdiMapMarkerRadius,
};
@@ -254,19 +229,18 @@ interface CategoryType {
}
export const getHassIcons = async <T extends IconCategory>(
connection: Connection,
hass: HomeAssistant,
category: T,
integration?: string
) =>
callWS<IconResources<CategoryType[T]>>(connection, {
hass.callWS<IconResources<CategoryType[T]>>({
type: "frontend/get_icons",
category,
integration,
});
export const getPlatformIcons = async (
hassConfig: HomeAssistant["config"],
connection: Connection,
hass: HomeAssistant,
integration: string,
force = false
): Promise<PlatformIcons | undefined> => {
@@ -274,12 +248,12 @@ export const getPlatformIcons = async (
return resources.entity[integration];
}
if (
!isComponentLoaded(hassConfig, integration) ||
!atLeastVersion(connection.haVersion, 2024, 2)
!isComponentLoaded(hass, integration) ||
!atLeastVersion(hass.connection.haVersion, 2024, 2)
) {
return undefined;
}
const result = getHassIcons(connection, "entity", integration).then(
const result = getHassIcons(hass, "entity", integration).then(
(res) => res?.resources[integration]
);
resources.entity[integration] = result;
@@ -287,13 +261,15 @@ export const getPlatformIcons = async (
};
export const getComponentIcons = async (
connection: Connection,
hassConfig: HomeAssistant["config"],
hass: HomeAssistant,
domain: string,
force = false
): Promise<ComponentIcons | undefined> => {
// For Cast, old instances can connect to it.
if (__BACKWARDS_COMPAT__ && !atLeastVersion(connection.haVersion, 2024, 2)) {
if (
__BACKWARDS_COMPAT__ &&
!atLeastVersion(hass.connection.haVersion, 2024, 2)
) {
return import("../fake_data/entity_component_icons")
.then((mod) => mod.ENTITY_COMPONENT_ICONS)
.then((res) => res[domain]);
@@ -307,12 +283,12 @@ export const getComponentIcons = async (
return resources.entity_component.resources.then((res) => res[domain]);
}
if (!isComponentLoaded(hassConfig, domain)) {
if (!isComponentLoaded(hass, domain)) {
return undefined;
}
resources.entity_component.domains = [...hassConfig.components];
resources.entity_component.domains = [...hass.config.components];
resources.entity_component.resources = getHassIcons(
connection,
hass,
"entity_component"
).then((result) => result.resources);
return resources.entity_component.resources.then((res) => res[domain]);
@@ -332,12 +308,10 @@ export const getCategoryIcons = async <
Record<string, CategoryType[T]>
>;
}
resources[category].all = getHassIcons(hass.connection, category).then(
(res) => {
resources[category].domains = res.resources as any;
return res?.resources as Record<string, CategoryType[T]>;
}
) as any;
resources[category].all = getHassIcons(hass, category).then((res) => {
resources[category].domains = res.resources as any;
return res?.resources as Record<string, CategoryType[T]>;
}) as any;
return resources[category].all as Promise<Record<string, CategoryType[T]>>;
}
if (!force && domain in resources[category].domains) {
@@ -349,10 +323,10 @@ export const getCategoryIcons = async <
return resources[category].domains[domain] as Promise<CategoryType[T]>;
}
}
if (!isComponentLoaded(hass.config, domain)) {
if (!isComponentLoaded(hass, domain)) {
return undefined;
}
const result = getHassIcons(hass.connection, category, domain);
const result = getHassIcons(hass, category, domain);
resources[category].domains[domain] = result.then(
(res) => res?.resources[domain]
) as any;
@@ -493,11 +467,7 @@ const getEntityIcon = async (
let icon: string | undefined;
if (translation_key && platform) {
const platformIcons = await getPlatformIcons(
hass.config,
hass.connection,
platform
);
const platformIcons = await getPlatformIcons(hass, platform);
if (platformIcons) {
const translations = platformIcons[domain]?.[translation_key];
@@ -510,11 +480,7 @@ const getEntityIcon = async (
}
if (!icon) {
const entityComponentIcons = await getComponentIcons(
hass.connection,
hass.config,
domain
);
const entityComponentIcons = await getComponentIcons(hass, domain);
if (entityComponentIcons) {
const translations =
(device_class && entityComponentIcons[device_class]) ||
@@ -545,11 +511,7 @@ export const attributeIcon = async (
(state.attributes[attribute] as string | number | undefined);
if (translation_key && platform) {
const platformIcons = await getPlatformIcons(
hass.config,
hass.connection,
platform
);
const platformIcons = await getPlatformIcons(hass, platform);
if (platformIcons) {
icon = getIconFromTranslations(
value,
@@ -558,11 +520,7 @@ export const attributeIcon = async (
}
}
if (!icon) {
const entityComponentIcons = await getComponentIcons(
hass.connection,
hass.config,
domain
);
const entityComponentIcons = await getComponentIcons(hass, domain);
if (entityComponentIcons) {
const translations =
(deviceClass &&
@@ -590,7 +548,7 @@ export const triggerIcon = async (
icon = trgrIcon?.trigger;
}
if (!icon) {
icon = await domainIcon(hass.connection, hass.config, domain);
icon = await domainIcon(hass, domain);
}
return icon;
};
@@ -609,7 +567,7 @@ export const conditionIcon = async (
icon = condIcon?.condition;
}
if (!icon) {
icon = await domainIcon(hass.connection, hass.config, domain);
icon = await domainIcon(hass, domain);
}
return icon;
};
@@ -627,7 +585,7 @@ export const serviceIcon = async (
icon = srvceIcon?.service;
}
if (!icon) {
icon = await domainIcon(hass.connection, hass.config, domain);
icon = await domainIcon(hass, domain);
}
return icon;
};
@@ -648,17 +606,12 @@ export const serviceSectionIcon = async (
};
export const domainIcon = async (
connection: Connection,
hassConfig: HomeAssistant["config"],
hass: HomeAssistant,
domain: string,
deviceClass?: string,
state?: string
): Promise<string | undefined> => {
const entityComponentIcons = await getComponentIcons(
connection,
hassConfig,
domain
);
const entityComponentIcons = await getComponentIcons(hass, domain);
if (entityComponentIcons) {
const translations =
(deviceClass && entityComponentIcons[deviceClass]) ||
+1 -1
View File
@@ -52,7 +52,7 @@ export const canCommissionMatterExternal = (hass: HomeAssistant) =>
hass.auth.external?.config.canCommissionMatter;
export const startExternalCommissioning = async (hass: HomeAssistant) => {
if (isComponentLoaded(hass.config, "thread")) {
if (isComponentLoaded(hass, "thread")) {
const datasets = await listThreadDataSets(hass);
const preferredDataset = datasets.datasets.find(
(dataset) => dataset.preferred
+2 -2
View File
@@ -5,11 +5,11 @@ import {
mdiServerNetwork,
mdiStorePlus,
} from "@mdi/js";
import { componentsWithService } from "../common/config/components_with_service";
import {
filterNavigationPages,
type NavigationFilterOptions,
} from "../common/config/filter_navigation_pages";
import { componentsWithService } from "../common/config/components_with_service";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
@@ -163,7 +163,7 @@ export const generateNavigationCommands = (
filterOptions
);
const appItems: BaseNavigationCommand[] = [];
if (hass.user?.is_admin && isComponentLoaded(hass.config, "hassio")) {
if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) {
appItems.push({
path: "/config/apps/available",
icon_path: mdiStorePlus,
-22
View File
@@ -60,7 +60,6 @@ export type Selector =
| NumberSelector
| NumericThresholdSelector
| ObjectSelector
| PeriodSelector
| AssistPipelineSelector
| QRCodeSelector
| SelectSelector
@@ -393,27 +392,6 @@ export interface ObjectSelector {
} | null;
}
export type PeriodKey =
| "today"
| "yesterday"
| "tomorrow"
| "this_week"
| "last_week"
| "next_week"
| "this_month"
| "last_month"
| "next_month"
| "this_year"
| "last_year"
| "next_7d"
| "next_30d"
| "none";
export interface PeriodSelector {
period: {
options: readonly PeriodKey[];
} | null;
}
export interface AssistPipelineSelector {
assist_pipeline: {
include_last_used?: boolean;
+16 -17
View File
@@ -1,6 +1,5 @@
import type { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import { callWS } from "../util/websocket";
export enum InclusionState {
/** The controller isn't doing anything regarding inclusion. */
@@ -326,10 +325,7 @@ export interface ZWaveJSRefreshNodeStatusMessage {
export interface ZWaveJSRebuildRoutesStatusMessage {
event: string;
rebuild_routes_status: Record<
number,
"pending" | "skipped" | "done" | "failed"
>;
rebuild_routes_status: Record<number, string>;
}
export interface ZWaveJSControllerStatisticsUpdatedMessage {
@@ -479,7 +475,7 @@ export const invokeZWaveCCApi = <T = unknown>(
});
export const fetchZwaveNetworkStatus = (
connection: Connection,
hass: HomeAssistant,
device_or_entry_id: {
device_id?: string;
entry_id?: string;
@@ -491,7 +487,7 @@ export const fetchZwaveNetworkStatus = (
if (!device_or_entry_id.device_id && !device_or_entry_id.entry_id) {
throw new Error("Either device or entry ID should be supplied.");
}
return callWS<ZWaveJSNetwork>(connection, {
return hass.callWS({
type: "zwave_js/network_status",
device_id: device_or_entry_id.device_id,
entry_id: device_or_entry_id.entry_id,
@@ -818,32 +814,35 @@ export const removeFailedZwaveNode = (
);
export const rebuildZwaveNetworkRoutes = (
connection: Connection,
hass: HomeAssistant,
entry_id: string
): Promise<UnsubscribeFunc> =>
callWS(connection, {
hass.callWS({
type: "zwave_js/begin_rebuilding_routes",
entry_id,
});
export const stopRebuildingZwaveNetworkRoutes = (
connection: Connection,
hass: HomeAssistant,
entry_id: string
): Promise<UnsubscribeFunc> =>
callWS(connection, {
hass.callWS({
type: "zwave_js/stop_rebuilding_routes",
entry_id,
});
export const subscribeRebuildZwaveNetworkRoutesProgress = (
connection: Connection,
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: ZWaveJSRebuildRoutesStatusMessage) => void
): Promise<UnsubscribeFunc> =>
connection.subscribeMessage((message: any) => callbackFunction(message), {
type: "zwave_js/subscribe_rebuild_routes_progress",
entry_id,
});
hass.connection.subscribeMessage(
(message: any) => callbackFunction(message),
{
type: "zwave_js/subscribe_rebuild_routes_progress",
entry_id,
}
);
export const subscribeZwaveControllerStatistics = (
hass: HomeAssistant,
@@ -338,7 +338,7 @@ class EntityPreviewRow extends LitElement {
.autoValidate=${stateObj.attributes.pattern}
.pattern=${stateObj.attributes.pattern}
.type=${stateObj.attributes.mode}
.placeholder=${this.hass!.localize("ui.card.text.empty_value")}
placeholder=${this.hass!.localize("ui.card.text.emtpy_value")}
></ha-input>
`;
}
@@ -170,8 +170,7 @@ class StepFlowCreateEntry extends LitElement {
)}
.placeholder=${computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
this.hass
)}
.value=${this._deviceUpdate[device.id]?.name ??
computeDeviceName(device)}
-2
View File
@@ -11,8 +11,6 @@ export const DialogMixin = <
superClass: T
) =>
class extends superClass implements HassDialogNext<P> {
public dialogNext = true as const;
declare public params?: P;
private _closePromise?: Promise<boolean>;
+2 -5
View File
@@ -26,7 +26,6 @@ export interface HassDialog<T = unknown> extends HTMLElement {
}
export interface HassDialogNext<T = unknown> extends HTMLElement {
dialogNext: true;
params?: T;
closeDialog?: (historyState?: any) => Promise<boolean> | boolean;
}
@@ -169,12 +168,10 @@ export const showDialog = async (
dialogElement = await LOADED[dialogTag].element;
}
if ("dialogNext" in dialogElement! && dialogElement.dialogNext) {
dialogElement!.params = dialogParams;
} else if ("showDialog" in dialogElement!) {
if ("showDialog" in dialogElement!) {
dialogElement.showDialog(dialogParams);
} else {
throw new Error("Unknown dialog type loaded");
dialogElement!.params = dialogParams;
}
(parentElement || element).shadowRoot!.appendChild(dialogElement!);
+3 -3
View File
@@ -3,9 +3,9 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeDomain } from "../../common/entity/compute_domain";
import type { GroupEntity } from "../../data/group";
import { computeGroupDomain } from "../../data/group";
import { isNumericEntity } from "../../data/history";
import { CONTINUOUS_DOMAINS } from "../../data/logbook";
import type { HomeAssistant } from "../../types";
import { isNumericEntity } from "../../data/history";
export const MORE_INFO_VIEWS = [
"info",
@@ -113,7 +113,7 @@ export const computeShowHistoryComponent = (
hass: HomeAssistant,
entityId: string
) =>
isComponentLoaded(hass.config, "history") &&
isComponentLoaded(hass, "history") &&
!DOMAINS_MORE_INFO_NO_HISTORY.includes(computeDomain(entityId));
export const computeShowLogBookComponent = (
@@ -121,7 +121,7 @@ export const computeShowLogBookComponent = (
entityId: string,
sensorNumericalDeviceClasses: string[] = []
): boolean => {
if (!isComponentLoaded(hass.config, "logbook")) {
if (!isComponentLoaded(hass, "logbook")) {
return false;
}
@@ -82,11 +82,6 @@ class MoreInfoInputDatetime extends LitElement {
display: flex;
align-items: center;
justify-content: flex-end;
--ha-input-padding-bottom: 0;
flex-wrap: wrap;
}
ha-date-input {
flex: 1 1 160px;
}
ha-date-input + ha-time-input {
margin-left: var(--ha-space-1);
@@ -355,7 +355,7 @@ class MoreInfoUpdate extends LitElement {
this._fetchEntitySources().then(() => {
const type = getUpdateType(this.stateObj!, this._entitySources!);
if (
isComponentLoaded(this.hass.config, "hassio") &&
isComponentLoaded(this.hass, "hassio") &&
["addon", "home_assistant", "home_assistant_os"].includes(type)
) {
this._fetchUpdateBackupConfig(type);
@@ -19,8 +19,8 @@ import type {
} from "../../data/recorder";
import { fetchStatistics, getStatisticMetadata } from "../../data/recorder";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { haStyle } from "../../resources/styles";
declare global {
interface HASSDomEvents {
@@ -57,7 +57,7 @@ export class MoreInfoHistory extends LitElement {
return nothing;
}
return html`${isComponentLoaded(this.hass.config, "history")
return html`${isComponentLoaded(this.hass, "history")
? html`<div class="header">
<div>
<h2>
@@ -204,7 +204,7 @@ export class MoreInfoHistory extends LitElement {
private async _getStateHistory(): Promise<void> {
if (
isComponentLoaded(this.hass.config, "recorder") &&
isComponentLoaded(this.hass, "recorder") &&
computeDomain(this.entityId) === "sensor"
) {
const stateObj = this.hass.states[this.entityId];
@@ -221,7 +221,7 @@ export class MoreInfoHistory extends LitElement {
}
}
if (!isComponentLoaded(this.hass.config, "history")) {
if (!isComponentLoaded(this.hass, "history")) {
return;
}
if (this._subscribed) {
@@ -6,8 +6,8 @@ import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { createSearchParam } from "../../common/url/search-params";
import "../../panels/logbook/ha-logbook";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { haStyle } from "../../resources/styles";
@customElement("ha-more-info-logbook")
export class MoreInfoLogbook extends LitElement {
@@ -22,7 +22,7 @@ export class MoreInfoLogbook extends LitElement {
private _entityIdAsList = memoizeOne((entityId: string) => [entityId]);
protected render() {
if (!isComponentLoaded(this.hass.config, "logbook") || !this.entityId) {
if (!isComponentLoaded(this.hass, "logbook") || !this.entityId) {
return nothing;
}
const stateObj = this.hass.states[this.entityId];
+2 -4
View File
@@ -129,10 +129,7 @@ export class QuickBar extends LitElement {
console.error("Error fetching config entries for quick bar", err);
}
if (
this.hass.user?.is_admin &&
isComponentLoaded(this.hass.config, "hassio")
) {
if (this.hass.user?.is_admin && isComponentLoaded(this.hass, "hassio")) {
try {
const hassioAddonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = hassioAddonsInfo.addons;
@@ -306,6 +303,7 @@ export class QuickBar extends LitElement {
<ha-domain-icon
slot="start"
style="margin: var(--ha-space-1);"
.hass=${this.hass}
.domain=${item.domain}
brand-fallback
></ha-domain-icon>
+27 -20
View File
@@ -11,11 +11,11 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-adaptive-dialog";
import "../../components/ha-alert";
import "../../components/ha-expansion-panel";
import "../../components/ha-fade-in";
import "../../components/ha-icon-next";
import "../../components/ha-adaptive-dialog";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
@@ -59,7 +59,7 @@ class DialogRestart extends LitElement {
private _dialogOpen = false;
public async showDialog(): Promise<void> {
const isHassioLoaded = isComponentLoaded(this.hass.config, "hassio");
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
this._open = true;
this._dialogOpen = true;
@@ -103,6 +103,7 @@ class DialogRestart extends LitElement {
return nothing;
}
const showReload = this.hass.userData?.showAdvanced;
const showRebootShutdown = !!this._hostInfo;
const dialogTitle = this.hass.localize("ui.dialogs.restart.heading");
@@ -134,24 +135,30 @@ class DialogRestart extends LitElement {
`
: html`
<ha-md-list dialogInitialFocus>
<ha-md-list-item
type="button"
@click=${this._reload}
.disabled=${this._loadingBackupInfo}
>
<div slot="headline">
${this.hass.localize("ui.dialogs.restart.reload.title")}
</div>
<div slot="supporting-text">
${this.hass.localize(
"ui.dialogs.restart.reload.description"
)}
</div>
<div slot="start" class="icon-background reload">
<ha-svg-icon .path=${mdiAutoFix}></ha-svg-icon>
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
${showReload
? html`
<ha-md-list-item
type="button"
@click=${this._reload}
.disabled=${this._loadingBackupInfo}
>
<div slot="headline">
${this.hass.localize(
"ui.dialogs.restart.reload.title"
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
"ui.dialogs.restart.reload.description"
)}
</div>
<div slot="start" class="icon-background reload">
<ha-svg-icon .path=${mdiAutoFix}></ha-svg-icon>
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
`
: nothing}
<ha-md-list-item
type="button"
.action=${"restart"}
@@ -182,7 +182,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
await this._pickOrCreatePipelineExists();
return;
}
if (!isComponentLoaded(this.hass.config, "hassio")) {
if (!isComponentLoaded(this.hass, "hassio")) {
this._state = "NOT_SUPPORTED";
return;
}
@@ -20,9 +20,9 @@ import { getLanguageScores, listAgents } from "../../data/conversation";
import { listSTTEngines } from "../../data/stt";
import { listTTSEngines, listTTSVoices } from "../../data/tts";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { AssistantSetupStyles } from "./styles";
import { STEP } from "./voice-assistant-setup-dialog";
import { documentationUrl } from "../../util/documentation-url";
const OPTIONS = ["cloud", "focused_local", "full_local"] as const;
@@ -251,7 +251,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
}
private async _hasCloud(): Promise<boolean> {
if (!isComponentLoaded(this.hass.config, "cloud")) {
if (!isComponentLoaded(this.hass, "cloud")) {
return false;
}
const cloudStatus = await fetchCloudStatus(this.hass);
@@ -104,11 +104,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.device_name"
)}
.placeholder=${computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)}
.placeholder=${computeDeviceNameDisplay(device, this.hass)}
.value=${this._deviceName ?? computeDeviceName(device)}
@change=${this._deviceNameChanged}
></ha-input>
@@ -505,6 +505,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
`
: ""}
<ha-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.columns=${this.columns}
.data=${this.data}
+23 -39
View File
@@ -1,13 +1,10 @@
import { consume } from "@lit/context";
import type { PropertyValues, ReactiveElement } from "lit";
import { state } from "lit/decorators";
import type { ReactiveElement } from "lit";
import type { HomeAssistant } from "../types";
import { setupConditionListeners } from "../common/condition/listeners";
import { maxColumnsContext } from "../panels/lovelace/common/context";
import type {
Condition,
ConditionContext,
} from "../panels/lovelace/common/validate-condition";
import {
setupMediaQueryListeners,
setupTimeListeners,
} from "../common/condition/listeners";
import type { Condition } from "../panels/lovelace/common/validate-condition";
type Constructor<T> = abstract new (...args: any[]) => T;
@@ -35,7 +32,6 @@ export interface ConditionalConfig {
* - Sets up listeners when component connects to DOM
* - Cleans up listeners when component disconnects from DOM
* - Handles conditional visibility based on defined conditions
* - Consumes column count from the view via Lit Context
*/
export const ConditionalListenerMixin = <
TConfig extends ConditionalConfig = ConditionalConfig,
@@ -51,12 +47,6 @@ export const ConditionalListenerMixin = <
public hass?: HomeAssistant;
@state()
@consume({ context: maxColumnsContext, subscribe: true })
protected _maxColumns?: number;
protected _conditionContext: ConditionContext = {};
protected _updateElement?(config: TConfig): void;
protected _updateVisibility?(conditionsMet?: boolean): void;
@@ -71,20 +61,6 @@ export const ConditionalListenerMixin = <
this.clearConditionalListeners();
}
protected willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (changedProperties.has("_maxColumns")) {
this._conditionContext = { max_columns: this._maxColumns };
}
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("_maxColumns")) {
this._updateVisibility?.();
}
}
/**
* Clear conditional listeners
*
@@ -130,18 +106,26 @@ export const ConditionalListenerMixin = <
return;
}
setupConditionListeners(
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),
(conditionsMet) => {
if (this._updateVisibility) {
this._updateVisibility(conditionsMet);
} else if (this._updateElement && config) {
this._updateElement(config);
}
},
() => this._conditionContext
onUpdate
);
setupTimeListeners(
finalConditions,
this.hass,
(unsub) => this.addConditionalListener(unsub),
onUpdate
);
}
}
+1 -1
View File
@@ -192,7 +192,7 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
}
private async _scanUSBDevices() {
if (!isComponentLoaded(this.hass.config, "usb")) {
if (!isComponentLoaded(this.hass, "usb")) {
return;
}
await scanUSBDevices(this.hass);
@@ -486,17 +486,6 @@ class SupervisorAppInfo extends LitElement {
<div class="description light-color">
${this.addon.description}.<br />
${this.hass.localize(
"ui.panel.config.apps.dashboard.visit_app_page",
{
name: html`<a
href=${this.addon.url!}
target="_blank"
rel="noreferrer"
>${getAppDisplayName(this.addon.name, this.addon.stage)}</a
>`,
}
)}
</div>
<div class="addon-container">
<div>
@@ -1038,7 +1027,6 @@ class SupervisorAppInfo extends LitElement {
}
private _updateComplete() {
this._scheduleDataUpdate();
const eventdata = {
success: true,
response: undefined,
@@ -1060,16 +1048,11 @@ class SupervisorAppInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
showConfirmationDialog(this, {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.install"
),
text: extractApiErrorMessage(err),
confirmText: this.hass.localize("ui.common.ok"),
dismissText: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.view_supervisor_logs"
),
cancel: () => navigate("/config/logs?provider=supervisor"),
});
}
button.progress = false;
@@ -108,6 +108,7 @@ export class HaConfigAppsRegistries extends LitElement {
.header=${this.hass.localize("ui.panel.config.apps.store.registries")}
>
<ha-data-table
.hass=${this.hass}
.columns=${this._columns(this.hass.localize)}
.data=${this._registries}
.noDataText=${this.hass.localize(
@@ -187,6 +187,7 @@ export class HaConfigAppsRepositories extends LitElement {
.header=${this.hass.localize("ui.panel.config.apps.store.repositories")}
>
<ha-data-table
.hass=${this.hass}
.columns=${this._columns(this.hass.localize, usedRepositories)}
.data=${this._data(repositories)}
.noDataText=${this.hass.localize(
+9 -13
View File
@@ -18,7 +18,6 @@ import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
@@ -53,6 +52,7 @@ import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
declare interface NameAndEntity<EntityType extends HassEntity> {
name: string;
@@ -166,11 +166,7 @@ class HaConfigAreaPage extends LitElement {
// Pre-compute the entity and device names, so we can sort by them
if (devices) {
devices.forEach((entry) => {
entry.name = computeDeviceNameDisplay(
entry,
this.hass.localize,
this.hass.states
);
entry.name = computeDeviceNameDisplay(entry, this.hass);
});
sortDeviceRegistryByName(devices, this.hass.locale.language);
}
@@ -194,7 +190,7 @@ class HaConfigAreaPage extends LitElement {
let relatedScenes: NameAndEntity<SceneEntity>[] = [];
let relatedScripts: NameAndEntity<ScriptEntity>[] = [];
if (isComponentLoaded(this.hass.config, "automation")) {
if (isComponentLoaded(this.hass, "automation")) {
({
groupedEntities: groupedAutomations,
relatedEntities: relatedAutomations,
@@ -204,7 +200,7 @@ class HaConfigAreaPage extends LitElement {
));
}
if (isComponentLoaded(this.hass.config, "scene")) {
if (isComponentLoaded(this.hass, "scene")) {
({ groupedEntities: groupedScenes, relatedEntities: relatedScenes } =
this._prepareEntities<SceneEntity>(
groupedEntities.scene,
@@ -212,7 +208,7 @@ class HaConfigAreaPage extends LitElement {
));
}
if (isComponentLoaded(this.hass.config, "script")) {
if (isComponentLoaded(this.hass, "script")) {
({ groupedEntities: groupedScripts, relatedEntities: relatedScripts } =
this._prepareEntities<ScriptEntity>(
groupedEntities.script,
@@ -332,7 +328,7 @@ class HaConfigAreaPage extends LitElement {
</ha-card>
</div>
<div class="column">
${isComponentLoaded(this.hass.config, "automation")
${isComponentLoaded(this.hass, "automation")
? html`
<ha-card
outlined
@@ -382,7 +378,7 @@ class HaConfigAreaPage extends LitElement {
</ha-card>
`
: ""}
${isComponentLoaded(this.hass.config, "scene")
${isComponentLoaded(this.hass, "scene")
? html`
<ha-card
outlined
@@ -426,7 +422,7 @@ class HaConfigAreaPage extends LitElement {
</ha-card>
`
: ""}
${isComponentLoaded(this.hass.config, "script")
${isComponentLoaded(this.hass, "script")
? html`
<ha-card
outlined
@@ -468,7 +464,7 @@ class HaConfigAreaPage extends LitElement {
: ""}
</div>
<div class="column">
${isComponentLoaded(this.hass.config, "logbook")
${isComponentLoaded(this.hass, "logbook")
? html`
<ha-card
outlined
@@ -95,8 +95,6 @@ import "./types/ha-automation-action-set_conversation_response";
import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../common/entity/compute_object_id";
export const getAutomationActionType = memoizeOne(
(action: Action | undefined) => {
@@ -241,14 +239,7 @@ export default class HaAutomationActionRow extends LitElement {
private _renderRow() {
const type = getAutomationActionType(this.action);
const action = type === "service" && (this.action as ServiceAction).action;
const actionHasTarget =
action &&
"target" in
(this.hass.services?.[computeDomain(action)]?.[
computeObjectId(action)
] || {});
const actionHasTarget = type === "service" && "target" in this.action;
const target = actionHasTarget
? (this.action as ServiceAction).target
@@ -559,10 +550,7 @@ export default class HaAutomationActionRow extends LitElement {
>${this._renderRow()}</ha-automation-row
>`
: html`
<ha-expansion-panel
left-chevron
@expanded-changed=${this._expansionPanelChanged}
>
<ha-expansion-panel left-chevron>
${this._renderRow()}
</ha-expansion-panel>
`}
@@ -820,12 +808,6 @@ export default class HaAutomationActionRow extends LitElement {
}
}
private _expansionPanelChanged(ev: CustomEvent) {
if (!ev.detail.expanded) {
this._isNew = false;
}
}
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
@@ -930,6 +912,9 @@ export default class HaAutomationActionRow extends LitElement {
);
private _toggleCollapse() {
if (!this._collapsed) {
this._isNew = false;
}
this._collapsed = !this._collapsed;
}
@@ -198,7 +198,7 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
private _addAction = (action: string, target?: HassServiceTarget) => {
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action!));
actions = this.actions.concat(deepClone(this._clipboard!.action));
} else if (action in VIRTUAL_ACTIONS) {
actions = this.actions.concat(VIRTUAL_ACTIONS[action]);
} else if (isDynamic(action)) {
@@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield";
import type { Action, ParallelAction } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
@@ -1,13 +1,14 @@
import type { CSSResultGroup } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { query, customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield";
import type { Action, SequenceAction } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-action";
import type HaAutomationAction from "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
import type HaAutomationAction from "../ha-automation-action";
@customElement("ha-automation-action-sequence")
export class HaSequenceAction extends LitElement implements ActionElement {

Some files were not shown because too many files have changed in this diff Show More