mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-04 07:21:42 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09982a9238 | |||
| ab1a58b3f3 | |||
| a7ff89385e | |||
| f3d41be3bf | |||
| b73707751a | |||
| 61bff43cdb | |||
| 0a0d08fa19 | |||
| ae29ba63ff | |||
| 0579cd8eb6 | |||
| 8c3eafec6d | |||
| b5c2e12016 | |||
| f7a13392cd | |||
| a2cdd592f1 | |||
| f04341a2a2 | |||
| 91bdc80a67 | |||
| b4824cc0a7 | |||
| 28f375c0d4 | |||
| da7ccac811 | |||
| a8ad921efd | |||
| 3b8f219800 | |||
| e36a2e1c70 | |||
| e06ea1047c | |||
| 99cb997d08 | |||
| ac3edd20f8 | |||
| 0d88d139f0 | |||
| b8d08ccb05 | |||
| 7c20316ba5 | |||
| fa633efc87 | |||
| 85d461f0fd | |||
| b55e1c9988 | |||
| 1da349a36d | |||
| 74f7139a09 | |||
| 2911cc77fa | |||
| ab20383a3a | |||
| 514cb9da9d | |||
| 7c52ac8ca7 | |||
| 07b4a44228 | |||
| 2b28a6c3f2 | |||
| 84f2e304cf | |||
| 18cd40ab01 | |||
| 8e3b1dc6ac | |||
| 5cc223a582 | |||
| 9a62a9217c | |||
| 70be747e9d | |||
| bb57a91494 | |||
| 7e22e6c0e2 | |||
| c93f910e56 | |||
| 8bf4ff5d25 | |||
| debc3adf19 | |||
| ae21017de8 | |||
| f15f518cc2 | |||
| 0e44417051 | |||
| 3581b43336 |
@@ -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;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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,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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -123,6 +123,9 @@ export class HaDateInput extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
min-width: 0px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
|
||||
@@ -72,7 +72,6 @@ export class HaFilterDomains extends LitElement {
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface CoreFrontendSystemData {
|
||||
export interface HomeFrontendSystemData {
|
||||
favorite_entities?: string[];
|
||||
welcome_banner_dismissed?: boolean;
|
||||
hidden_summaries?: string[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
+77
-30
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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)}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user