Compare commits

..

53 Commits

Author SHA1 Message Date
Bram Kragten 09982a9238 Fix form integer when data is null 2026-04-02 23:28:06 +02:00
renovate[bot] ab1a58b3f3 Update dependency @rsdoctor/rspack-plugin to v1.5.6 (#51375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-02 16:13:27 +01:00
Petar Petrov a7ff89385e Load energy translations in dashboard strategy before generating view titles (#51376) 2026-04-02 16:10:59 +01:00
Wendelin f3d41be3bf Fix login on legacy browsers (#51373) 2026-04-02 16:08:52 +02:00
Wendelin b73707751a Z-Wave rebuild routes add detail progress (#51361)
* WIP new dialog use states

* WIP add zwave rebuild network routes details

* Enhance Z-Wave JS rebuild network routes dialog with loading indicators and improved progress handling

* Remove hass param from `domain-icon`

* Remove unneeded @states

* List more compact

* fix prettier

* fix tests

* Rename device context to getDeviceArea
2026-04-02 16:51:33 +03:00
Petar Petrov 61bff43cdb Fix statistics-graph card not rendering self-imported stats (#51367) 2026-04-02 11:29:03 +01:00
Aidan Timson 0a0d08fa19 Remove advanced mode requirement reloading config (#51366) 2026-04-02 13:09:57 +03:00
Aidan Timson ae29ba63ff Remove advanced mode for dashboard url path creation (#51364)
Remove advanced mode requirement for dashboard url path creation
2026-04-02 13:08:30 +03:00
Aidan Timson 0579cd8eb6 Remove advanced mode requirement for manage resources link (#51363) 2026-04-02 13:08:07 +03:00
Aidan Timson 8c3eafec6d Remove advanced mode requirement for hardware data table (#51362) 2026-04-02 13:07:37 +03:00
Norbert Rittel b5c2e12016 Rename "Manual event" trigger and action to clarify (#51358) 2026-04-02 12:10:34 +03:00
Copilot f7a13392cd Rename "Custom cards" to "Community cards" (#51312)
* Initial plan

* Rename custom cards to community cards

Agent-Logs-Url: https://github.com/home-assistant/frontend/sessions/874ac3ba-2f7e-48cd-a0c4-e2dc2b371d8d

* Rename badges

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-04-02 10:47:29 +02:00
Paul Bottein a2cdd592f1 Fix device page entity names not refreshing after device rename (#51355) 2026-04-02 09:35:12 +01:00
Wendelin f04341a2a2 Fix input hint height (#51351) 2026-04-02 09:34:19 +01:00
Petar Petrov 91bdc80a67 Fix history-graph card not showing first value (#51350) 2026-04-02 10:13:59 +02:00
renovate[bot] b4824cc0a7 Update dependency typescript to v6 (#30363)
* Update dependency typescript to v6

* Fix deprecation

* Fix cast

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-04-02 08:59:32 +03:00
Paulus Schoutsen 28f375c0d4 Allow enabling/disabling summaries (#51319)
* Allow customizing home page summaries and adding quick links

Add ability to hide built-in summaries (light, climate, security,
media players, weather, energy) and add custom quick links to
dashboards, sidebar items, or other pages from the edit overview dialog.

https://claude.ai/code/session_01AqgbQULH5vfETibiba5RXH

* Remove quick links, focus on summary enable/disable only

https://claude.ai/code/session_01AqgbQULH5vfETibiba5RXH

* Match summary editor rows to dashboard order with icon, color, and toggle on right

Each summary row now shows its colored icon and title matching the
dashboard appearance, with the toggle switch moved to the right side.
Order matches the dashboard: light, climate, security, media players,
weather, energy, persons.

https://claude.ai/code/session_01AqgbQULH5vfETibiba5RXH

* Lint

* Apply suggestion from @balloob

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-02 08:42:04 +03:00
Simon Lamon da7ccac811 No longer take the first action when no action is selected (#51341)
No longer take the first action when no action is selected
2026-04-02 08:21:02 +03:00
Aidan Timson a8ad921efd Create shared select card feature base class (#51333)
* Create shared select card feature base class

* Add sound mode and source features

* Remove serviceValueKey as its the same as attribute

* Migrate more

* Migrate select options

* Add fan direction

* Remove default usages
2026-04-01 17:58:11 +03:00
Petar Petrov 3b8f219800 Add customizable dismiss label to ha-alert component (#51337)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-01 15:20:05 +01:00
Paul Bottein e36a2e1c70 Use view columns visibility condition in strategies (#51323) 2026-04-01 15:05:52 +03:00
Wendelin e06ea1047c Fix generic picker filter section padding (#51334)
Fix padding in picker section for improved layout
2026-04-01 15:04:24 +03:00
Petar Petrov 99cb997d08 Use localized string for empty logbook entries in trace view (#51324) 2026-04-01 15:03:42 +03:00
Wendelin ac3edd20f8 Fix picker search padding (#51331) 2026-04-01 09:30:49 +00:00
Wendelin 0d88d139f0 Fix date input field shrink (#51330) 2026-04-01 09:22:37 +00:00
Petar Petrov b8d08ccb05 Use ha-card-border-color for integration cards instead of divider-color (#51321) 2026-04-01 11:13:24 +02:00
Petar Petrov 7c20316ba5 Fix layout of compare card in water/gas views (#51329) 2026-04-01 08:57:25 +00:00
Aidan Timson fa633efc87 Fix target item loading error (#51326) 2026-04-01 10:43:42 +02:00
Petar Petrov 85d461f0fd Await energy translation fragment before generating dashboard strategy (#51327) 2026-04-01 10:41:27 +02:00
Wendelin b55e1c9988 Improve dialog open logic (#51328) 2026-04-01 10:40:40 +02:00
Petar Petrov 1da349a36d Fix ZHA device count not including devices without entities (#51322) 2026-04-01 09:17:39 +01:00
Paul Bottein 74f7139a09 Add view columns visibility condition (#51288)
* Add view columns visibility condition

* Use max column, not column count

* Rename

* Remove editor
2026-04-01 10:11:53 +02:00
Petar Petrov 2911cc77fa Fix Fill example data inserting incorrect datetime format (#51320) 2026-04-01 06:43:09 +00:00
Wendelin ab20383a3a Migrate all from ha-textfield to ha-input (#30349) 2026-04-01 08:37:49 +02:00
karwosts 514cb9da9d Add due_date_period to todo UI, create period selector (#51263)
* Add due_date_period to todo UI, create period selector

* updates

* Apply suggestions from code review

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* ??

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-04-01 09:10:15 +03:00
Wendelin 7c52ac8ca7 Remove target description (#51315) 2026-03-31 17:42:02 +02:00
Aidan Timson 07b4a44228 Fix tile secondary info pop in (#51308)
* Add support for skeleton on tile info secondary text

* Show loading state for users of tile info

* Update src/components/tile/ha-tile-info.ts

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-03-31 17:38:03 +03:00
Bram Kragten 2b28a6c3f2 Update download-translations.js 2026-03-31 15:50:59 +02:00
Bram Kragten 84f2e304cf Make translation downloading async (#51314) 2026-03-31 13:35:05 +00:00
Aidan Timson 18cd40ab01 Add select source card feature for supported media players (#51283)
* Add select source card feature for supported media players

* Show label

* Apply suggestions from code review

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Use shouldUpdate

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-03-31 15:44:18 +03:00
Bram Kragten 8e3b1dc6ac Triggers/conditions Add usage and grouping to new multi domains (#51287) 2026-03-31 12:42:15 +00:00
Bram Kragten 5cc223a582 Fix has target check for actions (#51309) 2026-03-31 14:37:46 +02:00
Wendelin 9a62a9217c Fix automation add TCA dialog sometimes not opening (#51306) 2026-03-31 13:36:56 +02:00
Simon Lamon 70be747e9d Gauge improvements (#30368)
* Gauge last improvements

* Change needle

* Fixup

* Feedback comments

* Update src/components/ha-gauge.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-03-31 14:27:22 +03:00
Aidan Timson bb57a91494 Add select sound mode card feature for supported media players (#51282)
* Add sound mode card feature for supported media players

* Show label

* Use shouldUpdate
2026-03-31 11:25:00 +00:00
Paul Bottein 7e22e6c0e2 Rename "People at home" summary tile to "Presence" (#51305)
* Rename "People at home" summary tile to "Presence"

* Improve person translation
2026-03-31 13:59:52 +03:00
Wendelin c93f910e56 Fix system hardware caption translation (#51303) 2026-03-31 12:50:17 +02:00
Aidan Timson 8bf4ff5d25 Fix above/below numeric state entity formatting (#51298) 2026-03-31 12:49:37 +02:00
Wendelin debc3adf19 Remove hass property in ha-data-table (#51304) 2026-03-31 12:47:17 +02:00
Petar Petrov ae21017de8 Use boundaryFilter data zoom mode only for line charts (#51307) 2026-03-31 12:44:52 +02:00
Wendelin f15f518cc2 Improve date-range-picker mobile presets (#51285) 2026-03-31 12:39:54 +02:00
Paulus Schoutsen 0e44417051 Fix My link for apps (#51258)
* Clean up My link matching

* Fix My link for apps to include repository_url

* Apply suggestion from @MindFreeze

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-03-31 13:33:23 +03:00
Timothy 3581b43336 Add delay before revoking URL on Android (#51299) 2026-03-31 12:23:00 +02:00
323 changed files with 4512 additions and 4840 deletions
+91 -48
View File
@@ -99,6 +99,44 @@ 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 {
@@ -118,55 +156,60 @@ gulp.task("fetch-lokalise", async function () {
]);
await Promise.all(
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();
}
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) {
throw new Error(response.statusText);
})
.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;
})
)
}
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;
}
})
);
});
+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/ha-textfield";
import "../../../../src/components/input/ha-input";
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-textfield
<ha-input
label="Home Assistant URL"
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
@keydown=${this._handleInputKeyDown}
></ha-textfield>
></ha-input>
${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-textfield")!;
const inputEl = this.shadowRoot!.querySelector("ha-input")!;
const value = inputEl.value || "";
this.error = undefined;
@@ -319,7 +319,7 @@ export class HcConnect extends LitElement {
flex: 1;
}
ha-textfield {
ha-input {
width: 100%;
}
`;
+1
View File
@@ -1,3 +1,4 @@
/// <reference types="chromecast-caf-sender" />
import { mdiTelevision } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
+6 -2
View File
@@ -1,5 +1,5 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } 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,7 +692,11 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
([key, value]) => html`
<ha-settings-row narrow slot=${slot}>
<span slot="heading">${value?.name || key}</span>
<span slot="description">${value?.description}</span>
${value?.description
? html`<span slot="description"
>${value?.description}</span
>`
: nothing}
<ha-selector
.hass=${this.hass}
.selector=${value!.selector}
+15
View File
@@ -134,6 +134,21 @@ 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,7 +422,6 @@ export class DemoEntityState extends LitElement {
return html`
<ha-data-table
.hass=${this.hass}
.columns=${this._columns(this.hass)}
.data=${this._rows()}
auto-height
+2 -2
View File
@@ -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.5",
"@rsdoctor/rspack-plugin": "1.5.6",
"@rspack/core": "1.7.10",
"@rspack/dev-server": "1.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -207,7 +207,7 @@
"tar": "7.5.13",
"terser-webpack-plugin": "5.4.0",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript": "6.0.2",
"typescript-eslint": "8.57.2",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.2",
+1
View File
@@ -1,4 +1,5 @@
/* eslint-disable no-console */
/// <reference types="chromecast-caf-sender" />
import type { Auth } from "home-assistant-js-websocket";
import { castApiAvailable } from "./cast_framework";
+26 -5
View File
@@ -1,6 +1,9 @@
import { listenMediaQuery } from "../dom/media_query";
import type { HomeAssistant } from "../../types";
import type { Condition } from "../../panels/lovelace/common/validate-condition";
import type {
Condition,
ConditionContext,
} from "../../panels/lovelace/common/validate-condition";
import { checkConditionsMet } from "../../panels/lovelace/common/validate-condition";
import { extractMediaQueries, extractTimeConditions } from "./extract";
import { calculateNextTimeUpdate } from "./time-calculator";
@@ -19,7 +22,8 @@ export function setupMediaQueryListeners(
conditions: Condition[],
hass: HomeAssistant,
addListener: (unsub: () => void) => void,
onUpdate: (conditionsMet: boolean) => void
onUpdate: (conditionsMet: boolean) => void,
getContext?: () => ConditionContext
): void {
const mediaQueries = extractMediaQueries(conditions);
@@ -36,7 +40,8 @@ export function setupMediaQueryListeners(
if (hasOnlyMediaQuery) {
onUpdate(matches);
} else {
const conditionsMet = checkConditionsMet(conditions, hass);
const context = getContext?.() ?? {};
const conditionsMet = checkConditionsMet(conditions, hass, context);
onUpdate(conditionsMet);
}
});
@@ -51,7 +56,8 @@ export function setupTimeListeners(
conditions: Condition[],
hass: HomeAssistant,
addListener: (unsub: () => void) => void,
onUpdate: (conditionsMet: boolean) => void
onUpdate: (conditionsMet: boolean) => void,
getContext?: () => ConditionContext
): void {
const timeConditions = extractTimeConditions(conditions);
@@ -70,7 +76,8 @@ export function setupTimeListeners(
timeoutId = setTimeout(() => {
if (delay <= MAX_TIMEOUT_DELAY) {
const conditionsMet = checkConditionsMet(conditions, hass);
const context = getContext?.() ?? {};
const conditionsMet = checkConditionsMet(conditions, hass, context);
onUpdate(conditionsMet);
}
scheduleUpdate();
@@ -87,3 +94,17 @@ 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, integration)
isComponentLoaded(hass.config, integration)
);
export const isNotLoadedIntegration = (
@@ -23,7 +23,7 @@ export const isNotLoadedIntegration = (
) =>
!page.not_component ||
!ensureArray(page.not_component).some((integration) =>
isComponentLoaded(hass, integration)
isComponentLoaded(hass.config, 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 = (
hass: HomeAssistant,
hassConfig: HomeAssistant["config"],
component: string
): boolean => hass && hass.config.components.includes(component);
): boolean => hassConfig && hassConfig.components.includes(component);
+7 -6
View File
@@ -14,24 +14,25 @@ export const computeDeviceName = (
export const computeDeviceNameDisplay = (
device: DeviceRegistryEntry,
hass: HomeAssistant,
localize: HomeAssistant["localize"],
hassStates: HomeAssistant["states"],
entities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) =>
computeDeviceName(device) ||
(entities && fallbackDeviceName(hass, entities)) ||
hass.localize("ui.panel.config.devices.unnamed_device", {
type: hass.localize(
(entities && fallbackDeviceName(hassStates, entities)) ||
localize("ui.panel.config.devices.unnamed_device", {
type: localize(
`ui.panel.config.devices.type.${device.entry_type || "device"}`
),
});
export const fallbackDeviceName = (
hass: HomeAssistant,
hassStates: HomeAssistant["states"],
entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) => {
for (const entity of entities || []) {
const entityId = typeof entity === "string" ? entity : entity.entity_id;
const stateObj = hass.states[entityId];
const stateObj = hassStates[entityId];
if (stateObj) {
return computeStateName(stateObj);
}
@@ -1,26 +1,11 @@
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";
interface DeviceContext {
device: DeviceRegistryEntry;
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}
export const getDeviceContext = (
export const getDeviceArea = (
device: DeviceRegistryEntry,
hass: HomeAssistant
): DeviceContext => {
areas: HomeAssistant["areas"]
): AreaRegistryEntry | undefined => {
const areaId = device.area_id;
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,
};
return areaId ? areas[areaId] : undefined;
};
+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, domain) &&
isComponentLoaded(hass.config, 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, domain)) {
if (isComponentLoaded(hass.config, domain)) {
if (
entityRegEntry &&
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)
@@ -0,0 +1,8 @@
/**
* 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, "zwave_js") ||
!isComponentLoaded(hass.config, "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, "zha") ||
!isComponentLoaded(hass.config, "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, domain) ||
!isComponentLoaded(hass.config, domain) ||
(!options?.config_entry && !entries?.length)
) {
// If the component isn't loaded, ask them to load the integration first
+18 -4
View File
@@ -606,10 +606,7 @@ export class HaChartBase extends LitElement {
id: "dataZoom",
type: "inside",
orient: "horizontal",
// "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,
filterMode: this._getDataZoomFilterMode() as any,
xAxisIndex: 0,
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
@@ -617,6 +614,23 @@ 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) {
+1 -1
View File
@@ -105,7 +105,7 @@ export class StateHistoryCharts extends LitElement {
@restoreScroll(".container") private _savedScrollPos?: number;
protected render() {
if (!isComponentLoaded(this.hass, "history")) {
if (!isComponentLoaded(this.hass.config, "history")) {
return html`<div class="info">
${this.hass.localize("ui.components.history_charts.history_disabled")}
</div>`;
+1 -1
View File
@@ -149,7 +149,7 @@ export class StatisticsChart extends LitElement {
}
protected render(): TemplateResult {
if (!isComponentLoaded(this.hass, "history")) {
if (!isComponentLoaded(this.hass.config, "history")) {
return html`<div class="info">
${this.hass.localize("ui.components.history_charts.history_disabled")}
</div>`;
+24 -19
View File
@@ -1,3 +1,4 @@
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";
@@ -21,9 +22,10 @@ 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";
@@ -104,9 +106,13 @@ const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
@customElement("ha-data-table")
export class HaDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: localizeContext, subscribe: true })
private _localize?: ContextType<typeof localizeContext>;
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
@state()
@consume({ context: localeContext, subscribe: true })
private _locale?: ContextType<typeof localeContext>;
@property({ type: Boolean }) public narrow = false;
@@ -378,8 +384,6 @@ 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) =>
@@ -503,7 +507,8 @@ 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 ||
localize("ui.components.data-table.no-data")}
this._localize?.("ui.components.data-table.no-data") ||
"No data"}
</div>
</div>
</div>
@@ -515,7 +520,8 @@ export class HaDataTable extends LitElement {
@scroll=${this._saveScrollPos}
.items=${this._groupData(
this._filteredData,
localize,
this._localize,
this._locale,
this.appendRow,
this.groupColumn,
this.groupOrder,
@@ -685,7 +691,7 @@ export class HaDataTable extends LitElement {
this._sortColumns[this.sortColumn],
this.sortDirection,
this.sortColumn,
this.hass.locale.language
this._locale?.language
)
: filteredData;
@@ -711,7 +717,8 @@ export class HaDataTable extends LitElement {
private _groupData = memoizeOne(
(
data: DataTableRowData[],
localize: LocalizeFunc,
localize: LocalizeFunc | undefined,
locale: FrontendLocaleData | undefined,
appendRow,
groupColumn: string | undefined,
groupOrder: string[] | undefined,
@@ -735,11 +742,7 @@ export class HaDataTable extends LitElement {
)
.sort((a, b) => {
if (!groupOrder && isGroupSortColumn) {
const comparison = stringCompare(
a,
b,
this.hass.locale.language
);
const comparison = stringCompare(a, b, locale?.language);
if (sortDirection === "asc") {
return comparison;
}
@@ -760,7 +763,7 @@ export class HaDataTable extends LitElement {
return stringCompare(
["", "-", "—"].includes(a) ? "zzz" : a,
["", "-", "—"].includes(b) ? "zzz" : b,
this.hass.locale.language
locale?.language
);
})
.reduce(
@@ -787,14 +790,15 @@ export class HaDataTable extends LitElement {
>
<ha-icon-button
.path=${mdiChevronUp}
.label=${this.hass.localize(
.label=${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")
? localize?.("ui.components.data-table.ungrouped") ||
"Ungrouped"
: groupName || ""}
</div>`,
});
@@ -863,7 +867,8 @@ export class HaDataTable extends LitElement {
const groupedData = this._groupData(
this._filteredData,
this.localizeFunc || this.hass.localize,
this._localize,
this._locale,
this.appendRow,
this.groupColumn,
this.groupOrder,
+62 -17
View File
@@ -19,7 +19,12 @@ 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";
@@ -27,12 +32,12 @@ 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";
import type { HaTimeInput } from "../ha-time-input";
@customElement("date-range-picker")
export class DateRangePicker extends LitElement {
export class DateRangePicker extends MobileAwareMixin(LitElement) {
@property({ attribute: false }) public ranges?: DateRangePickerRanges | false;
@property({ attribute: false }) public startDate?: Date;
@@ -103,16 +108,38 @@ export class DateRangePicker extends 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">
<ha-list @action=${this._setDateRange} activatable>
${Object.keys(this.ranges).map(
(name) => html`<ha-list-item>${name}</ha-list-item>`
)}
</ha-list>
</div>`
? html`<div class="date-range-ranges">${this._renderRanges()}</div>`
: nothing}
<div class="range">
<calendar-range
@@ -270,18 +297,30 @@ export class DateRangePicker extends 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) {
fireEvent(this, "value-changed", {
value: {
startDate: dateRange[0],
endDate: dateRange[1],
startDate: range[0],
endDate: range[1],
},
});
fireEvent(this, "preset-selected", {
index: ev.detail.index,
index,
});
}
@@ -306,6 +345,7 @@ export class DateRangePicker extends LitElement {
static styles = [
datePickerStyles,
dateRangePickerStyles,
haStyleScrollbar,
css`
.picker {
display: flex;
@@ -313,7 +353,7 @@ export class DateRangePicker extends LitElement {
}
.date-range-ranges {
border-right: 1px solid var(--divider-color);
border-right: var(--ha-border-width-sm) solid var(--divider-color);
min-width: 140px;
flex: 0 1 30%;
}
@@ -327,16 +367,21 @@ export class DateRangePicker extends LitElement {
overflow-x: hidden;
}
@media only screen and (max-width: 460px) {
@media all and (max-width: 450px), all and (max-height: 500px) {
.picker {
flex-direction: column;
}
.date-range-ranges {
flex-basis: 180px;
border-bottom: 1px solid var(--divider-color);
border-right: none;
overflow-y: scroll;
margin-top: var(--ha-space-5);
overflow: visible;
}
ha-chip-set {
padding: var(--ha-space-3);
flex-wrap: nowrap;
overflow-x: auto;
}
.range {
+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 { getDeviceContext } from "../../common/entity/context/get_device_context";
import { getDeviceArea } 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 } = getDeviceContext(device, this.hass);
const area = getDeviceArea(device, this.hass.areas);
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, "hassio")) {
if (isComponentLoaded(this.hass.config, "hassio")) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = addonsInfo.addons
.filter((addon) => addon.version)
+4 -1
View File
@@ -8,6 +8,7 @@ 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";
@@ -38,6 +39,8 @@ class HaAlert extends LitElement {
@property({ type: Boolean }) public dismissable = false;
@property({ attribute: false }) public localize?: LocalizeFunc;
@property({ type: Boolean }) public narrow = false;
public render() {
@@ -65,7 +68,7 @@ class HaAlert extends LitElement {
${this.dismissable
? html`<ha-icon-button
@click=${this._dismissClicked}
label="Dismiss alert"
.label=${this.localize!("ui.common.dismiss_alert")}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
@@ -267,7 +267,6 @@ 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>`
-2
View File
@@ -79,7 +79,6 @@ 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>
@@ -115,7 +114,6 @@ 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,6 +123,9 @@ export class HaDateInput extends LitElement {
}
static styles = css`
:host {
min-width: 0px;
}
ha-svg-icon {
color: var(--secondary-text-color);
}
+29 -8
View File
@@ -1,19 +1,23 @@
import { consume, type ContextType } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } 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;
@@ -25,6 +29,22 @@ 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>`;
@@ -34,12 +54,13 @@ export class HaDomainIcon extends LitElement {
return nothing;
}
if (!this.hass) {
if (!this._connection || !this._hassConfig) {
return this._renderFallback();
}
const icon = domainIcon(
this.hass,
this._connection,
this._hassConfig,
this.domain,
this.deviceClass,
this.state
@@ -65,9 +86,9 @@ export class HaDomainIcon extends LitElement {
{
domain: this.domain!,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
darkOptimized: this._themes?.darkMode,
},
this.hass.auth.data.hassUrl
this._auth?.data.hassUrl
);
return html`
<img
+12 -4
View File
@@ -101,7 +101,11 @@ export class HaFilterDevices extends LitElement {
.value=${device.id}
.selected=${this.value?.includes(device.id) ?? false}
>
${computeDeviceNameDisplay(device, this.hass)}
${computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)}
</ha-check-list-item>`;
private _handleItemClick(ev) {
@@ -151,14 +155,18 @@ export class HaFilterDevices extends LitElement {
.filter(
(device) =>
!filter ||
computeDeviceNameDisplay(device, this.hass)
computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)
.toLowerCase()
.includes(filter)
)
.sort((a, b) =>
stringCompare(
computeDeviceNameDisplay(a, this.hass),
computeDeviceNameDisplay(b, this.hass),
computeDeviceNameDisplay(a, this.hass.localize, this.hass.states),
computeDeviceNameDisplay(b, this.hass.localize, this.hass.states),
this.hass.locale.language
)
);
-1
View File
@@ -72,7 +72,6 @@ export class HaFilterDomains extends LitElement {
>
<ha-domain-icon
slot="graphic"
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>
-1
View File
@@ -82,7 +82,6 @@ export class HaFilterIntegrations extends LitElement {
>
<ha-domain-icon
slot="graphic"
.hass=${this.hass}
.domain=${integration.domain}
brand-fallback
></ha-domain-icon>
+3 -1
View File
@@ -100,7 +100,9 @@ export class HaFormInteger extends LitElement implements HaFormElement {
inputMode="numeric"
.label=${this.label}
.hint=${this.helper}
.value=${this.data !== undefined ? this.data.toString() : ""}
.value=${this.data !== undefined && this.data !== null
? this.data.toString()
: ""}
.disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
+108 -78
View File
@@ -1,4 +1,4 @@
import type { PropertyValues, TemplateResult } from "lit";
import type { PropertyValues } from "lit";
import { css, LitElement, svg } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
@@ -54,6 +54,7 @@ export class HaGauge extends LitElement {
this._angle = getAngle(this.value, this.min, this.max);
}
this._segment_label = this._getSegmentLabel();
this._rescaleSvg();
});
}
@@ -70,6 +71,7 @@ export class HaGauge extends LitElement {
}
this._angle = getAngle(this.value, this.min, this.max);
this._segment_label = this._getSegmentLabel();
this._rescaleSvg();
}
protected render() {
@@ -88,87 +90,91 @@ export class HaGauge extends LitElement {
/>
${
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;
${
this.levels
? (() => {
const sortedLevels = [...this.levels].sort(
(a, b) => a.level - b.level
);
const startAngle = getAngle(startLevel, this.min, this.max);
const endAngle = getAngle(endLevel, this.min, this.max);
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
if (
sortedLevels.length > 0 &&
sortedLevels[0].level !== this.min
) {
sortedLevels.unshift({
level: this.min,
stroke: "var(--info-color)",
});
}
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);
return sortedLevels.map((level, i, arr) => {
const startLevel = level.level;
const endLevel =
i + 1 < arr.length ? arr[i + 1].level : this.max;
const firstSegment = i === 0;
const lastSegment = i === arr.length - 1;
const startAngle = getAngle(startLevel, this.min, this.max);
const endAngle = getAngle(endLevel, this.min, this.max);
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
const paths: TemplateResult[] = [];
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);
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);
const isFirst = i === 0;
const isLast = i === arr.length - 1;
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 (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: 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}"
/>
`);
}
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);
return paths;
})
: ""
}
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>
`;
});
})()
: ""
}
${
this.needle
? svg`
<line
class="needle"
x1="-35.0"
y1="0"
x2="-45.0"
y2="0"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
/>
<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)` })}
/>
`
: svg`
<path
@@ -179,7 +185,8 @@ export class HaGauge extends LitElement {
/>
`
}
</svg>
<svg class="text">
<text
class="value-text"
x="0"
@@ -204,6 +211,18 @@ 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);
@@ -224,32 +243,43 @@ export class HaGauge extends LitElement {
.levels-base {
fill: none;
stroke: var(--primary-background-color);
stroke-width: 8;
stroke-linecap: round;
stroke-width: 6;
stroke-linecap: butt;
}
.level {
fill: none;
stroke-width: 8;
stroke-width: 6;
stroke-linecap: butt;
}
.value {
fill: none;
stroke-width: 8;
stroke-width: 6;
stroke: var(--gauge-color);
stroke-linecap: round;
stroke-linecap: butt;
transition: stroke-dashoffset 1s ease 0s;
}
.needle {
stroke: var(--primary-text-color);
stroke-width: 2;
fill: var(--primary-text-color);
stroke: var(--card-background-color);
color: var(--primary-text-color);
stroke-width: 1;
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!, "stream")) {
if (!isComponentLoaded(this.hass.config, "stream")) {
this._setFatalError("Streaming component is not loaded.");
return;
}
+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, "hassio")) {
if (isComponentLoaded(this.hass.config, "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,7 +132,6 @@ export class HaNavigationPicker extends LitElement {
? html`
<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.domain}
brand-fallback
></ha-domain-icon>
@@ -158,7 +157,6 @@ export class HaNavigationPicker extends LitElement {
? html`
<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.domain}
brand-fallback
></ha-domain-icon>
+4 -8
View File
@@ -798,11 +798,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
}
ha-input-search {
padding: 0 var(--ha-space-3);
padding: 0 var(--ha-space-3) var(--ha-space-3);
}
:host([mode="dialog"]) ha-input-search {
padding: 0 var(--ha-space-4);
padding: 0 var(--ha-space-4) var(--ha-space-3);
}
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: var(--ha-space-3) var(--ha-space-3);
padding: 0 var(--ha-space-3) var(--ha-space-3);
overflow: auto;
}
:host([mode="dialog"]) .sections {
padding: var(--ha-space-3) var(--ha-space-4);
padding: 0 var(--ha-space-4) var(--ha-space-3);
}
.sections ha-filter-chip {
@@ -915,10 +915,6 @@ 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;
-310
View File
@@ -1,310 +0,0 @@
export const RETRO_THEME = {
// Sharp corners
"ha-border-radius-sm": "0",
"ha-border-radius-md": "0",
"ha-border-radius-lg": "0",
"ha-border-radius-xl": "0",
"ha-border-radius-2xl": "0",
"ha-border-radius-3xl": "0",
"ha-border-radius-4xl": "0",
"ha-border-radius-5xl": "0",
"ha-border-radius-6xl": "0",
"ha-border-radius-pill": "0",
"ha-border-radius-circle": "0",
// Fonts
"ha-font-family-body":
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
"ha-font-family-heading":
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
"ha-font-family-code": "'Courier New', Courier, monospace",
"ha-font-family-longform":
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
// No transparency
"ha-dialog-scrim-backdrop-filter": "none",
// Disable animations
"ha-animation-duration-fast": "1ms",
"ha-animation-duration-normal": "1ms",
"ha-animation-duration-slow": "1ms",
modes: {
light: {
// Base colors
"primary-color": "#000080",
"dark-primary-color": "#00006B",
"light-primary-color": "#4040C0",
"accent-color": "#000080",
"primary-text-color": "#000000",
"secondary-text-color": "#404040",
"text-primary-color": "#ffffff",
"text-light-primary-color": "#000000",
"disabled-text-color": "#808080",
// Backgrounds
"primary-background-color": "#C0C0C0",
"lovelace-background": "#008080",
"secondary-background-color": "#C0C0C0",
"card-background-color": "#C0C0C0",
"clear-background-color": "#C0C0C0",
// RGB values
"rgb-primary-color": "0, 0, 128",
"rgb-accent-color": "0, 0, 128",
"rgb-primary-text-color": "0, 0, 0",
"rgb-secondary-text-color": "64, 64, 64",
"rgb-text-primary-color": "255, 255, 255",
"rgb-card-background-color": "192, 192, 192",
// UI chrome
"divider-color": "#808080",
"outline-color": "#808080",
"outline-hover-color": "#404040",
"shadow-color": "rgba(0, 0, 0, 0.5)",
"scrollbar-thumb-color": "#808080",
"disabled-color": "#808080",
// Cards - retro bevel effect
"ha-card-border-width": "1px",
"ha-card-border-color": "#808080",
"ha-card-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"ha-card-border-radius": "0",
// Dialogs
"ha-dialog-border-radius": "0",
"ha-dialog-surface-background": "#C0C0C0",
"dialog-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
// Box shadows - retro bevel
"ha-box-shadow-s": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"ha-box-shadow-m": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"ha-box-shadow-l": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
// Header
"app-header-background-color": "#000080",
"app-header-text-color": "#ffffff",
"app-header-border-bottom": "2px outset #C0C0C0",
// Sidebar
"sidebar-background-color": "#C0C0C0",
"sidebar-text-color": "#000000",
"sidebar-selected-text-color": "#ffffff",
"sidebar-selected-icon-color": "#000080",
"sidebar-icon-color": "#000000",
// Input
"input-fill-color": "#C0C0C0",
"input-disabled-fill-color": "#C0C0C0",
"input-ink-color": "#000000",
"input-label-ink-color": "#000000",
"input-disabled-ink-color": "#808080",
"input-idle-line-color": "#808080",
"input-hover-line-color": "#000000",
"input-disabled-line-color": "#808080",
"input-outlined-idle-border-color": "#808080",
"input-outlined-hover-border-color": "#000000",
"input-outlined-disabled-border-color": "#C0C0C0",
"input-dropdown-icon-color": "#000000",
// Status colors
"error-color": "#FF0000",
"warning-color": "#FF8000",
"success-color": "#008000",
"info-color": "#000080",
// State
"state-icon-color": "#000080",
"state-active-color": "#000080",
"state-inactive-color": "#808080",
// Data table
"data-table-border-width": "0",
// Primary scale
"ha-color-primary-05": "#00003A",
"ha-color-primary-10": "#000050",
"ha-color-primary-20": "#000066",
"ha-color-primary-30": "#00007A",
"ha-color-primary-40": "#000080",
"ha-color-primary-50": "#0000AA",
"ha-color-primary-60": "#4040C0",
"ha-color-primary-70": "#6060D0",
"ha-color-primary-80": "#8080E0",
"ha-color-primary-90": "#C8C8D8",
"ha-color-primary-95": "#D8D8E0",
// Neutral scale
"ha-color-neutral-05": "#000000",
"ha-color-neutral-10": "#2A2A2A",
"ha-color-neutral-20": "#404040",
"ha-color-neutral-30": "#606060",
"ha-color-neutral-40": "#707070",
"ha-color-neutral-50": "#808080",
"ha-color-neutral-60": "#909090",
"ha-color-neutral-70": "#A0A0A0",
"ha-color-neutral-80": "#B0B0B0",
"ha-color-neutral-90": "#C8C8C8",
"ha-color-neutral-95": "#D0D0D0",
// Codemirror
"codemirror-keyword": "#000080",
"codemirror-operator": "#000000",
"codemirror-variable": "#008080",
"codemirror-variable-2": "#000080",
"codemirror-variable-3": "#808000",
"codemirror-builtin": "#800080",
"codemirror-atom": "#008080",
"codemirror-number": "#FF0000",
"codemirror-def": "#000080",
"codemirror-string": "#008000",
"codemirror-string-2": "#808000",
"codemirror-comment": "#808080",
"codemirror-tag": "#800000",
"codemirror-meta": "#000080",
"codemirror-attribute": "#FF0000",
"codemirror-property": "#000080",
"codemirror-qualifier": "#808000",
"codemirror-type": "#000080",
},
dark: {
// Base colors
"primary-color": "#4040C0",
"dark-primary-color": "#000080",
"light-primary-color": "#6060D0",
"accent-color": "#4040C0",
"primary-text-color": "#C0C0C0",
"secondary-text-color": "#A0A0A0",
"text-primary-color": "#ffffff",
"text-light-primary-color": "#C0C0C0",
"disabled-text-color": "#606060",
// Backgrounds
"primary-background-color": "#2A2A2A",
"lovelace-background": "#003030",
"secondary-background-color": "#2A2A2A",
"card-background-color": "#3A3A3A",
"clear-background-color": "#2A2A2A",
// RGB values
"rgb-primary-color": "64, 64, 192",
"rgb-accent-color": "64, 64, 192",
"rgb-primary-text-color": "192, 192, 192",
"rgb-secondary-text-color": "160, 160, 160",
"rgb-text-primary-color": "255, 255, 255",
"rgb-card-background-color": "58, 58, 58",
// UI chrome
"divider-color": "#606060",
"outline-color": "#606060",
"outline-hover-color": "#808080",
"shadow-color": "rgba(0, 0, 0, 0.7)",
"scrollbar-thumb-color": "#606060",
"disabled-color": "#606060",
// Cards - retro bevel effect
"ha-card-border-width": "1px",
"ha-card-border-color": "#606060",
"ha-card-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"ha-card-border-radius": "0",
// Dialogs
"ha-dialog-border-radius": "0",
"ha-dialog-surface-background": "#3A3A3A",
"dialog-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
// Box shadows - retro bevel
"ha-box-shadow-s": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"ha-box-shadow-m": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"ha-box-shadow-l": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
// Header
"app-header-background-color": "#000060",
"app-header-text-color": "#ffffff",
"app-header-border-bottom": "2px outset #3A3A3A",
// Sidebar
"sidebar-background-color": "#2A2A2A",
"sidebar-text-color": "#C0C0C0",
"sidebar-selected-text-color": "#ffffff",
"sidebar-selected-icon-color": "#4040C0",
"sidebar-icon-color": "#A0A0A0",
// Input
"input-fill-color": "#3A3A3A",
"input-disabled-fill-color": "#3A3A3A",
"input-ink-color": "#C0C0C0",
"input-label-ink-color": "#A0A0A0",
"input-disabled-ink-color": "#606060",
"input-idle-line-color": "#606060",
"input-hover-line-color": "#808080",
"input-disabled-line-color": "#404040",
"input-outlined-idle-border-color": "#606060",
"input-outlined-hover-border-color": "#808080",
"input-outlined-disabled-border-color": "#404040",
"input-dropdown-icon-color": "#A0A0A0",
// Status colors
"error-color": "#FF4040",
"warning-color": "#FFA040",
"success-color": "#40C040",
"info-color": "#4040C0",
// State
"state-icon-color": "#4040C0",
"state-active-color": "#4040C0",
"state-inactive-color": "#606060",
// Data table
"data-table-border-width": "0",
// Primary scale
"ha-color-primary-05": "#00002A",
"ha-color-primary-10": "#000040",
"ha-color-primary-20": "#000060",
"ha-color-primary-30": "#000080",
"ha-color-primary-40": "#4040C0",
"ha-color-primary-50": "#6060D0",
"ha-color-primary-60": "#8080E0",
"ha-color-primary-70": "#A0A0F0",
"ha-color-primary-80": "#C0C0FF",
"ha-color-primary-90": "#3A3A58",
"ha-color-primary-95": "#303048",
// Neutral scale
"ha-color-neutral-05": "#1A1A1A",
"ha-color-neutral-10": "#2A2A2A",
"ha-color-neutral-20": "#3A3A3A",
"ha-color-neutral-30": "#4A4A4A",
"ha-color-neutral-40": "#606060",
"ha-color-neutral-50": "#707070",
"ha-color-neutral-60": "#808080",
"ha-color-neutral-70": "#909090",
"ha-color-neutral-80": "#A0A0A0",
"ha-color-neutral-90": "#C0C0C0",
"ha-color-neutral-95": "#D0D0D0",
// Codemirror
"codemirror-keyword": "#8080E0",
"codemirror-operator": "#C0C0C0",
"codemirror-variable": "#40C0C0",
"codemirror-variable-2": "#8080E0",
"codemirror-variable-3": "#C0C040",
"codemirror-builtin": "#C040C0",
"codemirror-atom": "#40C0C0",
"codemirror-number": "#FF6060",
"codemirror-def": "#8080E0",
"codemirror-string": "#40C040",
"codemirror-string-2": "#C0C040",
"codemirror-comment": "#808080",
"codemirror-tag": "#C04040",
"codemirror-meta": "#8080E0",
"codemirror-attribute": "#FF6060",
"codemirror-property": "#8080E0",
"codemirror-qualifier": "#C0C040",
"codemirror-type": "#8080E0",
"map-filter":
"invert(0.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) saturate(0.3)",
},
},
};
-683
View File
@@ -1,683 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
applyThemesOnElement,
invalidateThemeCache,
} from "../common/dom/apply_themes_on_element";
import type { LocalizeKeys } from "../common/translations/localize";
import { subscribeLabFeature } from "../data/labs";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import { RETRO_THEME } from "./ha-retro-theme";
const TIP_COUNT = 25;
type CasitaExpression =
| "hi"
| "ok-nabu"
| "heart"
| "sleep"
| "great-job"
| "error";
const STORAGE_KEY = "retro-position";
const DRAG_THRESHOLD = 5;
const BUBBLE_TIMEOUT = 8000;
const SLEEP_TIMEOUT = 30000;
const BSOD_CLICK_COUNT = 5;
const BSOD_CLICK_TIMEOUT = 3000;
const BSOD_DISMISS_DELAY = 500;
@customElement("ha-retro")
export class HaRetro extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _enabled = false;
public hassSubscribe() {
return [
subscribeLabFeature(
this.hass!.connection,
"frontend",
"retro",
(feature) => {
this._enabled = feature.enabled;
}
),
];
}
@state() private _casitaVisible = true;
@state() private _showBubble = false;
@state() private _bubbleText = "";
@state() private _expression: CasitaExpression = "hi";
@state() private _position: { x: number; y: number } | null = null;
@state() private _showBsod = false;
private _clickCount = 0;
private _clickTimer?: ReturnType<typeof setTimeout>;
private _dragging = false;
private _dragStartX = 0;
private _dragStartY = 0;
private _dragOffsetX = 0;
private _dragOffsetY = 0;
private _dragMoved = false;
private _bubbleTimer?: ReturnType<typeof setTimeout>;
private _sleepTimer?: ReturnType<typeof setTimeout>;
private _boundPointerMove = this._onPointerMove.bind(this);
private _boundPointerUp = this._onPointerUp.bind(this);
private _themeApplied = false;
private _isApplyingTheme = false;
private _themeObserver?: MutationObserver;
connectedCallback(): void {
super.connectedCallback();
this._loadPosition();
this._resetSleepTimer();
this._applyRetroTheme();
this._startThemeObserver();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._clearTimers();
this._stopThemeObserver();
this._revertTheme();
document.removeEventListener("pointermove", this._boundPointerMove);
document.removeEventListener("pointerup", this._boundPointerUp);
document.removeEventListener("keydown", this._boundDismissBsod);
}
protected willUpdate(changedProps: Map<string, unknown>): void {
if (changedProps.has("_enabled")) {
if (this._enabled) {
this.hass!.loadFragmentTranslation("retro");
this._applyRetroTheme();
this._startThemeObserver();
} else {
this._stopThemeObserver();
this._revertTheme();
}
}
if (changedProps.has("hass") && this._enabled) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
// Re-apply if darkMode changed
if (oldHass && oldHass.themes.darkMode !== this.hass!.themes.darkMode) {
this._themeApplied = false;
this._applyRetroTheme();
}
}
}
private _startThemeObserver(): void {
if (this._themeObserver) return;
this._themeObserver = new MutationObserver(() => {
if (this._isApplyingTheme || !this._enabled || !this.hass) return;
// Check if our theme was overwritten by the themes mixin
const el = document.documentElement as HTMLElement & {
__themes?: { cacheKey?: string };
};
if (!el.__themes?.cacheKey?.startsWith("Retro")) {
this._themeApplied = false;
this._applyRetroTheme();
}
});
this._themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["style"],
});
}
private _stopThemeObserver(): void {
this._themeObserver?.disconnect();
this._themeObserver = undefined;
}
private _applyRetroTheme(): void {
if (!this.hass || this._themeApplied) return;
this._isApplyingTheme = true;
const themes = {
...this.hass.themes,
themes: {
...this.hass.themes.themes,
Retro: RETRO_THEME,
},
};
invalidateThemeCache();
applyThemesOnElement(
document.documentElement,
themes,
"Retro",
{ dark: this.hass.themes.darkMode },
true
);
this._themeApplied = true;
this._isApplyingTheme = false;
}
private _revertTheme(): void {
if (!this.hass || !this._themeApplied) return;
this._isApplyingTheme = true;
invalidateThemeCache();
applyThemesOnElement(
document.documentElement,
this.hass.themes,
this.hass.selectedTheme?.theme || "default",
{
dark: this.hass.themes.darkMode,
primaryColor: this.hass.selectedTheme?.primaryColor,
accentColor: this.hass.selectedTheme?.accentColor,
},
true
);
this._themeApplied = false;
this._isApplyingTheme = false;
}
private _loadPosition(): void {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const pos = JSON.parse(stored);
if (typeof pos.x === "number" && typeof pos.y === "number") {
this._position = this._clampPosition(pos.x, pos.y);
}
}
} catch {
// Ignore invalid stored position
}
}
private _savePosition(): void {
if (this._position) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this._position));
} catch {
// Ignore storage errors
}
}
}
private _clampPosition(x: number, y: number): { x: number; y: number } {
const size = 80;
return {
x: Math.max(0, Math.min(window.innerWidth - size, x)),
y: Math.max(0, Math.min(window.innerHeight - size, y)),
};
}
private _onPointerDown(ev: PointerEvent): void {
if (ev.button !== 0 || this._showBsod) return;
this._dragging = true;
this._dragMoved = false;
this._dragStartX = ev.clientX;
this._dragStartY = ev.clientY;
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
this._dragOffsetX = ev.clientX - rect.left;
this._dragOffsetY = ev.clientY - rect.top;
(ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId);
document.addEventListener("pointermove", this._boundPointerMove);
document.addEventListener("pointerup", this._boundPointerUp);
ev.preventDefault();
}
private _onPointerMove(ev: PointerEvent): void {
if (!this._dragging) return;
const dx = ev.clientX - this._dragStartX;
const dy = ev.clientY - this._dragStartY;
if (!this._dragMoved && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
return;
}
this._dragMoved = true;
const x = ev.clientX - this._dragOffsetX;
const y = ev.clientY - this._dragOffsetY;
this._position = this._clampPosition(x, y);
}
private _onPointerUp(ev: PointerEvent): void {
document.removeEventListener("pointermove", this._boundPointerMove);
document.removeEventListener("pointerup", this._boundPointerUp);
this._dragging = false;
if (this._dragMoved) {
this._savePosition();
} else {
this._toggleBubble();
}
ev.preventDefault();
}
private _stopPropagation(ev: Event): void {
ev.stopPropagation();
}
private _dismiss(ev: Event): void {
ev.stopPropagation();
this._casitaVisible = false;
this._clearTimers();
}
private _toggleBubble(): void {
this._clickCount++;
if (this._clickTimer) {
clearTimeout(this._clickTimer);
}
this._clickTimer = setTimeout(() => {
this._clickCount = 0;
}, BSOD_CLICK_TIMEOUT);
if (this._clickCount >= BSOD_CLICK_COUNT) {
this._clickCount = 0;
this._triggerBsod();
return;
}
if (this._showBubble) {
this._hideBubble();
} else {
this._showTip();
}
}
private _boundDismissBsod = this._dismissBsodOnKey.bind(this);
private _bsodReadyToDismiss = false;
private _triggerBsod(): void {
this._hideBubble();
this._showBsod = true;
this._bsodReadyToDismiss = false;
this._expression = "error";
// Delay enabling dismiss so the rapid clicks that triggered the BSOD don't immediately close it
setTimeout(() => {
this._bsodReadyToDismiss = true;
document.addEventListener("keydown", this._boundDismissBsod);
}, BSOD_DISMISS_DELAY);
}
private _dismissBsod(): void {
if (!this._bsodReadyToDismiss) return;
this._showBsod = false;
this._expression = "hi";
this._resetSleepTimer();
document.removeEventListener("keydown", this._boundDismissBsod);
}
private _dismissBsodOnKey(): void {
this._dismissBsod();
}
private _showTip(): void {
const tipIndex = Math.floor(Math.random() * TIP_COUNT) + 1;
this._bubbleText = this.hass!.localize(
`ui.panel.retro.tip_${tipIndex}` as LocalizeKeys
);
this._showBubble = true;
this._expression = "ok-nabu";
this._resetSleepTimer();
if (this._bubbleTimer) {
clearTimeout(this._bubbleTimer);
}
this._bubbleTimer = setTimeout(() => {
this._hideBubble();
}, BUBBLE_TIMEOUT);
}
private _hideBubble(): void {
this._showBubble = false;
this._expression = "hi";
this._resetSleepTimer();
if (this._bubbleTimer) {
clearTimeout(this._bubbleTimer);
this._bubbleTimer = undefined;
}
}
private _closeBubble(ev: Event): void {
ev.stopPropagation();
this._hideBubble();
}
private _resetSleepTimer(): void {
if (this._sleepTimer) {
clearTimeout(this._sleepTimer);
}
this._sleepTimer = setTimeout(() => {
if (!this._showBubble) {
this._expression = "sleep";
}
}, SLEEP_TIMEOUT);
}
private _clearTimers(): void {
if (this._bubbleTimer) {
clearTimeout(this._bubbleTimer);
this._bubbleTimer = undefined;
}
if (this._sleepTimer) {
clearTimeout(this._sleepTimer);
this._sleepTimer = undefined;
}
if (this._clickTimer) {
clearTimeout(this._clickTimer);
this._clickTimer = undefined;
}
}
protected render() {
if (!this._enabled || !this._casitaVisible) {
return nothing;
}
const size = 80;
const posStyle = this._position
? `left: ${this._position.x}px; top: ${this._position.y}px;`
: `right: 16px; bottom: 16px;`;
return html`
${this._showBsod
? html`
<div class="bsod" @click=${this._dismissBsod}>
<div class="bsod-content">
<h1 class="bsod-title">
${this.hass!.localize("ui.panel.retro.bsod_title")}
</h1>
<p>${this.hass!.localize("ui.panel.retro.bsod_error")}</p>
<p>
* ${this.hass!.localize("ui.panel.retro.bsod_line_1")}<br />
* ${this.hass!.localize("ui.panel.retro.bsod_line_2")}
</p>
<p class="bsod-prompt">
${this.hass!.localize("ui.panel.retro.bsod_continue")}
<span class="bsod-cursor">_</span>
</p>
</div>
</div>
`
: nothing}
<div
class="casita-container ${this._dragging ? "dragging" : ""}"
style="width: ${size}px; ${posStyle}"
aria-hidden="true"
@pointerdown=${this._onPointerDown}
>
${this._showBubble
? html`
<div class="speech-bubble">
<span class="bubble-text">${this._bubbleText}</span>
<button
class="bubble-close"
@pointerdown=${this._stopPropagation}
@click=${this._closeBubble}
>
</button>
<button
class="bubble-dismiss"
@pointerdown=${this._stopPropagation}
@click=${this._dismiss}
>
${this.hass!.localize("ui.panel.retro.dismiss")}
</button>
<div class="bubble-arrow"></div>
</div>
`
: nothing}
<img
class="casita-image"
src="/static/images/voice-assistant/${this._expression}.png"
alt="Casita"
draggable="false"
/>
</div>
`;
}
static readonly styles = css`
:host {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
user-select: none;
z-index: 9999;
}
.casita-container {
position: fixed;
pointer-events: auto;
cursor: grab;
user-select: none;
touch-action: none;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
}
.casita-container.dragging {
cursor: grabbing;
}
.casita-image {
width: 100%;
height: auto;
animation: bob 3s ease-in-out infinite;
pointer-events: none;
}
.dragging .casita-image {
animation: none;
}
.speech-bubble {
position: absolute;
bottom: calc(100% + 8px);
right: 0;
background: #ffffe1;
color: #000000;
border-radius: 12px;
border: 2px solid #000000;
padding: 12px 28px 12px 12px;
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
font-size: 14px;
line-height: 1.4;
width: 300px;
box-sizing: border-box;
word-wrap: break-word;
overflow-wrap: break-word;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
animation: bubble-in 200ms ease-out;
pointer-events: auto;
}
.bubble-close {
position: absolute;
top: 4px;
right: 4px;
background: none;
border: none;
cursor: pointer;
color: #000000;
font-size: 14px;
padding: 2px 6px;
line-height: 1;
border-radius: 50%;
}
.bubble-close:hover {
background: #e0e0c0;
}
.bubble-dismiss {
display: block;
margin-top: 8px;
background: none;
border: none;
cursor: pointer;
color: #808080;
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
font-size: 12px;
padding: 0;
text-decoration: underline;
}
.bubble-dismiss:hover {
color: #000000;
}
.bubble-arrow {
position: absolute;
bottom: -8px;
right: 32px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #ffffe1;
}
@keyframes bob {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}
@keyframes bubble-in {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.bsod {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #0000aa;
color: #ffffff;
font-family: "Lucida Console", "Courier New", monospace;
font-size: 16px;
line-height: 1.6;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
pointer-events: auto;
animation: bsod-in 100ms ease-out;
}
.bsod-content {
max-width: 700px;
padding: 32px;
text-align: left;
}
.bsod-title {
display: inline-block;
background: #aaaaaa;
color: #0000aa;
padding: 2px 12px;
font-size: 18px;
font-weight: normal;
margin: 0 0 24px;
}
.bsod-content p {
margin: 16px 0;
}
.bsod-prompt {
margin-top: 32px;
}
.bsod-cursor {
animation: blink 1s step-end infinite;
}
@keyframes bsod-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes blink {
50% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.casita-image {
animation: none;
}
.speech-bubble {
animation: none;
}
.bsod {
animation: none;
}
.bsod-cursor {
animation: none;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-retro": HaRetro;
}
}
@@ -0,0 +1,144 @@
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,6 +41,7 @@ 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"),
+1 -8
View File
@@ -516,17 +516,10 @@ 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
>
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
<ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(
serviceData.target as TargetSelector,
+15 -3
View File
@@ -1,5 +1,6 @@
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-settings-row")
@@ -16,17 +17,28 @@ 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}
?two-line=${!this.threeLine && hasDescription}
?three-line=${this.threeLine}
>
<slot name="heading"></slot>
<div class="secondary"><slot name="description"></slot></div>
${hasDescription
? html`<span class="secondary"
><slot name="description"></slot
></span>`
: nothing}
</div>
</div>
<div class="content">
+7 -7
View File
@@ -1,19 +1,19 @@
import type { PropertyValues } from "lit";
import { html, css, LitElement, nothing } from "lit";
import { mdiStarFourPoints } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
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, "ai_task")) {
if (!this.hass || !isComponentLoaded(this.hass.config, "ai_task")) {
return;
}
fetchAITaskPreferences(this.hass).then((prefs) => {
-294
View File
@@ -1,294 +0,0 @@
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;
}
}
+13 -4
View File
@@ -19,6 +19,7 @@ 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"
@@ -235,7 +236,8 @@ export class HaInput extends LitElement {
this,
"label",
"hint",
"input"
"input",
"start"
);
static shadowRootOptions: ShadowRootInit = {
@@ -287,7 +289,9 @@ export class HaInput extends LitElement {
}
public checkValidity(): boolean {
return this._input?.checkValidity() ?? true;
return nativeElementInternalsSupported
? (this._input?.checkValidity() ?? true)
: true;
}
public reportValidity(): boolean {
@@ -318,6 +322,8 @@ export class HaInput extends LitElement {
? false
: this._hasSlotController.test("hint");
const hasStartSlot = this._hasSlotController.test("start");
return html`
<wa-input
.type=${this.type}
@@ -348,7 +354,8 @@ export class HaInput extends LitElement {
invalid: this.invalid || this._invalid,
"label-raised":
(this.value !== undefined && this.value !== "") ||
(this.label && this.placeholder),
(this.label && this.placeholder) ||
(hasStartSlot && this.insetLabel),
"no-label": !this.label,
"hint-hidden":
!this.hint &&
@@ -589,6 +596,7 @@ 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) {
@@ -625,7 +633,7 @@ export class HaInput extends LitElement {
}
wa-input::part(hint) {
height: var(--ha-space-5);
min-height: var(--ha-space-5);
margin-block-start: 0;
margin-inline-start: var(--ha-space-3);
font-size: var(--ha-font-size-xs);
@@ -636,6 +644,7 @@ 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 {
mdiClose,
mdiDelete,
mdiCheckboxBlankOutline,
mdiCheckboxMarkedOutline,
mdiClose,
mdiDelete,
} 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-header";
import "../ha-dialog-footer";
import "../ha-dialog-header";
import "../ha-icon-button";
import "../ha-list";
import "../ha-spinner";
@@ -227,7 +227,7 @@ class DialogMediaManage extends LitElement {
)}
</ha-list>
`}
${isComponentLoaded(this.hass, "hassio")
${isComponentLoaded(this.hass.config, "hassio")
? html`<ha-tip .hass=${this.hass}>
${this.hass.localize(
"ui.components.media-browser.file_management.tip_media_storage",
@@ -524,7 +524,13 @@ export class HaTargetPickerItemRow extends LitElement {
}
return {
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
name: device
? computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)
: item,
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
fallbackIconPath: mdiDevices,
notFound: !device,
@@ -161,7 +161,13 @@ export class HaTargetPickerValueChip extends LitElement {
}
return {
name: device ? computeDeviceNameDisplay(device, this.hass) : itemId,
name: device
? computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)
: itemId,
fallbackIconPath: mdiDevices,
};
}
+25 -3
View File
@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/skeleton/skeleton";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
@@ -12,6 +13,8 @@ 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)`.
@@ -29,21 +32,31 @@ 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>
<slot name="secondary" class="secondary">
<span>${this.secondary}</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>`}
</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)
@@ -112,6 +125,15 @@ 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);
}
`;
}
+3 -1
View File
@@ -32,7 +32,9 @@ export class HaTraceLogbook extends LitElement {
></hat-logbook-note>
`
: html`<div class="padded-box">
No Logbook entries found for this step.
${this.hass.localize(
"ui.panel.config.automation.trace.path.no_logbook_entries"
)}
</div>`;
}
+26 -13
View File
@@ -78,6 +78,19 @@ 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,
@@ -233,8 +246,8 @@ const describeLegacyTrigger = (
attribute: attribute,
entity: formatListWithOrs(hass.locale, entities),
numberOfEntities: entities.length,
above: trigger.above,
below: trigger.below,
above: formatNumericLimitValue(hass, trigger.above),
below: formatNumericLimitValue(hass, trigger.below),
duration: duration,
}
);
@@ -246,7 +259,7 @@ const describeLegacyTrigger = (
attribute: attribute,
entity: formatListWithOrs(hass.locale, entities),
numberOfEntities: entities.length,
above: trigger.above,
above: formatNumericLimitValue(hass, trigger.above),
duration: duration,
}
);
@@ -258,7 +271,7 @@ const describeLegacyTrigger = (
attribute: attribute,
entity: formatListWithOrs(hass.locale, entities),
numberOfEntities: entities.length,
below: trigger.below,
below: formatNumericLimitValue(hass, trigger.below),
duration: duration,
}
);
@@ -813,16 +826,16 @@ const describeLegacyTrigger = (
: trigger.entity_id;
let offsetChoice = "other";
let offset: string | string[] = "";
let offset = "";
if (trigger.offset) {
offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
offset = trigger.offset.startsWith("-")
const parts = trigger.offset.startsWith("-")
? trigger.offset.substring(1).split(":")
: trigger.offset.split(":");
const duration = {
hours: offset.length > 0 ? +offset[0] : 0,
minutes: offset.length > 1 ? +offset[1] : 0,
seconds: offset.length > 2 ? +offset[2] : 0,
hours: parts.length > 0 ? +parts[0] : 0,
minutes: parts.length > 1 ? +parts[1] : 0,
seconds: parts.length > 2 ? +parts[2] : 0,
};
offset = formatDurationLong(hass.locale, duration);
if (offset === "") {
@@ -1116,8 +1129,8 @@ const describeLegacyCondition = (
attribute,
entity,
numberOfEntities: entity_ids.length,
above: condition.above,
below: condition.below,
above: formatNumericLimitValue(hass, condition.above),
below: formatNumericLimitValue(hass, condition.below),
}
);
}
@@ -1128,7 +1141,7 @@ const describeLegacyCondition = (
attribute,
entity,
numberOfEntities: entity_ids.length,
above: condition.above,
above: formatNumericLimitValue(hass, condition.above),
}
);
}
@@ -1139,7 +1152,7 @@ const describeLegacyCondition = (
attribute,
entity,
numberOfEntities: entity_ids.length,
below: condition.below,
below: formatNumericLimitValue(hass, condition.below),
}
);
}
+4 -3
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 { getDeviceContext } from "../../common/entity/context/get_device_context";
import { getDeviceArea } 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,11 +144,12 @@ export const getDevices = (
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
const deviceName = computeDeviceNameDisplay(
device,
hass,
hass.localize,
hass.states,
deviceEntityLookup[device.id]
);
const { area } = getDeviceContext(device, hass);
const area = getDeviceArea(device, hass.areas);
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, "hassio") &&
isComponentLoaded(hass.config, "hassio") &&
atLeastVersion(hass.config.version, 2025, 10)
? "/api/hassio/core/logs/latest"
: "/api/error_log";
+1
View File
@@ -20,6 +20,7 @@ export interface CoreFrontendSystemData {
export interface HomeFrontendSystemData {
favorite_entities?: string[];
welcome_banner_dismissed?: boolean;
hidden_summaries?: string[];
}
declare global {
+77 -30
View File
@@ -3,8 +3,10 @@ import {
mdiAirFilter,
mdiAlert,
mdiAppleSafari,
mdiBattery,
mdiBell,
mdiBookmark,
mdiBrightness6,
mdiBullhorn,
mdiButtonPointer,
mdiCalendar,
@@ -16,13 +18,18 @@ import {
mdiCog,
mdiCommentAlert,
mdiCounter,
mdiDoorOpen,
mdiEye,
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
mdiFormTextbox,
mdiForumOutline,
mdiGarageOpen,
mdiGate,
mdiGoogleAssistant,
mdiGoogleCirclesCommunities,
mdiHomeAccount,
mdiHomeAutomation,
mdiImage,
mdiImageFilterFrames,
@@ -30,6 +37,7 @@ import {
mdiLightbulb,
mdiMapMarkerRadius,
mdiMicrophoneMessage,
mdiMotionSensor,
mdiPalette,
mdiRayVertex,
mdiRemote,
@@ -40,13 +48,17 @@ import {
mdiScriptText,
mdiSpeakerMessage,
mdiStarFourPoints,
mdiThermometer,
mdiThermostat,
mdiTimerOutline,
mdiToggleSwitch,
mdiWater,
mdiWaterPercent,
mdiWeatherPartlyCloudy,
mdiWhiteBalanceSunny,
mdiWindowClosed,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { Connection, 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";
@@ -60,6 +72,7 @@ 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";
@@ -75,6 +88,7 @@ export const FALLBACK_DOMAIN_ICONS = {
air_quality: mdiAirFilter,
alert: mdiAlert,
automation: mdiRobot,
battery: mdiBattery,
calendar: mdiCalendar,
climate: mdiThermostat,
configurator: mdiCog,
@@ -84,10 +98,15 @@ 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,
@@ -99,11 +118,15 @@ 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,
@@ -115,6 +138,7 @@ export const FALLBACK_DOMAIN_ICONS = {
siren: mdiBullhorn,
stt: mdiMicrophoneMessage,
sun: mdiWhiteBalanceSunny,
temperature: mdiThermometer,
text: mdiFormTextbox,
time: mdiClock,
timer: mdiTimerOutline,
@@ -124,6 +148,7 @@ export const FALLBACK_DOMAIN_ICONS = {
vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep,
weather: mdiWeatherPartlyCloudy,
window: mdiWindowClosed,
zone: mdiMapMarkerRadius,
};
@@ -229,18 +254,19 @@ interface CategoryType {
}
export const getHassIcons = async <T extends IconCategory>(
hass: HomeAssistant,
connection: Connection,
category: T,
integration?: string
) =>
hass.callWS<IconResources<CategoryType[T]>>({
callWS<IconResources<CategoryType[T]>>(connection, {
type: "frontend/get_icons",
category,
integration,
});
export const getPlatformIcons = async (
hass: HomeAssistant,
hassConfig: HomeAssistant["config"],
connection: Connection,
integration: string,
force = false
): Promise<PlatformIcons | undefined> => {
@@ -248,12 +274,12 @@ export const getPlatformIcons = async (
return resources.entity[integration];
}
if (
!isComponentLoaded(hass, integration) ||
!atLeastVersion(hass.connection.haVersion, 2024, 2)
!isComponentLoaded(hassConfig, integration) ||
!atLeastVersion(connection.haVersion, 2024, 2)
) {
return undefined;
}
const result = getHassIcons(hass, "entity", integration).then(
const result = getHassIcons(connection, "entity", integration).then(
(res) => res?.resources[integration]
);
resources.entity[integration] = result;
@@ -261,15 +287,13 @@ export const getPlatformIcons = async (
};
export const getComponentIcons = async (
hass: HomeAssistant,
connection: Connection,
hassConfig: HomeAssistant["config"],
domain: string,
force = false
): Promise<ComponentIcons | undefined> => {
// For Cast, old instances can connect to it.
if (
__BACKWARDS_COMPAT__ &&
!atLeastVersion(hass.connection.haVersion, 2024, 2)
) {
if (__BACKWARDS_COMPAT__ && !atLeastVersion(connection.haVersion, 2024, 2)) {
return import("../fake_data/entity_component_icons")
.then((mod) => mod.ENTITY_COMPONENT_ICONS)
.then((res) => res[domain]);
@@ -283,12 +307,12 @@ export const getComponentIcons = async (
return resources.entity_component.resources.then((res) => res[domain]);
}
if (!isComponentLoaded(hass, domain)) {
if (!isComponentLoaded(hassConfig, domain)) {
return undefined;
}
resources.entity_component.domains = [...hass.config.components];
resources.entity_component.domains = [...hassConfig.components];
resources.entity_component.resources = getHassIcons(
hass,
connection,
"entity_component"
).then((result) => result.resources);
return resources.entity_component.resources.then((res) => res[domain]);
@@ -308,10 +332,12 @@ export const getCategoryIcons = async <
Record<string, CategoryType[T]>
>;
}
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;
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;
return resources[category].all as Promise<Record<string, CategoryType[T]>>;
}
if (!force && domain in resources[category].domains) {
@@ -323,10 +349,10 @@ export const getCategoryIcons = async <
return resources[category].domains[domain] as Promise<CategoryType[T]>;
}
}
if (!isComponentLoaded(hass, domain)) {
if (!isComponentLoaded(hass.config, domain)) {
return undefined;
}
const result = getHassIcons(hass, category, domain);
const result = getHassIcons(hass.connection, category, domain);
resources[category].domains[domain] = result.then(
(res) => res?.resources[domain]
) as any;
@@ -467,7 +493,11 @@ const getEntityIcon = async (
let icon: string | undefined;
if (translation_key && platform) {
const platformIcons = await getPlatformIcons(hass, platform);
const platformIcons = await getPlatformIcons(
hass.config,
hass.connection,
platform
);
if (platformIcons) {
const translations = platformIcons[domain]?.[translation_key];
@@ -480,7 +510,11 @@ const getEntityIcon = async (
}
if (!icon) {
const entityComponentIcons = await getComponentIcons(hass, domain);
const entityComponentIcons = await getComponentIcons(
hass.connection,
hass.config,
domain
);
if (entityComponentIcons) {
const translations =
(device_class && entityComponentIcons[device_class]) ||
@@ -511,7 +545,11 @@ export const attributeIcon = async (
(state.attributes[attribute] as string | number | undefined);
if (translation_key && platform) {
const platformIcons = await getPlatformIcons(hass, platform);
const platformIcons = await getPlatformIcons(
hass.config,
hass.connection,
platform
);
if (platformIcons) {
icon = getIconFromTranslations(
value,
@@ -520,7 +558,11 @@ export const attributeIcon = async (
}
}
if (!icon) {
const entityComponentIcons = await getComponentIcons(hass, domain);
const entityComponentIcons = await getComponentIcons(
hass.connection,
hass.config,
domain
);
if (entityComponentIcons) {
const translations =
(deviceClass &&
@@ -548,7 +590,7 @@ export const triggerIcon = async (
icon = trgrIcon?.trigger;
}
if (!icon) {
icon = await domainIcon(hass, domain);
icon = await domainIcon(hass.connection, hass.config, domain);
}
return icon;
};
@@ -567,7 +609,7 @@ export const conditionIcon = async (
icon = condIcon?.condition;
}
if (!icon) {
icon = await domainIcon(hass, domain);
icon = await domainIcon(hass.connection, hass.config, domain);
}
return icon;
};
@@ -585,7 +627,7 @@ export const serviceIcon = async (
icon = srvceIcon?.service;
}
if (!icon) {
icon = await domainIcon(hass, domain);
icon = await domainIcon(hass.connection, hass.config, domain);
}
return icon;
};
@@ -606,12 +648,17 @@ export const serviceSectionIcon = async (
};
export const domainIcon = async (
hass: HomeAssistant,
connection: Connection,
hassConfig: HomeAssistant["config"],
domain: string,
deviceClass?: string,
state?: string
): Promise<string | undefined> => {
const entityComponentIcons = await getComponentIcons(hass, domain);
const entityComponentIcons = await getComponentIcons(
connection,
hassConfig,
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, "thread")) {
if (isComponentLoaded(hass.config, "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, "hassio")) {
if (hass.user?.is_admin && isComponentLoaded(hass.config, "hassio")) {
appItems.push({
path: "/config/apps/available",
icon_path: mdiStorePlus,
+22
View File
@@ -60,6 +60,7 @@ export type Selector =
| NumberSelector
| NumericThresholdSelector
| ObjectSelector
| PeriodSelector
| AssistPipelineSelector
| QRCodeSelector
| SelectSelector
@@ -392,6 +393,27 @@ 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;
+7 -1
View File
@@ -1,7 +1,13 @@
import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
export type ThemeVars = Record<string, string>;
export interface ThemeVars {
// Incomplete
"primary-color": string;
"text-primary-color": string;
"accent-color": string;
[key: string]: string;
}
export type Theme = ThemeVars & {
modes?: {
+17 -16
View File
@@ -1,5 +1,6 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { Connection, 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. */
@@ -325,7 +326,10 @@ export interface ZWaveJSRefreshNodeStatusMessage {
export interface ZWaveJSRebuildRoutesStatusMessage {
event: string;
rebuild_routes_status: Record<number, string>;
rebuild_routes_status: Record<
number,
"pending" | "skipped" | "done" | "failed"
>;
}
export interface ZWaveJSControllerStatisticsUpdatedMessage {
@@ -475,7 +479,7 @@ export const invokeZWaveCCApi = <T = unknown>(
});
export const fetchZwaveNetworkStatus = (
hass: HomeAssistant,
connection: Connection,
device_or_entry_id: {
device_id?: string;
entry_id?: string;
@@ -487,7 +491,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 hass.callWS({
return callWS<ZWaveJSNetwork>(connection, {
type: "zwave_js/network_status",
device_id: device_or_entry_id.device_id,
entry_id: device_or_entry_id.entry_id,
@@ -814,35 +818,32 @@ export const removeFailedZwaveNode = (
);
export const rebuildZwaveNetworkRoutes = (
hass: HomeAssistant,
connection: Connection,
entry_id: string
): Promise<UnsubscribeFunc> =>
hass.callWS({
callWS(connection, {
type: "zwave_js/begin_rebuilding_routes",
entry_id,
});
export const stopRebuildingZwaveNetworkRoutes = (
hass: HomeAssistant,
connection: Connection,
entry_id: string
): Promise<UnsubscribeFunc> =>
hass.callWS({
callWS(connection, {
type: "zwave_js/stop_rebuilding_routes",
entry_id,
});
export const subscribeRebuildZwaveNetworkRoutesProgress = (
hass: HomeAssistant,
connection: Connection,
entry_id: string,
callbackFunction: (message: ZWaveJSRebuildRoutesStatusMessage) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(
(message: any) => callbackFunction(message),
{
type: "zwave_js/subscribe_rebuild_routes_progress",
entry_id,
}
);
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.emtpy_value")}
.placeholder=${this.hass!.localize("ui.card.text.empty_value")}
></ha-input>
`;
}
@@ -170,7 +170,8 @@ class StepFlowCreateEntry extends LitElement {
)}
.placeholder=${computeDeviceNameDisplay(
device,
this.hass
this.hass.localize,
this.hass.states
)}
.value=${this._deviceUpdate[device.id]?.name ??
computeDeviceName(device)}
+2
View File
@@ -11,6 +11,8 @@ export const DialogMixin = <
superClass: T
) =>
class extends superClass implements HassDialogNext<P> {
public dialogNext = true as const;
declare public params?: P;
private _closePromise?: Promise<boolean>;
+5 -2
View File
@@ -26,6 +26,7 @@ export interface HassDialog<T = unknown> extends HTMLElement {
}
export interface HassDialogNext<T = unknown> extends HTMLElement {
dialogNext: true;
params?: T;
closeDialog?: (historyState?: any) => Promise<boolean> | boolean;
}
@@ -168,10 +169,12 @@ export const showDialog = async (
dialogElement = await LOADED[dialogTag].element;
}
if ("showDialog" in dialogElement!) {
if ("dialogNext" in dialogElement! && dialogElement.dialogNext) {
dialogElement!.params = dialogParams;
} else if ("showDialog" in dialogElement!) {
dialogElement.showDialog(dialogParams);
} else {
dialogElement!.params = dialogParams;
throw new Error("Unknown dialog type loaded");
}
(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, "history") &&
isComponentLoaded(hass.config, "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, "logbook")) {
if (!isComponentLoaded(hass.config, "logbook")) {
return false;
}
@@ -83,6 +83,10 @@ class MoreInfoInputDatetime extends LitElement {
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, "hassio") &&
isComponentLoaded(this.hass.config, "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 type { HomeAssistant } from "../../types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
declare global {
interface HASSDomEvents {
@@ -57,7 +57,7 @@ export class MoreInfoHistory extends LitElement {
return nothing;
}
return html`${isComponentLoaded(this.hass, "history")
return html`${isComponentLoaded(this.hass.config, "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, "recorder") &&
isComponentLoaded(this.hass.config, "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, "history")) {
if (!isComponentLoaded(this.hass.config, "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 type { HomeAssistant } from "../../types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
@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, "logbook") || !this.entityId) {
if (!isComponentLoaded(this.hass.config, "logbook") || !this.entityId) {
return nothing;
}
const stateObj = this.hass.states[this.entityId];
+4 -2
View File
@@ -129,7 +129,10 @@ export class QuickBar extends LitElement {
console.error("Error fetching config entries for quick bar", err);
}
if (this.hass.user?.is_admin && isComponentLoaded(this.hass, "hassio")) {
if (
this.hass.user?.is_admin &&
isComponentLoaded(this.hass.config, "hassio")
) {
try {
const hassioAddonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = hassioAddonsInfo.addons;
@@ -303,7 +306,6 @@ 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>
+20 -27
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, "hassio");
const isHassioLoaded = isComponentLoaded(this.hass.config, "hassio");
this._open = true;
this._dialogOpen = true;
@@ -103,7 +103,6 @@ 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");
@@ -135,30 +134,24 @@ class DialogRestart extends LitElement {
`
: html`
<ha-md-list dialogInitialFocus>
${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"
@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>
<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, "hassio")) {
if (!isComponentLoaded(this.hass.config, "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, "cloud")) {
if (!isComponentLoaded(this.hass.config, "cloud")) {
return false;
}
const cloudStatus = await fetchCloudStatus(this.hass);
@@ -104,7 +104,11 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.device_name"
)}
.placeholder=${computeDeviceNameDisplay(device, this.hass)}
.placeholder=${computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)}
.value=${this._deviceName ?? computeDeviceName(device)}
@change=${this._deviceNameChanged}
></ha-input>
@@ -505,7 +505,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
`
: ""}
<ha-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.columns=${this.columns}
.data=${this.data}
-2
View File
@@ -52,7 +52,6 @@ export class HomeAssistantMain extends LitElement {
return html`
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
<ha-retro .hass=${this.hass} .narrow=${this.narrow}></ha-retro>
<ha-drawer
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : false}
@@ -80,7 +79,6 @@ export class HomeAssistantMain extends LitElement {
protected firstUpdated() {
import(/* webpackPreload: true */ "../components/ha-sidebar");
import("../components/ha-snowflakes");
import("../components/ha-retro");
if (this.hass.auth.external) {
this._externalSidebar =
+39 -23
View File
@@ -1,10 +1,13 @@
import type { ReactiveElement } from "lit";
import { consume } from "@lit/context";
import type { PropertyValues, ReactiveElement } from "lit";
import { state } from "lit/decorators";
import type { HomeAssistant } from "../types";
import {
setupMediaQueryListeners,
setupTimeListeners,
} from "../common/condition/listeners";
import type { Condition } from "../panels/lovelace/common/validate-condition";
import { setupConditionListeners } from "../common/condition/listeners";
import { maxColumnsContext } from "../panels/lovelace/common/context";
import type {
Condition,
ConditionContext,
} from "../panels/lovelace/common/validate-condition";
type Constructor<T> = abstract new (...args: any[]) => T;
@@ -32,6 +35,7 @@ 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,
@@ -47,6 +51,12 @@ 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;
@@ -61,6 +71,20 @@ 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
*
@@ -106,26 +130,18 @@ export const ConditionalListenerMixin = <
return;
}
const onUpdate = (conditionsMet: boolean) => {
if (this._updateVisibility) {
this._updateVisibility(conditionsMet);
} else if (this._updateElement && config) {
this._updateElement(config);
}
};
setupMediaQueryListeners(
setupConditionListeners(
finalConditions,
this.hass,
(unsub) => this.addConditionalListener(unsub),
onUpdate
);
setupTimeListeners(
finalConditions,
this.hass,
(unsub) => this.addConditionalListener(unsub),
onUpdate
(conditionsMet) => {
if (this._updateVisibility) {
this._updateVisibility(conditionsMet);
} else if (this._updateElement && config) {
this._updateElement(config);
}
},
() => this._conditionContext
);
}
}
+1 -1
View File
@@ -192,7 +192,7 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
}
private async _scanUSBDevices() {
if (!isComponentLoaded(this.hass, "usb")) {
if (!isComponentLoaded(this.hass.config, "usb")) {
return;
}
await scanUSBDevices(this.hass);
@@ -108,7 +108,6 @@ 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,7 +187,6 @@ 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(
+13 -9
View File
@@ -18,6 +18,7 @@ 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";
@@ -52,7 +53,6 @@ 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,7 +166,11 @@ 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);
entry.name = computeDeviceNameDisplay(
entry,
this.hass.localize,
this.hass.states
);
});
sortDeviceRegistryByName(devices, this.hass.locale.language);
}
@@ -190,7 +194,7 @@ class HaConfigAreaPage extends LitElement {
let relatedScenes: NameAndEntity<SceneEntity>[] = [];
let relatedScripts: NameAndEntity<ScriptEntity>[] = [];
if (isComponentLoaded(this.hass, "automation")) {
if (isComponentLoaded(this.hass.config, "automation")) {
({
groupedEntities: groupedAutomations,
relatedEntities: relatedAutomations,
@@ -200,7 +204,7 @@ class HaConfigAreaPage extends LitElement {
));
}
if (isComponentLoaded(this.hass, "scene")) {
if (isComponentLoaded(this.hass.config, "scene")) {
({ groupedEntities: groupedScenes, relatedEntities: relatedScenes } =
this._prepareEntities<SceneEntity>(
groupedEntities.scene,
@@ -208,7 +212,7 @@ class HaConfigAreaPage extends LitElement {
));
}
if (isComponentLoaded(this.hass, "script")) {
if (isComponentLoaded(this.hass.config, "script")) {
({ groupedEntities: groupedScripts, relatedEntities: relatedScripts } =
this._prepareEntities<ScriptEntity>(
groupedEntities.script,
@@ -328,7 +332,7 @@ class HaConfigAreaPage extends LitElement {
</ha-card>
</div>
<div class="column">
${isComponentLoaded(this.hass, "automation")
${isComponentLoaded(this.hass.config, "automation")
? html`
<ha-card
outlined
@@ -378,7 +382,7 @@ class HaConfigAreaPage extends LitElement {
</ha-card>
`
: ""}
${isComponentLoaded(this.hass, "scene")
${isComponentLoaded(this.hass.config, "scene")
? html`
<ha-card
outlined
@@ -422,7 +426,7 @@ class HaConfigAreaPage extends LitElement {
</ha-card>
`
: ""}
${isComponentLoaded(this.hass, "script")
${isComponentLoaded(this.hass.config, "script")
? html`
<ha-card
outlined
@@ -464,7 +468,7 @@ class HaConfigAreaPage extends LitElement {
: ""}
</div>
<div class="column">
${isComponentLoaded(this.hass, "logbook")
${isComponentLoaded(this.hass.config, "logbook")
? html`
<ha-card
outlined
@@ -95,6 +95,8 @@ 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) => {
@@ -239,7 +241,14 @@ export default class HaAutomationActionRow extends LitElement {
private _renderRow() {
const type = getAutomationActionType(this.action);
const actionHasTarget = type === "service" && "target" in this.action;
const action = type === "service" && (this.action as ServiceAction).action;
const actionHasTarget =
action &&
"target" in
(this.hass.services?.[computeDomain(action)]?.[
computeObjectId(action)
] || {});
const target = actionHasTarget
? (this.action as ServiceAction).target
@@ -550,7 +559,10 @@ export default class HaAutomationActionRow extends LitElement {
>${this._renderRow()}</ha-automation-row
>`
: html`
<ha-expansion-panel left-chevron>
<ha-expansion-panel
left-chevron
@expanded-changed=${this._expansionPanelChanged}
>
${this._renderRow()}
</ha-expansion-panel>
`}
@@ -808,6 +820,12 @@ export default class HaAutomationActionRow extends LitElement {
}
}
private _expansionPanelChanged(ev: CustomEvent) {
if (!ev.detail.expanded) {
this._isNew = false;
}
}
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
@@ -912,9 +930,6 @@ export default class HaAutomationActionRow extends LitElement {
);
private _toggleCollapse() {
if (!this._collapsed) {
this._isNew = false;
}
this._collapsed = !this._collapsed;
}
@@ -2,7 +2,6 @@ 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,14 +1,13 @@
import type { CSSResultGroup } from "lit";
import { html, LitElement } from "lit";
import { query, customElement, property } from "lit/decorators";
import { customElement, property, query } 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 { ActionElement } from "../ha-automation-action-row";
import type HaAutomationAction from "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
@customElement("ha-automation-action-sequence")
export class HaSequenceAction extends LitElement implements ActionElement {
@@ -17,6 +17,7 @@ import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event";
import { mainWindow } from "../../../common/dom/get_main_window";
import { computeAreaName } from "../../../common/entity/compute_area_name";
@@ -98,6 +99,7 @@ import {
domainToName,
fetchIntegrationManifests,
} from "../../../data/integration";
import { filterSelectorEntities } from "../../../data/selector";
import type { LabelRegistryEntry } from "../../../data/label/label_registry";
import { subscribeLabFeature } from "../../../data/labs";
import {
@@ -287,6 +289,7 @@ class DialogAddAutomationElement
public showDialog(params): void {
this._params = params;
this._resetVariables();
this.addKeyboardShortcuts();
@@ -376,9 +379,15 @@ class DialogAddAutomationElement
if (this._params) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._params = undefined;
this._resetVariables();
return true;
}
private _resetVariables() {
this._open = true;
this._closing = false;
this._params = undefined;
this._selectedCollectionIndex = undefined;
this._selectedGroup = undefined;
this._selectedTarget = undefined;
@@ -390,7 +399,6 @@ class DialogAddAutomationElement
this._narrow = false;
this._targetItems = undefined;
this._loadItemsError = false;
return true;
}
private _updateNarrow = () => {
@@ -406,6 +414,86 @@ class DialogAddAutomationElement
}
}
private _calculateActiveSystemDomains = memoizeOne(
(
descriptions: TriggerDescriptions | ConditionDescriptions,
manifests: DomainManifestLookup,
getDomain: (key: string) => string
): { active: Set<string>; byEntityDomain: Map<string, Set<string>> } => {
const active = new Set<string>();
// Group all entity filters by system domain
const domainFilters: Record<
string,
Parameters<typeof filterSelectorEntities>[0][]
> = {};
// Also collect which entity domains each system domain targets
const entityDomainsPerSystemDomain: Record<string, Set<string>> = {};
for (const [key, desc] of Object.entries(descriptions)) {
const domain = getDomain(key);
if (manifests[domain]?.integration_type !== "system") {
continue;
}
if (!domainFilters[domain]) {
domainFilters[domain] = [];
entityDomainsPerSystemDomain[domain] = new Set();
}
const entityFilters = ensureArray(desc.target?.entity);
if (entityFilters) {
// target.entity can be EntitySelectorFilter | readonly EntitySelectorFilter[]
// ensureArray wraps it but each element may still be an array, so flatten
for (const filterOrArray of entityFilters) {
const filters = ensureArray(filterOrArray);
domainFilters[domain].push(...filters);
for (const filter of filters) {
for (const entityDomain of ensureArray(filter.domain) ?? []) {
entityDomainsPerSystemDomain[domain].add(entityDomain);
}
}
}
}
}
// Check each entity in hass.states against the filters
for (const entity of Object.values(this.hass.states)) {
for (const [domain, filters] of Object.entries(domainFilters)) {
if (active.has(domain)) {
continue;
}
if (filters.some((f) => filterSelectorEntities(f, entity))) {
active.add(domain);
}
}
}
// Build reverse map: entity domain → set of system domains that cover it
const byEntityDomain = new Map<string, Set<string>>();
for (const [systemDomain, entityDomains] of Object.entries(
entityDomainsPerSystemDomain
)) {
for (const entityDomain of entityDomains) {
if (!byEntityDomain.has(entityDomain)) {
byEntityDomain.set(entityDomain, new Set());
}
byEntityDomain.get(entityDomain)!.add(systemDomain);
}
}
return { active, byEntityDomain };
}
);
private get _systemDomains() {
if (!this._manifests) {
return undefined;
}
const descriptions =
this._params?.type === "trigger"
? this._triggerDescriptions
: this._conditionDescriptions;
return this._calculateActiveSystemDomains(
descriptions,
this._manifests,
this._params?.type === "trigger" ? getTriggerDomain : getConditionDomain
);
}
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
@@ -420,6 +508,11 @@ class DialogAddAutomationElement
manifests[manifest.domain] = manifest;
}
this._manifests = manifests;
// If a target was already selected and items computed before manifests
// loaded, recompute so system domain grouping applies correctly.
if (this._selectedTarget && this._targetItems) {
this._getItemsByTarget();
}
}
public disconnectedCallback(): void {
@@ -899,7 +992,8 @@ class DialogAddAutomationElement
this._domains,
this.hass.localize,
this.hass.services,
this._manifests
this._manifests,
this._systemDomains?.byEntityDomain
),
},
]
@@ -948,10 +1042,17 @@ class DialogAddAutomationElement
const items = flattenGroups(groups).flat();
if (type === "trigger") {
items.push(...this._triggers(localize, this._triggerDescriptions));
items.push(
...this._triggers(localize, this._triggerDescriptions, undefined)
);
} else if (type === "condition") {
items.push(
...this._conditions(localize, this._conditionDescriptions, manifests)
...this._conditions(
localize,
this._conditionDescriptions,
manifests,
undefined
)
);
} else if (type === "action") {
items.push(...this._services(localize, services, manifests));
@@ -1135,16 +1236,23 @@ class DialogAddAutomationElement
domains: Set<string> | undefined,
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests?: DomainManifestLookup
manifests?: DomainManifestLookup,
systemDomainsByEntityDomain?: Map<string, Set<string>>
): AddAutomationElementListItem[] => {
if (type === "trigger" && isDynamic(group)) {
return this._triggers(localize, this._triggerDescriptions, group);
return this._triggers(
localize,
this._triggerDescriptions,
systemDomainsByEntityDomain,
group
);
}
if (type === "condition" && isDynamic(group)) {
return this._conditions(
localize,
this._conditionDescriptions,
manifests,
systemDomainsByEntityDomain,
group
);
}
@@ -1227,11 +1335,7 @@ class DialogAddAutomationElement
) {
result.push({
icon: html`
<ha-domain-icon
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>
<ha-domain-icon .domain=${domain} brand-fallback></ha-domain-icon>
`,
key: `${DYNAMIC_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
@@ -1244,6 +1348,38 @@ class DialogAddAutomationElement
);
};
private _domainMatchesGroupType(
domain: string,
manifest: DomainManifestLookup[string] | undefined,
domainUsed: boolean,
type: "helper" | "other" | undefined
): boolean {
if (type === undefined) {
return (
ENTITY_DOMAINS_MAIN.has(domain) ||
(manifest?.integration_type === "entity" &&
domainUsed &&
!ENTITY_DOMAINS_OTHER.has(domain)) ||
(manifest?.integration_type === "system" &&
(this._systemDomains?.active.has(domain) ?? false))
);
}
if (type === "helper") {
return manifest?.integration_type === "helper";
}
// type === "other"
return (
!ENTITY_DOMAINS_MAIN.has(domain) &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
(!domainUsed && manifest?.integration_type === "entity") ||
(manifest?.integration_type === "system" &&
!(this._systemDomains?.active.has(domain) ?? false)) ||
!["helper", "entity", "system"].includes(
manifest?.integration_type || ""
))
);
}
private _triggerGroups = (
localize: LocalizeFunc,
triggers: TriggerDescriptions,
@@ -1267,26 +1403,10 @@ class DialogAddAutomationElement
const manifest = manifests[domain];
const domainUsed = !domains ? true : domains.has(domain);
if (
(type === undefined &&
(ENTITY_DOMAINS_MAIN.has(domain) ||
(manifest?.integration_type === "entity" &&
domainUsed &&
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
!ENTITY_DOMAINS_MAIN.has(domain) &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
(!domainUsed && manifest?.integration_type === "entity") ||
!["helper", "entity"].includes(manifest?.integration_type || "")))
) {
if (this._domainMatchesGroupType(domain, manifest, domainUsed, type)) {
result.push({
icon: html`
<ha-domain-icon
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>
<ha-domain-icon .domain=${domain} brand-fallback></ha-domain-icon>
`,
key: `${DYNAMIC_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
@@ -1303,17 +1423,30 @@ class DialogAddAutomationElement
(
localize: LocalizeFunc,
triggers: TriggerDescriptions,
systemDomainsByEntityDomain: Map<string, Set<string>> | undefined,
group?: string
): AddAutomationElementListItem[] => {
if (!triggers) {
return [];
}
const browsedEntityDomain =
group && isDynamic(group) ? getValueFromDynamic(group) : undefined;
// System domains that should be merged into this entity domain group
const systemDomainsForGroup = browsedEntityDomain
? systemDomainsByEntityDomain?.get(browsedEntityDomain)
: undefined;
return this._getTriggerListItems(
localize,
Object.keys(triggers).filter((trigger) => {
const domain = getTriggerDomain(trigger);
return !group || group === `${DYNAMIC_PREFIX}${domain}`;
if (!group || group === `${DYNAMIC_PREFIX}${domain}`) {
return true;
}
// Also include system domain triggers that cover the browsed entity domain
return systemDomainsForGroup?.has(domain) ?? false;
})
);
}
@@ -1342,26 +1475,10 @@ class DialogAddAutomationElement
const manifest = manifests[domain];
const domainUsed = !domains ? true : domains.has(domain);
if (
(type === undefined &&
(ENTITY_DOMAINS_MAIN.has(domain) ||
(manifest?.integration_type === "entity" &&
domainUsed &&
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
!ENTITY_DOMAINS_MAIN.has(domain) &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
(!domainUsed && manifest?.integration_type === "entity") ||
!["helper", "entity"].includes(manifest?.integration_type || "")))
) {
if (this._domainMatchesGroupType(domain, manifest, domainUsed, type)) {
result.push({
icon: html`
<ha-domain-icon
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>
<ha-domain-icon .domain=${domain} brand-fallback></ha-domain-icon>
`,
key: `${DYNAMIC_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
@@ -1379,6 +1496,7 @@ class DialogAddAutomationElement
localize: LocalizeFunc,
conditions: ConditionDescriptions,
_manifests: DomainManifestLookup | undefined,
systemDomainsByEntityDomain: Map<string, Set<string>> | undefined,
group?: string
): AddAutomationElementListItem[] => {
if (!conditions) {
@@ -1386,10 +1504,21 @@ class DialogAddAutomationElement
}
const result: AddAutomationElementListItem[] = [];
const browsedEntityDomain =
group && isDynamic(group) ? getValueFromDynamic(group) : undefined;
const systemDomainsForGroup = browsedEntityDomain
? systemDomainsByEntityDomain?.get(browsedEntityDomain)
: undefined;
for (const condition of Object.keys(conditions)) {
const domain = getConditionDomain(condition);
if (group && group !== `${DYNAMIC_PREFIX}${domain}`) {
if (
group &&
group !== `${DYNAMIC_PREFIX}${domain}` &&
!(systemDomainsForGroup?.has(domain) ?? false)
) {
continue;
}
@@ -1485,13 +1614,23 @@ class DialogAddAutomationElement
);
private _getDomainType(domain: string) {
return ENTITY_DOMAINS_MAIN.has(domain) ||
(this._manifests?.[domain].integration_type === "entity" &&
if (
ENTITY_DOMAINS_MAIN.has(domain) ||
(this._manifests?.[domain]?.integration_type === "entity" &&
!ENTITY_DOMAINS_OTHER.has(domain))
? "dynamicGroups"
: this._manifests?.[domain].integration_type === "helper"
? "helpers"
: "other";
) {
return "dynamicGroups";
}
if (this._manifests?.[domain]?.integration_type === "helper") {
return "helpers";
}
if (
this._manifests?.[domain]?.integration_type === "system" &&
this._systemDomains?.active.has(domain)
) {
return "dynamicGroups";
}
return "other";
}
private _sortDomainsByCollection(
@@ -1596,30 +1735,56 @@ class DialogAddAutomationElement
iconPath: options.icon || TYPES[type].icons[key],
});
private _getDomainGroupedTriggerListItems(
private _getDomainGroupedListItems(
localize: LocalizeFunc,
triggerIds: string[]
ids: string[],
getDomain: (id: string) => string,
getListItem: (
localize: LocalizeFunc,
domain: string,
id: string
) => AddAutomationElementListItem
): { title: string; items: AddAutomationElementListItem[] }[] {
const items: Record<
string,
{ title: string; items: AddAutomationElementListItem[] }
> = {};
triggerIds.forEach((trigger) => {
const domain = getTriggerDomain(trigger);
// When a specific entity is selected, system domain items are merged
// under the entity's real domain rather than under their system domain name.
const targetEntityId = this._selectedTarget?.entity_id;
const targetEntityDomain =
targetEntityId &&
this._manifests?.[computeDomain(targetEntityId)]?.integration_type !==
"system"
? computeDomain(targetEntityId)
: undefined;
if (!items[domain]) {
items[domain] = {
title: domainToName(localize, domain, this._manifests?.[domain]),
ids.forEach((id) => {
const itemDomain = getDomain(id);
const isSystemDomain =
this._manifests?.[itemDomain]?.integration_type === "system";
// System domain items are grouped under the entity's real domain (if
// a specific entity is selected), so they appear alongside that domain's
// own items rather than in a separate section.
const groupDomain =
isSystemDomain && targetEntityDomain ? targetEntityDomain : itemDomain;
if (!items[groupDomain]) {
items[groupDomain] = {
title: domainToName(
localize,
groupDomain,
this._manifests?.[groupDomain]
),
items: [],
};
}
items[domain].items.push(
this._getTriggerListItem(localize, domain, trigger)
);
items[groupDomain].items.push(getListItem(localize, itemDomain, id));
items[domain].items.sort((a, b) =>
items[groupDomain].items.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
});
@@ -1741,39 +1906,6 @@ class DialogAddAutomationElement
);
}
private _getDomainGroupedConditionListItems(
localize: LocalizeFunc,
conditionIds: string[]
): { title: string; items: AddAutomationElementListItem[] }[] {
const items: Record<
string,
{ title: string; items: AddAutomationElementListItem[] }
> = {};
conditionIds.forEach((condition) => {
const domain = getConditionDomain(condition);
if (!items[domain]) {
items[domain] = {
title: domainToName(localize, domain, this._manifests?.[domain]),
items: [],
};
}
items[domain].items.push(
this._getConditionListItem(localize, domain, condition)
);
items[domain].items.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
});
return this._sortDomainsByCollection(
this._params!.type,
Object.entries(items)
);
}
// #endregion render prepare
// #region interaction
@@ -1903,9 +2035,12 @@ class DialogAddAutomationElement
this._selectedTarget
);
const grouped = this._getDomainGroupedTriggerListItems(
const grouped = this._getDomainGroupedListItems(
this.hass.localize,
items
items,
getTriggerDomain,
(localize, domain, trigger) =>
this._getTriggerListItem(localize, domain, trigger)
);
if (this._selectedTarget.entity_id) {
grouped.push({
@@ -1924,9 +2059,12 @@ class DialogAddAutomationElement
this._selectedTarget
);
const grouped = this._getDomainGroupedConditionListItems(
const grouped = this._getDomainGroupedListItems(
this.hass.localize,
items
items,
getConditionDomain,
(localize, domain, condition) =>
this._getConditionListItem(localize, domain, condition)
);
if (this._selectedTarget.entity_id) {
grouped.push({
@@ -362,7 +362,6 @@ export class HaAutomationAddSearch extends LitElement {
? html`
<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${(item as DevicePickerItem).domain!}
brand-fallback
></ha-domain-icon>
@@ -151,11 +151,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
.value=${this._newIcon}
@value-changed=${this._iconChanged}
>
<ha-domain-icon
slot="start"
domain=${this._params.domain}
.hass=${this.hass}
>
<ha-domain-icon slot="start" domain=${this._params.domain}>
</ha-domain-icon>
</ha-icon-picker>
`
@@ -429,7 +429,10 @@ export default class HaAutomationConditionRow extends LitElement {
>${this._renderRow()}</ha-automation-row
>`
: html`
<ha-expansion-panel left-chevron>
<ha-expansion-panel
left-chevron
@expanded-changed=${this._expansionPanelChanged}
>
${this._renderRow()}
</ha-expansion-panel>
`}
@@ -754,6 +757,12 @@ export default class HaAutomationConditionRow extends LitElement {
this._isNew = true;
}
private _expansionPanelChanged(ev: CustomEvent) {
if (!ev.detail.expanded) {
this._isNew = false;
}
}
public openSidebar(condition?: Condition): void {
const sidebarCondition = condition || this.condition;
fireEvent(this, "open-sidebar", {
@@ -818,9 +827,6 @@ export default class HaAutomationConditionRow extends LitElement {
);
private _toggleCollapse() {
if (!this._collapsed) {
this._isNew = false;
}
this._collapsed = !this._collapsed;
}
@@ -166,19 +166,12 @@ export class HaPlatformCondition extends LitElement {
</div>
${conditionDesc && "target" in conditionDesc
? html`<ha-settings-row narrow>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: nothing}
<span slot="heading"
>${this.hass.localize(
"ui.components.service-control.target"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
<ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(conditionDesc.target)}
.disabled=${this.disabled}
@@ -240,6 +233,10 @@ export class HaPlatformCondition extends LitElement {
return nothing;
}
const description = this.hass.localize(
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.description`
);
return html`<ha-settings-row narrow>
${!showOptional
? hasOptional
@@ -259,11 +256,9 @@ export class HaPlatformCondition extends LitElement {
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.name`
) || conditionName}</span
>
<span slot="description"
>${this.hass.localize(
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.description`
)}</span
>
${description
? html`<span slot="description">${description}</span>`
: nothing}
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
@@ -1087,7 +1087,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
private _createNew() {
if (isComponentLoaded(this.hass, "blueprint")) {
if (isComponentLoaded(this.hass.config, "blueprint")) {
showNewAutomationDialog(this, { mode: "automation" });
} else {
navigate("/config/automation/edit/new");
@@ -20,6 +20,7 @@ import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/trace/ha-trace-blueprint-config";
@@ -46,7 +47,6 @@ import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
const TABS = ["details", "timeline", "logbook", "automation_config"] as const;
@@ -438,7 +438,7 @@ export class HaAutomationTrace extends LitElement {
this.automationId,
this._runId!
);
this._logbookEntries = isComponentLoaded(this.hass, "logbook")
this._logbookEntries = isComponentLoaded(this.hass.config, "logbook")
? await getLogbookDataForContext(
this.hass,
trace.timestamp.start,
@@ -464,7 +464,6 @@ export class HaAutomationTrace extends LitElement {
url,
`trace ${this._entityId} ${this._trace!.timestamp.start}.json`
);
URL.revokeObjectURL(url);
}
private _importTrace() {
@@ -43,7 +43,6 @@ export const getTargetIcon = (
if (domain) {
return html`<ha-domain-icon
.hass=${hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>`;
@@ -202,18 +202,10 @@ export class HaPlatformTrigger extends LitElement {
</div>
${triggerDesc && "target" in triggerDesc
? html`<ha-settings-row narrow>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: nothing}
<span slot="heading"
>${this.hass.localize(
"ui.components.service-control.target"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(triggerDesc.target)}
@@ -276,6 +268,10 @@ export class HaPlatformTrigger extends LitElement {
return nothing;
}
const description = this.hass.localize(
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.description`
);
return html`<ha-settings-row narrow>
${!showOptional
? hasOptional
@@ -295,11 +291,9 @@ export class HaPlatformTrigger extends LitElement {
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.name`
) || triggerName}</span
>
<span slot="description"
>${this.hass.localize(
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.description`
)}</span
>
${description
? html`<span slot="description">${description}</span>`
: nothing}
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
@@ -88,7 +88,7 @@ class HaBackupConfigData extends LitElement {
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this._checkDbOption();
if (isComponentLoaded(this.hass, "hassio")) {
if (isComponentLoaded(this.hass.config, "hassio")) {
this._fetchAddons();
this._fetchStorageInfo();
}
@@ -96,7 +96,7 @@ class HaBackupConfigData extends LitElement {
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("value")) {
if (isComponentLoaded(this.hass, "hassio")) {
if (isComponentLoaded(this.hass.config, "hassio")) {
if (this.value?.include_addons?.length) {
this._showAddons = true;
}
@@ -111,7 +111,7 @@ class HaBackupConfigData extends LitElement {
}
private async _checkDbOption() {
if (isComponentLoaded(this.hass, "recorder")) {
if (isComponentLoaded(this.hass.config, "recorder")) {
const info = await getRecorderInfo(this.hass.connection);
this._showDbOption = info.db_in_default_location;
if (!this._showDbOption && this.value?.include_database) {
@@ -234,7 +234,7 @@ class HaBackupConfigData extends LitElement {
protected render() {
const data = this._getData(this.value, this._showAddons);
const isHassio = isComponentLoaded(this.hass, "hassio");
const isHassio = isComponentLoaded(this.hass.config, "hassio");
return html`
${this._renderSizeEstimate()}
@@ -455,7 +455,7 @@ class HaBackupConfigData extends LitElement {
}
private _renderSizeEstimate() {
if (!isComponentLoaded(this.hass, "hassio")) {
if (!isComponentLoaded(this.hass.config, "hassio")) {
return nothing;
}
@@ -67,7 +67,7 @@ export class HaBackupDataPicker extends LitElement {
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (this.hass && isComponentLoaded(this.hass, "hassio")) {
if (this.hass && isComponentLoaded(this.hass.config, "hassio")) {
this._fetchAddonInfo();
}
}
@@ -60,7 +60,7 @@ class HaBackupOverviewBackups extends LitElement {
);
render() {
const isHassio = isComponentLoaded(this.hass, "hassio");
const isHassio = isComponentLoaded(this.hass.config, "hassio");
const stats = this._stats(this.backups, isHassio);
return html`
@@ -158,7 +158,7 @@ export class HaBackupOverviewProgress extends LitElement {
: null;
const currentGroupIndex = stage ? this._getStageGroupIndex(stage) : -1;
const isHassio = isComponentLoaded(this.hass, "hassio");
const isHassio = isComponentLoaded(this.hass.config, "hassio");
if (isHassio) {
// Split creation into 3 sub-segments + Upload + Cleaning up
@@ -2,6 +2,7 @@ import { mdiCalendar, mdiDatabase, mdiPuzzle, mdiUpload } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
@@ -16,10 +17,9 @@ import {
getFormattedBackupTime,
isLocalAgent,
} from "../../../../../data/backup";
import { getRecorderInfo } from "../../../../../data/recorder";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { getRecorderInfo } from "../../../../../data/recorder";
@customElement("ha-backup-overview-settings")
class HaBackupBackupsSummary extends LitElement {
@@ -41,7 +41,7 @@ class HaBackupBackupsSummary extends LitElement {
}
private async _checkDbOption() {
if (isComponentLoaded(this.hass, "recorder")) {
if (isComponentLoaded(this.hass.config, "recorder")) {
const info = await getRecorderInfo(this.hass.connection);
this._showDbOption = info.db_in_default_location;
} else {
@@ -6,11 +6,11 @@ import { isComponentLoaded } from "../../../../common/config/is_component_loaded
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
@@ -150,7 +150,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
automatic_backups_configured: done,
};
if (isComponentLoaded(this.hass, "hassio")) {
if (isComponentLoaded(this.hass.config, "hassio")) {
params.create_backup!.include_folders =
this._config.create_backup.include_folders || [];
params.create_backup!.include_all_addons =
@@ -265,7 +265,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
private get _defaultAgents(): string[] {
const agents: string[] = [];
// Enable local location by default
if (isComponentLoaded(this.hass, "hassio")) {
if (isComponentLoaded(this.hass.config, "hassio")) {
agents.push(HASSIO_LOCAL_AGENT);
} else {
agents.push(CORE_LOCAL_AGENT);

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