mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-21 08:47:26 +00:00
Compare commits
36 Commits
dev
...
add-automa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d070754bb9 | ||
|
|
a2a0f900a8 | ||
|
|
b18d3bff21 | ||
|
|
b06b51d03d | ||
|
|
022343c113 | ||
|
|
5ca10f4a38 | ||
|
|
33af5b6a08 | ||
|
|
0c89415c1c | ||
|
|
bd6b2c685c | ||
|
|
cdb37edc5f | ||
|
|
d68b1571ae | ||
|
|
a274727713 | ||
|
|
58a1aac131 | ||
|
|
4cdbda25ed | ||
|
|
618c6e1ef2 | ||
|
|
10b308b906 | ||
|
|
563c6f0fbf | ||
|
|
1dc7f0ab73 | ||
|
|
5c8954b714 | ||
|
|
e506e4b082 | ||
|
|
9628d69998 | ||
|
|
350d8e7a49 | ||
|
|
61a5b607e0 | ||
|
|
2f3a2b8418 | ||
|
|
12c1e4eec4 | ||
|
|
b704b621f2 | ||
|
|
0eb993a8df | ||
|
|
cda97766ac | ||
|
|
e6936a9294 | ||
|
|
5ad73287a2 | ||
|
|
591b464508 | ||
|
|
acf963d38c | ||
|
|
68f383c293 | ||
|
|
0a25d8106c | ||
|
|
5c3cf17df9 | ||
|
|
e905fa6f23 |
183
.github/copilot-instructions.md
vendored
183
.github/copilot-instructions.md
vendored
@@ -2,8 +2,6 @@
|
||||
|
||||
You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control.
|
||||
|
||||
**Note**: This file contains high-level guidelines and references to implementation patterns. For detailed component documentation, API references, and usage examples, refer to the `gallery/` directory.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Reference](#quick-reference)
|
||||
@@ -153,10 +151,6 @@ try {
|
||||
### Styling Guidelines
|
||||
|
||||
- **Use CSS custom properties**: Leverage the theme system
|
||||
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
||||
- Spacing scale: `--ha-space-0` (0px) through `--ha-space-20` (80px) in 4px increments
|
||||
- Defined in `src/resources/theme/core.globals.ts`
|
||||
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
||||
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
||||
- **Follow Material Design**: Use Material Web Components where appropriate
|
||||
- **Support RTL**: Ensure all layouts work in RTL languages
|
||||
@@ -165,68 +159,21 @@ try {
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
padding: var(--ha-space-4);
|
||||
--spacing: 16px;
|
||||
padding: var(--spacing);
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
:host {
|
||||
padding: var(--ha-space-2);
|
||||
--spacing: 8px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
### View Transitions
|
||||
|
||||
The View Transitions API creates smooth animations between DOM state changes. When implementing view transitions:
|
||||
|
||||
**Core Resources:**
|
||||
|
||||
- **Utility wrapper**: `src/common/util/view-transition.ts` - `withViewTransition()` function with graceful fallback
|
||||
- **Real-world example**: `src/util/launch-screen.ts` - Launch screen fade pattern with browser support detection
|
||||
- **Animation keyframes**: `src/resources/theme/animations.globals.ts` - Global `fade-in`, `fade-out`, `scale` animations
|
||||
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-base-duration` (350ms, respects `prefers-reduced-motion`)
|
||||
|
||||
**Implementation Guidelines:**
|
||||
|
||||
1. Always use `withViewTransition()` wrapper for automatic fallback
|
||||
2. Keep transitions simple (subtle crossfades and fades work best)
|
||||
3. Use `--ha-animation-base-duration` CSS variable for consistent timing
|
||||
4. Assign unique `view-transition-name` to elements (must be unique at any given time)
|
||||
5. For Lit components: Override `performUpdate()` or use `::part()` for internal elements
|
||||
|
||||
**Default Root Transition:**
|
||||
|
||||
By default, `:root` receives `view-transition-name: root`, creating a full-page crossfade. Target with [`::view-transition-group(root)`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-group) to customize the default page transition.
|
||||
|
||||
**Important Constraints:**
|
||||
|
||||
- Each `view-transition-name` must be unique at any given time
|
||||
- Only one view transition can run at a time
|
||||
- **Shadow DOM incompatibility**: View transitions operate at document level and do not work within Shadow DOM due to style isolation ([spec discussion](https://github.com/w3c/csswg-drafts/issues/10303)). For web components, set `view-transition-name` on the `:host` element or use document-level transitions
|
||||
|
||||
**Current Usage & Planned Applications:**
|
||||
|
||||
- Launch screen fade out (implemented)
|
||||
- Automation sidebar transitions (planned - #27238)
|
||||
- More info dialog content changes (planned - #27672)
|
||||
- Toolbar navigation, ha-spinner transitions (planned)
|
||||
|
||||
**Specification & Documentation:**
|
||||
|
||||
For browser support, API details, and current specifications, refer to these authoritative sources (note: check publication dates as specs evolve):
|
||||
|
||||
- [MDN: View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) - Comprehensive API reference
|
||||
- [Chrome for Developers: View Transitions](https://developer.chrome.com/docs/web-platform/view-transitions) - Implementation guide and examples
|
||||
- [W3C Draft Specification](https://drafts.csswg.org/css-view-transitions/) - Official specification (evolving)
|
||||
|
||||
### Performance Best Practices
|
||||
|
||||
- **Code split**: Split code at the panel/dialog level
|
||||
@@ -248,9 +195,8 @@ For browser support, API details, and current specifications, refer to these aut
|
||||
|
||||
**Available Dialog Types:**
|
||||
|
||||
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
|
||||
- `ha-md-dialog` - Material Design 3 dialog component
|
||||
- `ha-dialog` - Legacy component (still widely used)
|
||||
- `ha-md-dialog` - Preferred for new code (Material Design 3)
|
||||
- `ha-dialog` - Legacy component still widely used
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
|
||||
@@ -265,45 +211,15 @@ fireEvent(this, "show-dialog", {
|
||||
**Dialog Implementation Requirements:**
|
||||
|
||||
- Implement `HassDialog<T>` interface
|
||||
- Use `@state() private _open = false` to control dialog visibility
|
||||
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
|
||||
- Use `createCloseHeading()` for standard headers
|
||||
- Import `haStyleDialog` for consistent styling
|
||||
- Return `nothing` when no params (loading state)
|
||||
- Fire `dialog-closed` event in `_dialogClosed()` handler
|
||||
- Use `header-title` attribute for simple titles
|
||||
- Use `header-subtitle` attribute for simple subtitles
|
||||
- Use slots for custom content where the standard attributes are not enough
|
||||
- Use `ha-dialog-footer` with `primaryAction`/`secondaryAction` slots for footer content
|
||||
- Add `autofocus` to first focusable element (e.g., `<ha-form autofocus>`). The component may need to forward this attribute internally.
|
||||
- Fire `dialog-closed` event when closing
|
||||
- Add `dialogInitialFocus` for accessibility
|
||||
|
||||
**Dialog Sizing:**
|
||||
|
||||
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
|
||||
- Custom sizing is NOT recommended - use the standard width presets
|
||||
- Example: `<ha-wa-dialog width="small">` for alert/confirmation dialogs
|
||||
|
||||
**Button Appearance Guidelines:**
|
||||
|
||||
- **Primary action buttons**: Default appearance (no appearance attribute) or omit for standard styling
|
||||
- **Secondary action buttons**: Use `appearance="plain"` for cancel/dismiss actions
|
||||
- **Destructive actions**: Use `appearance="filled"` for delete/remove operations (combined with appropriate semantic styling)
|
||||
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
|
||||
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
|
||||
|
||||
**Recent Examples:**
|
||||
|
||||
See these files for current patterns:
|
||||
|
||||
- `src/panels/config/repairs/dialog-repairs-issue.ts`
|
||||
- `src/dialogs/restart/dialog-restart.ts`
|
||||
- `src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts`
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-wa-dialog.markdown`
|
||||
- `gallery/src/pages/components/ha-dialogs.markdown`
|
||||
````
|
||||
|
||||
### Form Component (ha-form)
|
||||
|
||||
- Schema-driven using `HaFormSchema[]`
|
||||
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
|
||||
- Built-in validation with error display
|
||||
@@ -319,11 +235,7 @@ See these files for current patterns:
|
||||
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
```
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-form.markdown`
|
||||
````
|
||||
|
||||
### Alert Component (ha-alert)
|
||||
|
||||
@@ -337,35 +249,6 @@ See these files for current patterns:
|
||||
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
|
||||
```
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-alert.markdown`
|
||||
|
||||
### Keyboard Shortcuts (ShortcutManager)
|
||||
|
||||
The `ShortcutManager` class provides a unified way to register keyboard shortcuts with automatic input field protection.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Automatically blocks shortcuts when input fields are focused
|
||||
- Prevents shortcuts during text selection (configurable via `allowWhenTextSelected`)
|
||||
- Supports both character-based and KeyCode-based shortcuts (for non-latin keyboards)
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- **Class definition**: `src/common/keyboard/shortcuts.ts`
|
||||
- **Real-world example**: `src/state/quick-bar-mixin.ts` - Global shortcuts (e, c, d, m, a, Shift+?) with non-latin keyboard fallbacks
|
||||
|
||||
### Tooltip Component (ha-tooltip)
|
||||
|
||||
The `ha-tooltip` component wraps Web Awesome tooltip with Home Assistant theming. Use for providing contextual help text on hover.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- **Component definition**: `src/components/ha-tooltip.ts`
|
||||
- **Usage example**: `src/components/ha-label.ts`
|
||||
- **Gallery documentation**: `gallery/src/pages/components/ha-tooltip.markdown`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Creating a Panel
|
||||
@@ -406,19 +289,11 @@ export class DialogMyFeature
|
||||
@state()
|
||||
private _params?: MyDialogParams;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
public async showDialog(params: MyDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -429,27 +304,23 @@ export class DialogMyFeature
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
header-subtitle=${this._params.subtitle}
|
||||
@closed=${this._dialogClosed}
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, this._params.title)}
|
||||
>
|
||||
<p>Dialog content</p>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
<!-- Dialog content -->
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._submit} slot="primaryAction">
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.11.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.10.3.cjs
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.19",
|
||||
"@fullcalendar/luxon3": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@home-assistant/webawesome": "3.0.0",
|
||||
"@home-assistant/webawesome": "3.0.0-ha.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
@@ -194,7 +194,7 @@
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"glob": "12.0.0",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
@@ -233,10 +233,9 @@
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"globals": "16.5.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.11.0",
|
||||
"packageManager": "yarn@4.10.3",
|
||||
"volta": {
|
||||
"node": "22.21.1"
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
|
||||
export interface AreasFloorHierarchy {
|
||||
floors: {
|
||||
id: string;
|
||||
areas: string[];
|
||||
}[];
|
||||
areas: string[];
|
||||
}
|
||||
|
||||
export const getAreasFloorHierarchy = (
|
||||
floors: FloorRegistryEntry[],
|
||||
areas: AreaRegistryEntry[]
|
||||
): AreasFloorHierarchy => {
|
||||
const floorAreas = new Map<string, string[]>();
|
||||
const unassignedAreas: string[] = [];
|
||||
|
||||
for (const area of areas) {
|
||||
if (area.floor_id) {
|
||||
if (!floorAreas.has(area.floor_id)) {
|
||||
floorAreas.set(area.floor_id, []);
|
||||
}
|
||||
floorAreas.get(area.floor_id)!.push(area.area_id);
|
||||
} else {
|
||||
unassignedAreas.push(area.area_id);
|
||||
}
|
||||
}
|
||||
|
||||
const hierarchy: AreasFloorHierarchy = {
|
||||
floors: floors.map((floor) => ({
|
||||
id: floor.floor_id,
|
||||
areas: floorAreas.get(floor.floor_id) || [],
|
||||
})),
|
||||
areas: unassignedAreas,
|
||||
};
|
||||
|
||||
return hierarchy;
|
||||
};
|
||||
|
||||
export const getAreasOrder = (hierarchy: AreasFloorHierarchy): string[] => {
|
||||
const order: string[] = [];
|
||||
|
||||
for (const floor of hierarchy.floors) {
|
||||
order.push(...floor.areas);
|
||||
}
|
||||
order.push(...hierarchy.areas);
|
||||
|
||||
return order;
|
||||
};
|
||||
|
||||
export const getFloorOrder = (hierarchy: AreasFloorHierarchy): string[] =>
|
||||
hierarchy.floors.map((floor) => floor.id);
|
||||
@@ -1,67 +0,0 @@
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { canOverrideAlphanumericInput } from "../dom/can-override-input";
|
||||
|
||||
/**
|
||||
* A function to handle a keyboard shortcut.
|
||||
*/
|
||||
export type ShortcutHandler = (event: KeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
* Configuration for a keyboard shortcut.
|
||||
*/
|
||||
export interface ShortcutConfig {
|
||||
handler: ShortcutHandler;
|
||||
/**
|
||||
* If true, allows shortcuts even when text is selected.
|
||||
* Default is false to avoid interrupting copy/paste.
|
||||
*/
|
||||
allowWhenTextSelected?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register keyboard shortcuts using tinykeys.
|
||||
* Automatically blocks shortcuts in input fields and during text selection.
|
||||
*/
|
||||
function registerShortcuts(
|
||||
shortcuts: Record<string, ShortcutConfig>
|
||||
): () => void {
|
||||
const wrappedShortcuts: Record<string, ShortcutHandler> = {};
|
||||
|
||||
Object.entries(shortcuts).forEach(([key, config]) => {
|
||||
wrappedShortcuts[key] = (event: KeyboardEvent) => {
|
||||
if (!canOverrideAlphanumericInput(event.composedPath())) {
|
||||
return;
|
||||
}
|
||||
if (!config.allowWhenTextSelected && window.getSelection()?.toString()) {
|
||||
return;
|
||||
}
|
||||
config.handler(event);
|
||||
};
|
||||
});
|
||||
|
||||
return tinykeys(window, wrappedShortcuts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages keyboard shortcuts registration and cleanup.
|
||||
*/
|
||||
export class ShortcutManager {
|
||||
private _disposer?: () => void;
|
||||
|
||||
/**
|
||||
* Register keyboard shortcuts.
|
||||
* Uses tinykeys syntax: https://github.com/jamiebuilds/tinykeys#usage
|
||||
*/
|
||||
public add(shortcuts: Record<string, ShortcutConfig>) {
|
||||
this._disposer?.();
|
||||
this._disposer = registerShortcuts(shortcuts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all registered shortcuts.
|
||||
*/
|
||||
public remove() {
|
||||
this._disposer?.();
|
||||
this._disposer = undefined;
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,6 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
layout: physicsEnabled ? "force" : "none",
|
||||
draggable: true,
|
||||
roam: true,
|
||||
roamTrigger: "global",
|
||||
selectedMode: "single",
|
||||
label: {
|
||||
show: showLabels,
|
||||
|
||||
@@ -298,18 +298,6 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
if (properties.has("data")) {
|
||||
// Clean up checked rows that no longer exist in the data
|
||||
if (this._checkedRows.length) {
|
||||
const validIds = new Set(this.data.map((row) => String(row[this.id])));
|
||||
const validCheckedRows = this._checkedRows.filter((id) =>
|
||||
validIds.has(id)
|
||||
);
|
||||
if (validCheckedRows.length !== this._checkedRows.length) {
|
||||
this._checkedRows = validCheckedRows;
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
this._checkableRowsCount = this.data.filter(
|
||||
(row) => row.selectable !== false
|
||||
).length;
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { HomeAssistant } from "../types";
|
||||
import { AudioRecorder } from "../util/audio-recorder";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-alert";
|
||||
import "./ha-markdown";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@@ -41,11 +40,7 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
@query("#message-input") private _messageInput!: HaTextField;
|
||||
|
||||
@query(".message:last-child")
|
||||
private _lastChatMessage!: LitElement;
|
||||
|
||||
@query(".message:last-child img:last-of-type")
|
||||
private _lastChatMessageImage: HTMLImageElement | undefined;
|
||||
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
|
||||
|
||||
@state() private _conversation: AssistMessage[] = [];
|
||||
|
||||
@@ -97,7 +92,10 @@ export class HaAssistChat extends LitElement {
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._audioRecorder?.close();
|
||||
this._audioRecorder = undefined;
|
||||
this._unloadAudio();
|
||||
this._conversation = [];
|
||||
this._conversationId = null;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -114,7 +112,7 @@ export class HaAssistChat extends LitElement {
|
||||
const supportsSTT = this.pipeline?.stt_engine && !this.disableSpeech;
|
||||
|
||||
return html`
|
||||
<div class="messages">
|
||||
<div class="messages" id="scroll-container">
|
||||
${controlHA
|
||||
? nothing
|
||||
: html`
|
||||
@@ -126,18 +124,11 @@ export class HaAssistChat extends LitElement {
|
||||
`}
|
||||
<div class="spacer"></div>
|
||||
${this._conversation!.map(
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
(message) => html`
|
||||
<ha-markdown
|
||||
class="message ${classMap({
|
||||
error: !!message.error,
|
||||
[message.who]: true,
|
||||
})}"
|
||||
breaks
|
||||
cache
|
||||
.content=${message.text}
|
||||
>
|
||||
</ha-markdown>
|
||||
`
|
||||
<div class="message ${classMap({ error: !!message.error, [message.who]: true })}">${message.text}</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<div class="input" slot="primaryAction">
|
||||
@@ -198,28 +189,12 @@ export class HaAssistChat extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _scrollMessagesBottom() {
|
||||
const lastChatMessage = this._lastChatMessage;
|
||||
if (!lastChatMessage.hasUpdated) {
|
||||
await lastChatMessage.updateComplete;
|
||||
}
|
||||
if (
|
||||
this._lastChatMessageImage &&
|
||||
!this._lastChatMessageImage.naturalHeight
|
||||
) {
|
||||
try {
|
||||
await this._lastChatMessageImage.decode();
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Failed to decode image:", err);
|
||||
}
|
||||
}
|
||||
const isLastMessageFullyVisible =
|
||||
lastChatMessage.getBoundingClientRect().y <
|
||||
this.getBoundingClientRect().top + 24;
|
||||
if (!isLastMessageFullyVisible) {
|
||||
lastChatMessage.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
private _scrollMessagesBottom() {
|
||||
const scrollContainer = this._scrollContainer;
|
||||
if (!scrollContainer) {
|
||||
return;
|
||||
}
|
||||
scrollContainer.scrollTo(0, scrollContainer.scrollHeight);
|
||||
}
|
||||
|
||||
private _handleKeyUp(ev: KeyboardEvent) {
|
||||
@@ -611,31 +586,42 @@ export class HaAssistChat extends LitElement {
|
||||
flex: 1;
|
||||
}
|
||||
.message {
|
||||
white-space: pre-line;
|
||||
font-size: var(--ha-font-size-l);
|
||||
clear: both;
|
||||
max-width: -webkit-fill-available;
|
||||
overflow-wrap: break-word;
|
||||
scroll-margin-top: 24px;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
}
|
||||
.message:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
}
|
||||
|
||||
.message p {
|
||||
margin: 0;
|
||||
}
|
||||
.message p:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
align-self: flex-end;
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
--markdown-link-color: var(--text-primary-color);
|
||||
background-color: var(--chat-background-color-user, var(--primary-color));
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.hass {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
@@ -650,21 +636,20 @@ export class HaAssistChat extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.user a {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.message.hass a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background-color: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-markdown {
|
||||
--markdown-image-border-radius: calc(var(--ha-border-radius-xl) / 2);
|
||||
--markdown-table-border-color: var(--divider-color);
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
@@ -94,12 +94,6 @@ export class HaDateInput extends LitElement {
|
||||
}
|
||||
|
||||
private _keyDown(ev: KeyboardEvent) {
|
||||
if (["Space", "Enter"].includes(ev.code)) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._openDialog();
|
||||
return;
|
||||
}
|
||||
if (!this.canClear) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,8 +90,7 @@ export class HaDialog extends DialogBase {
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
||||
var(--ha-space-4);
|
||||
padding: 12px 16px 16px 16px;
|
||||
}
|
||||
.mdc-dialog__actions span:nth-child(1) {
|
||||
flex: var(--secondary-action-button-flex, unset);
|
||||
@@ -101,24 +100,22 @@ export class HaDialog extends DialogBase {
|
||||
}
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertical-align-dialog, center);
|
||||
padding: var(--dialog-container-padding, var(--ha-space-0));
|
||||
}
|
||||
.mdc-dialog__title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-0)
|
||||
var(--ha-space-4);
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
.mdc-dialog__title:has(span) {
|
||||
padding: var(--ha-space-3) var(--ha-space-3) var(--ha-space-0);
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
.mdc-dialog__title::before {
|
||||
content: unset;
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__content {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
||||
padding: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
||||
padding-bottom: var(--dialog-content-padding, var(--ha-space-6));
|
||||
padding-bottom: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__surface {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
@@ -136,7 +133,7 @@ export class HaDialog extends DialogBase {
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
padding: var(--dialog-surface-padding, var(--ha-space-0));
|
||||
padding: var(--dialog-surface-padding);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
@@ -153,22 +150,22 @@ export class HaDialog extends DialogBase {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
padding-left: var(--ha-space-1);
|
||||
padding-right: var(--ha-space-1);
|
||||
margin-right: var(--ha-space-3);
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
margin-right: 12px;
|
||||
margin-inline-end: 12px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.header_button {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: calc(var(--ha-space-3) * -1);
|
||||
inset-inline-end: -12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.dialog-actions {
|
||||
inset-inline-start: initial !important;
|
||||
inset-inline-end: var(--ha-space-0) !important;
|
||||
inset-inline-end: 0px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -60,10 +60,6 @@ class HaHLSPlayer extends LitElement {
|
||||
private static streamCount = 0;
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (document.pictureInPictureElement) {
|
||||
// video is playing in picture-in-picture mode, don't do anything
|
||||
return;
|
||||
}
|
||||
if (document.hidden) {
|
||||
this._cleanUp();
|
||||
} else {
|
||||
|
||||
@@ -154,7 +154,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
return this._getLabelsMemoized(
|
||||
this.hass,
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._labels,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement, render, html } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import hash from "object-hash";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { renderMarkdown } from "../resources/render-markdown";
|
||||
import { CacheManager } from "../util/cache-manager";
|
||||
|
||||
const h = (template: ReturnType<typeof unsafeHTML>) => html`${template}`;
|
||||
|
||||
const markdownCache = new CacheManager<string>(1000);
|
||||
|
||||
const _gitHubMarkdownAlerts = {
|
||||
@@ -52,26 +48,18 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
private _renderPromise: ReturnType<typeof this._render> = Promise.resolve();
|
||||
|
||||
protected update(changedProps) {
|
||||
super.update(changedProps);
|
||||
if (this.content !== undefined) {
|
||||
this._renderPromise = this._render();
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
|
||||
protected async getUpdateComplete(): Promise<boolean> {
|
||||
await super.getUpdateComplete();
|
||||
await this._renderPromise;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (!this.innerHTML && this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
if (markdownCache.has(key)) {
|
||||
render(markdownCache.get(key)!, this.renderRoot);
|
||||
this.innerHTML = markdownCache.get(key)!;
|
||||
this._resize();
|
||||
}
|
||||
}
|
||||
@@ -87,7 +75,7 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
|
||||
private async _render() {
|
||||
const elements = await renderMarkdown(
|
||||
this.innerHTML = await renderMarkdown(
|
||||
String(this.content),
|
||||
{
|
||||
breaks: this.breaks,
|
||||
@@ -99,11 +87,6 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
elements.map((e) => h(unsafeHTML(e))),
|
||||
this.renderRoot
|
||||
);
|
||||
|
||||
this._resize();
|
||||
|
||||
const walker = document.createTreeWalker(
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type ReactiveElement,
|
||||
type CSSResultGroup,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-markdown-element";
|
||||
|
||||
@customElement("ha-markdown")
|
||||
@@ -25,14 +18,6 @@ export class HaMarkdown extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
@query("ha-markdown-element") private _markdownElement!: ReactiveElement;
|
||||
|
||||
protected async getUpdateComplete() {
|
||||
const result = await super.getUpdateComplete();
|
||||
await this._markdownElement.updateComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.content) {
|
||||
return nothing;
|
||||
@@ -68,46 +53,19 @@ export class HaMarkdown extends LitElement {
|
||||
margin: var(--ha-space-1) 0;
|
||||
}
|
||||
a {
|
||||
color: var(--markdown-link-color, var(--primary-color));
|
||||
color: var(--primary-color);
|
||||
}
|
||||
img {
|
||||
background-color: rgba(10, 10, 10, 0.15);
|
||||
border-radius: var(--markdown-image-border-radius);
|
||||
max-width: 100%;
|
||||
min-height: 2lh;
|
||||
height: auto;
|
||||
width: auto;
|
||||
text-indent: 4px;
|
||||
transition: height 0.2s ease-in-out;
|
||||
}
|
||||
p:first-child > img:first-child {
|
||||
vertical-align: top;
|
||||
}
|
||||
p:first-child > img:last-child {
|
||||
vertical-align: top;
|
||||
}
|
||||
ol,
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
li {
|
||||
&:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
& > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
color: var(--markdown-svg-color, none);
|
||||
}
|
||||
code,
|
||||
pre {
|
||||
background-color: var(--markdown-code-background-color, none);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
color: var(--markdown-code-text-color, inherit);
|
||||
}
|
||||
svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
color: var(--markdown-svg-color, none);
|
||||
}
|
||||
code {
|
||||
font-size: var(--ha-font-size-s);
|
||||
@@ -139,24 +97,6 @@ export class HaMarkdown extends LitElement {
|
||||
border-bottom: none;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
th {
|
||||
text-align: start;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
border: 1px solid var(--markdown-table-border-color, transparent);
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 4px solid var(--divider-color);
|
||||
margin-inline: 0;
|
||||
padding-inline: 1em;
|
||||
}
|
||||
` as CSSResultGroup;
|
||||
}
|
||||
|
||||
|
||||
@@ -175,10 +175,10 @@ export class HaMdDialog extends Dialog {
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: var(--safe-area-inset-top, var(--ha-space-0));
|
||||
margin-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
|
||||
margin-left: var(--safe-area-inset-left, var(--ha-space-0));
|
||||
margin-right: var(--safe-area-inset-right, var(--ha-space-0));
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ export class HaMdDialog extends Dialog {
|
||||
}
|
||||
|
||||
slot[name="actions"]::slotted(*) {
|
||||
padding: var(--ha-space-4);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
@@ -195,7 +195,7 @@ export class HaMdDialog extends Dialog {
|
||||
}
|
||||
|
||||
slot[name="content"]::slotted(*) {
|
||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
||||
padding: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
.scrim {
|
||||
z-index: 10; /* overlay navigation */
|
||||
|
||||
@@ -6,7 +6,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { titleCase } from "../common/string/title-case";
|
||||
import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
@@ -45,7 +44,7 @@ const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
||||
path: `/${panel.url_path}`,
|
||||
icon: panel.icon ?? "mdi:view-dashboard",
|
||||
title:
|
||||
panel.url_path === getDefaultPanelUrlPath(hass)
|
||||
panel.url_path === hass.defaultPanel
|
||||
? hass.localize("panel.states")
|
||||
: hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title ||
|
||||
|
||||
@@ -192,7 +192,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
@focus=${this._focusList}
|
||||
@visibilityChanged=${this._visibilityChanged}
|
||||
>
|
||||
</lit-virtualizer> `;
|
||||
</lit-virtualizer>`;
|
||||
}
|
||||
|
||||
private _renderSectionButtons() {
|
||||
|
||||
28
src/components/ha-section-title.ts
Normal file
28
src/components/ha-section-title.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-section-title")
|
||||
class HaSectionTitle extends LitElement {
|
||||
protected render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-section-title": HaSectionTitle;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-checkbox";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-markdown";
|
||||
import "./ha-selector/ha-selector";
|
||||
import "./ha-service-picker";
|
||||
import "./ha-service-section-icon";
|
||||
@@ -685,14 +684,10 @@ export class HaServiceControl extends LitElement {
|
||||
dataField.key}</span
|
||||
>
|
||||
<span slot="description"
|
||||
><ha-markdown
|
||||
breaks
|
||||
allow-svg
|
||||
.content=${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
) || dataField?.description}
|
||||
></ha-markdown>
|
||||
</span>
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
) || dataField?.description}</span
|
||||
>
|
||||
<ha-selector
|
||||
.context=${this._selectorContext(targetEntities)}
|
||||
.disabled=${this.disabled ||
|
||||
|
||||
@@ -33,7 +33,6 @@ import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import type { PersistentNotification } from "../data/persistent_notification";
|
||||
import { subscribeNotifications } from "../data/persistent_notification";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
@@ -143,7 +142,7 @@ const defaultPanelSorter = (
|
||||
export const computePanels = memoizeOne(
|
||||
(
|
||||
panels: HomeAssistant["panels"],
|
||||
defaultPanel: string,
|
||||
defaultPanel: HomeAssistant["defaultPanel"],
|
||||
panelsOrder: string[],
|
||||
hiddenPanels: string[],
|
||||
locale: HomeAssistant["locale"]
|
||||
@@ -299,8 +298,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
hass.localize !== oldHass.localize ||
|
||||
hass.locale !== oldHass.locale ||
|
||||
hass.states !== oldHass.states ||
|
||||
hass.userData !== oldHass.userData ||
|
||||
hass.systemData !== oldHass.systemData ||
|
||||
hass.defaultPanel !== oldHass.defaultPanel ||
|
||||
hass.connected !== oldHass.connected
|
||||
);
|
||||
}
|
||||
@@ -403,11 +401,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
defaultPanel,
|
||||
this.hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels,
|
||||
this.hass.locale
|
||||
@@ -422,27 +418,23 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this._renderPanels(beforeSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderPanels(beforeSpacer, selectedPanel)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||
${this._renderExternalConfiguration()}
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanels(
|
||||
panels: PanelInfo[],
|
||||
selectedPanel: string,
|
||||
defaultPanel: string
|
||||
) {
|
||||
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||
return panels.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
panel.url_path === defaultPanel
|
||||
panel.url_path === this.hass.defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.icon,
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
areaMeetsFilter,
|
||||
deviceMeetsFilter,
|
||||
entityRegMeetsFilter,
|
||||
getTargetComboBoxItemType,
|
||||
type TargetType,
|
||||
type TargetTypeFloorless,
|
||||
} from "../data/target";
|
||||
@@ -47,7 +48,6 @@ import "./ha-tree-indicator";
|
||||
import "./target-picker/ha-target-picker-item-group";
|
||||
import "./target-picker/ha-target-picker-value-chip";
|
||||
|
||||
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
|
||||
const SEPARATOR = "________";
|
||||
const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
@@ -634,35 +634,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getRowType = (
|
||||
item:
|
||||
| PickerComboBoxItem
|
||||
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
) => {
|
||||
if (
|
||||
(item as FloorComboBoxItem).type === "area" ||
|
||||
(item as FloorComboBoxItem).type === "floor"
|
||||
) {
|
||||
return (item as FloorComboBoxItem).type;
|
||||
}
|
||||
|
||||
if ("domain" in item) {
|
||||
return "device";
|
||||
}
|
||||
|
||||
if ("stateObj" in item) {
|
||||
return "entity";
|
||||
}
|
||||
|
||||
if (item.id === EMPTY_SEARCH) {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return "label";
|
||||
};
|
||||
|
||||
private _sectionTitleFunction = ({
|
||||
firstIndex,
|
||||
lastIndex,
|
||||
@@ -686,7 +657,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const type = this._getRowType(firstItem as PickerComboBoxItem);
|
||||
const type = getTargetComboBoxItemType(firstItem as PickerComboBoxItem);
|
||||
const translationType:
|
||||
| "areas"
|
||||
| "entities"
|
||||
@@ -858,7 +829,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
if (!filterType || filterType === "label") {
|
||||
let labels = this._getLabelsMemoized(
|
||||
this.hass,
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._labelRegistry,
|
||||
includeDomains,
|
||||
undefined,
|
||||
@@ -974,7 +948,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const type = this._getRowType(item);
|
||||
const type = getTargetComboBoxItemType(item);
|
||||
let hasFloor = false;
|
||||
let rtl = false;
|
||||
let showEntityId = false;
|
||||
|
||||
@@ -235,7 +235,7 @@ export class HaWaDialog extends LitElement {
|
||||
}
|
||||
|
||||
:host([width="large"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
|
||||
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="full"]) wa-dialog {
|
||||
|
||||
@@ -62,10 +62,6 @@ class HaWebRtcPlayer extends LitElement {
|
||||
private _candidatesList: RTCIceCandidate[] = [];
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (document.pictureInPictureElement) {
|
||||
// video is playing in picture-in-picture mode, don't do anything
|
||||
return;
|
||||
}
|
||||
if (document.hidden) {
|
||||
this._cleanUp();
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
removeLocalMedia,
|
||||
} from "../../data/media_source";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-check-list-item";
|
||||
@@ -305,7 +305,6 @@ class DialogMediaManage extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 9;
|
||||
@@ -315,9 +314,9 @@ class DialogMediaManage extends LitElement {
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--dialog-surface-position: fixed;
|
||||
--dialog-surface-top: 40px;
|
||||
--mdc-dialog-max-height: calc(100vh - 72px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
MediaPlayerItem,
|
||||
MediaPlayerLayoutType,
|
||||
} from "../../data/media-player";
|
||||
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-header";
|
||||
@@ -223,7 +223,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 9;
|
||||
@@ -231,27 +230,23 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
ha-media-player-browse {
|
||||
--media-browser-max-height: calc(
|
||||
100vh - 65px - var(--safe-area-inset-y)
|
||||
);
|
||||
--media-browser-max-height: calc(100vh - 65px);
|
||||
}
|
||||
|
||||
:host(.opened) ha-media-player-browse {
|
||||
height: calc(100vh - 65px - var(--safe-area-inset-y));
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--dialog-surface-position: fixed;
|
||||
--dialog-surface-top: 40px;
|
||||
--mdc-dialog-max-height: calc(100vh - 72px);
|
||||
}
|
||||
ha-media-player-browse {
|
||||
position: initial;
|
||||
--media-browser-max-height: calc(
|
||||
100vh - 145px - var(--safe-area-inset-y)
|
||||
);
|
||||
--media-browser-max-height: calc(100vh - 145px);
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ class SearchInput extends LitElement {
|
||||
return html`
|
||||
<ha-textfield
|
||||
.autofocus=${this.autofocus}
|
||||
autocomplete="off"
|
||||
.label=${this.label || this.hass.localize("ui.common.search")}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getAreasFloorHierarchy } from "../common/areas/areas-floor-hierarchy";
|
||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
@@ -13,7 +12,11 @@ import {
|
||||
} from "./device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
import type { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||
import type { FloorRegistryEntry } from "./floor_registry";
|
||||
import {
|
||||
floorCompare,
|
||||
getFloorAreaLookup,
|
||||
type FloorRegistryEntry,
|
||||
} from "./floor_registry";
|
||||
|
||||
export interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||
type: "floor" | "area";
|
||||
@@ -21,11 +24,54 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||
area?: AreaRegistryEntry;
|
||||
}
|
||||
|
||||
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
|
||||
floor?: FloorRegistryEntry;
|
||||
areas: FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
|
||||
areas: FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
export interface AreaFloorValue {
|
||||
id: string;
|
||||
type: "floor" | "area";
|
||||
}
|
||||
|
||||
export const getAreasNestedInFloors = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
formatId: (value: AreaFloorValue) => string,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[],
|
||||
includeEmptyFloors = false
|
||||
) =>
|
||||
getAreasAndFloorsItems(
|
||||
states,
|
||||
haFloors,
|
||||
haAreas,
|
||||
haDevices,
|
||||
haEntities,
|
||||
formatId,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
excludeFloors,
|
||||
includeEmptyFloors,
|
||||
true
|
||||
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
|
||||
|
||||
export const getAreasAndFloors = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
@@ -39,8 +85,47 @@ export const getAreasAndFloors = (
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[]
|
||||
): FloorComboBoxItem[] => {
|
||||
excludeFloors?: string[],
|
||||
includeEmptyFloors = false
|
||||
) =>
|
||||
getAreasAndFloorsItems(
|
||||
states,
|
||||
haFloors,
|
||||
haAreas,
|
||||
haDevices,
|
||||
haEntities,
|
||||
formatId,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
excludeFloors,
|
||||
includeEmptyFloors
|
||||
) as FloorComboBoxItem[];
|
||||
|
||||
const getAreasAndFloorsItems = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
formatId: (value: AreaFloorValue) => string,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[],
|
||||
includeEmptyFloors = false,
|
||||
nested = false
|
||||
): (
|
||||
| FloorComboBoxItem
|
||||
| FloorNestedComboBoxItem
|
||||
| UnassignedAreasFloorComboBoxItem
|
||||
)[] => {
|
||||
const floors = Object.values(haFloors);
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
@@ -179,59 +264,66 @@ export const getAreasAndFloors = (
|
||||
);
|
||||
}
|
||||
|
||||
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
|
||||
const floorAreaLookup = getFloorAreaLookup(outputAreas);
|
||||
const unassignedAreas = Object.values(outputAreas).filter(
|
||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||
);
|
||||
|
||||
const items: FloorComboBoxItem[] = [];
|
||||
const compare = floorCompare(haFloors);
|
||||
|
||||
hierarchy.floors.forEach((f) => {
|
||||
const floor = haFloors[f.id];
|
||||
const floorAreas = f.areas.map((areaId) => haAreas[areaId]);
|
||||
|
||||
const floorName = computeFloorName(floor);
|
||||
|
||||
const areaSearchLabels = floorAreas
|
||||
.map((area) => {
|
||||
const areaName = computeAreaName(area);
|
||||
return [area.area_id, ...(areaName ? [areaName] : []), ...area.aliases];
|
||||
})
|
||||
.flat();
|
||||
|
||||
items.push({
|
||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||
type: "floor",
|
||||
primary: floorName,
|
||||
floor: floor,
|
||||
icon: floor.icon || undefined,
|
||||
search_labels: [
|
||||
floor.floor_id,
|
||||
floorName,
|
||||
...floor.aliases,
|
||||
...areaSearchLabels,
|
||||
],
|
||||
if (includeEmptyFloors) {
|
||||
Object.values(haFloors).forEach((floor) => {
|
||||
if (!floorAreaLookup[floor.floor_id]) {
|
||||
floorAreaLookup[floor.floor_id] = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
items.push(
|
||||
...floorAreas.map((area) => {
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
type: "area" as const,
|
||||
primary: areaName || area.area_id,
|
||||
area: area,
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [
|
||||
area.area_id,
|
||||
...(areaName ? [areaName] : []),
|
||||
...area.aliases,
|
||||
],
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
// @ts-ignore
|
||||
const floorAreaEntries: [
|
||||
FloorRegistryEntry | undefined,
|
||||
AreaRegistryEntry[],
|
||||
][] = Object.entries(floorAreaLookup)
|
||||
.map(([floorId, floorAreas]) => {
|
||||
const floor = floors.find((fl) => fl.floor_id === floorId)!;
|
||||
return [floor, floorAreas] as const;
|
||||
})
|
||||
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
|
||||
|
||||
items.push(
|
||||
...hierarchy.areas.map((areaId) => {
|
||||
const area = haAreas[areaId];
|
||||
const items: (
|
||||
| FloorComboBoxItem
|
||||
| FloorNestedComboBoxItem
|
||||
| UnassignedAreasFloorComboBoxItem
|
||||
)[] = [];
|
||||
|
||||
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||
let floorItem: FloorComboBoxItem | FloorNestedComboBoxItem;
|
||||
|
||||
if (floor) {
|
||||
const floorName = computeFloorName(floor);
|
||||
|
||||
const areaSearchLabels = floorAreas
|
||||
.map((area) => {
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return [area.area_id, areaName, ...area.aliases];
|
||||
})
|
||||
.flat();
|
||||
|
||||
floorItem = {
|
||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||
type: "floor",
|
||||
primary: floorName,
|
||||
floor: floor,
|
||||
icon: floor.icon || undefined,
|
||||
search_labels: [
|
||||
floor.floor_id,
|
||||
floorName,
|
||||
...floor.aliases,
|
||||
...areaSearchLabels,
|
||||
],
|
||||
};
|
||||
}
|
||||
const floorAreasItems = floorAreas.map((area) => {
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
@@ -241,8 +333,38 @@ export const getAreasAndFloors = (
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (floor) {
|
||||
items.push(floorItem!);
|
||||
}
|
||||
|
||||
if (nested && floor) {
|
||||
(floorItem! as FloorNestedComboBoxItem).areas = floorAreasItems;
|
||||
} else {
|
||||
items.push(...floorAreasItems);
|
||||
}
|
||||
});
|
||||
|
||||
const unassignedAreaItems = unassignedAreas.map((area) => {
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
type: "area" as const,
|
||||
primary: areaName,
|
||||
area: area,
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||
};
|
||||
});
|
||||
|
||||
if (nested && unassignedAreaItems.length) {
|
||||
items.push({
|
||||
areas: unassignedAreaItems,
|
||||
} as UnassignedAreasFloorComboBoxItem);
|
||||
} else {
|
||||
items.push(...unassignedAreaItems);
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DeviceRegistryEntry } from "./device_registry";
|
||||
import type { EntityRegistryEntry } from "./entity_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
@@ -18,7 +21,10 @@ export interface AreaRegistryEntry extends RegistryEntry {
|
||||
temperature_entity_id: string | null;
|
||||
}
|
||||
|
||||
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
|
||||
export type AreaEntityLookup = Record<
|
||||
string,
|
||||
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
|
||||
>;
|
||||
|
||||
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
|
||||
|
||||
@@ -59,21 +65,18 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
|
||||
area_id: areaId,
|
||||
});
|
||||
|
||||
export const reorderAreaRegistryEntries = (
|
||||
hass: HomeAssistant,
|
||||
areaIds: string[]
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/area_registry/reorder",
|
||||
area_ids: areaIds,
|
||||
});
|
||||
|
||||
export const getAreaEntityLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
|
||||
filterHidden = false
|
||||
): AreaEntityLookup => {
|
||||
const areaEntityLookup: AreaEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.area_id) {
|
||||
if (
|
||||
!entity.area_id ||
|
||||
(filterHidden &&
|
||||
((entity as EntityRegistryDisplayEntry).hidden ||
|
||||
(entity as EntityRegistryEntry).hidden_by))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.area_id in areaEntityLookup)) {
|
||||
|
||||
@@ -31,7 +31,6 @@ export interface CalendarEventData {
|
||||
dtend: string;
|
||||
rrule?: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export interface CalendarEventMutableParams {
|
||||
@@ -40,7 +39,6 @@ export interface CalendarEventMutableParams {
|
||||
dtend: string;
|
||||
rrule?: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
// The scope of a delete/update for a recurring event
|
||||
@@ -98,7 +96,6 @@ export const fetchCalendarEvents = async (
|
||||
uid: ev.uid,
|
||||
summary: ev.summary,
|
||||
description: ev.description,
|
||||
location: ev.location,
|
||||
dtstart: eventStart,
|
||||
dtend: eventEnd,
|
||||
recurrence_id: ev.recurrence_id,
|
||||
|
||||
@@ -50,7 +50,10 @@ export type DeviceEntityDisplayLookup = Record<
|
||||
EntityRegistryDisplayEntry[]
|
||||
>;
|
||||
|
||||
export type DeviceEntityLookup = Record<string, EntityRegistryEntry[]>;
|
||||
export type DeviceEntityLookup = Record<
|
||||
string,
|
||||
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
|
||||
>;
|
||||
|
||||
export interface DeviceRegistryEntryMutableParams {
|
||||
area_id?: string | null;
|
||||
@@ -107,11 +110,17 @@ export const sortDeviceRegistryByName = (
|
||||
);
|
||||
|
||||
export const getDeviceEntityLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
|
||||
filterHidden = false
|
||||
): DeviceEntityLookup => {
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
if (
|
||||
!entity.device_id ||
|
||||
(filterHidden &&
|
||||
((entity as EntityRegistryDisplayEntry).hidden ||
|
||||
(entity as EntityRegistryEntry).hidden_by))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
|
||||
@@ -51,15 +51,6 @@ export const deleteFloorRegistryEntry = (
|
||||
floor_id: floorId,
|
||||
});
|
||||
|
||||
export const reorderFloorRegistryEntries = (
|
||||
hass: HomeAssistant,
|
||||
floorIds: string[]
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/floor_registry/reorder",
|
||||
floor_ids: floorIds,
|
||||
});
|
||||
|
||||
export const getFloorAreaLookup = (
|
||||
areas: AreaRegistryEntry[]
|
||||
): FloorAreaLookup => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
export interface CoreFrontendUserData {
|
||||
showAdvanced?: boolean;
|
||||
showEntityIdPicker?: boolean;
|
||||
defaultPanel?: string;
|
||||
}
|
||||
|
||||
export interface SidebarFrontendUserData {
|
||||
@@ -11,24 +10,15 @@ export interface SidebarFrontendUserData {
|
||||
hiddenPanels: string[];
|
||||
}
|
||||
|
||||
export interface CoreFrontendSystemData {
|
||||
defaultPanel?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
core: CoreFrontendUserData;
|
||||
sidebar: SidebarFrontendUserData;
|
||||
}
|
||||
interface FrontendSystemData {
|
||||
core: CoreFrontendSystemData;
|
||||
}
|
||||
}
|
||||
|
||||
export type ValidUserDataKey = keyof FrontendUserData;
|
||||
|
||||
export type ValidSystemDataKey = keyof FrontendSystemData;
|
||||
|
||||
export const fetchFrontendUserData = async <
|
||||
UserDataKey extends ValidUserDataKey,
|
||||
>(
|
||||
@@ -69,46 +59,3 @@ export const subscribeFrontendUserData = <UserDataKey extends ValidUserDataKey>(
|
||||
key: userDataKey,
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchFrontendSystemData = async <
|
||||
SystemDataKey extends ValidSystemDataKey,
|
||||
>(
|
||||
conn: Connection,
|
||||
key: SystemDataKey
|
||||
): Promise<FrontendSystemData[SystemDataKey] | null> => {
|
||||
const result = await conn.sendMessagePromise<{
|
||||
value: FrontendSystemData[SystemDataKey] | null;
|
||||
}>({
|
||||
type: "frontend/get_system_data",
|
||||
key,
|
||||
});
|
||||
return result.value;
|
||||
};
|
||||
|
||||
export const saveFrontendSystemData = async <
|
||||
SystemDataKey extends ValidSystemDataKey,
|
||||
>(
|
||||
conn: Connection,
|
||||
key: SystemDataKey,
|
||||
value: FrontendSystemData[SystemDataKey]
|
||||
): Promise<void> =>
|
||||
conn.sendMessagePromise<undefined>({
|
||||
type: "frontend/set_system_data",
|
||||
key,
|
||||
value,
|
||||
});
|
||||
|
||||
export const subscribeFrontendSystemData = <
|
||||
SystemDataKey extends ValidSystemDataKey,
|
||||
>(
|
||||
conn: Connection,
|
||||
systemDataKey: SystemDataKey,
|
||||
onChange: (data: { value: FrontendSystemData[SystemDataKey] | null }) => void
|
||||
) =>
|
||||
conn.subscribeMessage<{ value: FrontendSystemData[SystemDataKey] | null }>(
|
||||
onChange,
|
||||
{
|
||||
type: "frontend/subscribe_system_data",
|
||||
key: systemDataKey,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const integrationsWithPanel = {
|
||||
bluetooth: "config/bluetooth",
|
||||
@@ -25,6 +25,8 @@ export type IntegrationType =
|
||||
| "entity"
|
||||
| "system";
|
||||
|
||||
export type DomainManifestLookup = Record<string, IntegrationManifest>;
|
||||
|
||||
export interface IntegrationManifest {
|
||||
is_built_in: boolean;
|
||||
overwrites_built_in?: boolean;
|
||||
|
||||
@@ -101,7 +101,10 @@ export const deleteLabelRegistryEntry = (
|
||||
});
|
||||
|
||||
export const getLabels = (
|
||||
hass: HomeAssistant,
|
||||
hassStates: HomeAssistant["states"],
|
||||
hassAreas: HomeAssistant["areas"],
|
||||
hassDevices: HomeAssistant["devices"],
|
||||
hassEntities: HomeAssistant["entities"],
|
||||
labels?: LabelRegistryEntry[],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
@@ -115,8 +118,8 @@ export const getLabels = (
|
||||
return [];
|
||||
}
|
||||
|
||||
const devices = Object.values(hass.devices);
|
||||
const entities = Object.values(hass.entities);
|
||||
const devices = Object.values(hassDevices);
|
||||
const entities = Object.values(hassEntities);
|
||||
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
@@ -170,7 +173,7 @@ export const getLabels = (
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -181,7 +184,7 @@ export const getLabels = (
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
@@ -200,7 +203,7 @@ export const getLabels = (
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -208,7 +211,7 @@ export const getLabels = (
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -245,7 +248,7 @@ export const getLabels = (
|
||||
|
||||
if (areaIds) {
|
||||
areaIds.forEach((areaId) => {
|
||||
const area = hass.areas[areaId];
|
||||
const area = hassAreas[areaId];
|
||||
area.labels.forEach((label) => usedLabels.add(label));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface LabPreviewFeature {
|
||||
preview_feature: string;
|
||||
domain: string;
|
||||
enabled: boolean;
|
||||
is_built_in: boolean;
|
||||
feedback_url?: string;
|
||||
learn_more_url?: string;
|
||||
report_issue_url?: string;
|
||||
}
|
||||
|
||||
export interface LabPreviewFeaturesResponse {
|
||||
features: LabPreviewFeature[];
|
||||
}
|
||||
|
||||
export const fetchLabFeatures = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<LabPreviewFeature[]> => {
|
||||
const response = await hass.callWS<LabPreviewFeaturesResponse>({
|
||||
type: "labs/list",
|
||||
});
|
||||
return response.features;
|
||||
};
|
||||
|
||||
export const labsUpdatePreviewFeature = (
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
preview_feature: string,
|
||||
enabled: boolean,
|
||||
create_backup?: boolean
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "labs/update",
|
||||
domain,
|
||||
preview_feature,
|
||||
enabled,
|
||||
...(create_backup !== undefined && { create_backup }),
|
||||
});
|
||||
|
||||
const fetchLabFeaturesCollection = (conn: Connection) =>
|
||||
conn
|
||||
.sendMessagePromise<LabPreviewFeaturesResponse>({
|
||||
type: "labs/list",
|
||||
})
|
||||
.then((response) => response.features);
|
||||
|
||||
const subscribeLabUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<LabPreviewFeature[]>
|
||||
) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchLabFeaturesCollection(conn).then((features: LabPreviewFeature[]) =>
|
||||
store.setState(features, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"labs_updated"
|
||||
);
|
||||
|
||||
export const subscribeLabFeatures = (
|
||||
conn: Connection,
|
||||
onChange: (features: LabPreviewFeature[]) => void
|
||||
) =>
|
||||
createCollection<LabPreviewFeature[]>(
|
||||
"_labFeatures",
|
||||
fetchLabFeaturesCollection,
|
||||
subscribeLabUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
@@ -1,25 +1,27 @@
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
export const DEFAULT_PANEL = "lovelace";
|
||||
|
||||
export const getLegacyDefaultPanelUrlPath = (): string | null => {
|
||||
export const getStorageDefaultPanelUrlPath = (): string => {
|
||||
const defaultPanel = window.localStorage.getItem("defaultPanel");
|
||||
return defaultPanel ? JSON.parse(defaultPanel) : null;
|
||||
|
||||
return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL;
|
||||
};
|
||||
|
||||
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string =>
|
||||
hass.userData?.defaultPanel ||
|
||||
hass.systemData?.defaultPanel ||
|
||||
getLegacyDefaultPanelUrlPath() ||
|
||||
DEFAULT_PANEL;
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
|
||||
const panel = getDefaultPanelUrlPath(hass);
|
||||
|
||||
return (panel ? hass.panels[panel] : undefined) ?? hass.panels[DEFAULT_PANEL];
|
||||
export const setDefaultPanel = (
|
||||
element: HTMLElement,
|
||||
urlPath: string
|
||||
): void => {
|
||||
fireEvent(element, "hass-default-panel", { defaultPanel: urlPath });
|
||||
};
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
|
||||
hass.panels[hass.defaultPanel]
|
||||
? hass.panels[hass.defaultPanel]
|
||||
: hass.panels[DEFAULT_PANEL];
|
||||
|
||||
export const getPanelNameTranslationKey = (panel: PanelInfo) => {
|
||||
if (panel.url_path === "lovelace") {
|
||||
return "panel.states" as const;
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { FloorComboBoxItem } from "./area_floor";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { DeviceRegistryEntry } from "./device_registry";
|
||||
import type { DevicePickerItem, DeviceRegistryEntry } from "./device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
import type { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||
import type {
|
||||
EntityComboBoxItem,
|
||||
EntityRegistryDisplayEntry,
|
||||
} from "./entity_registry";
|
||||
|
||||
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
|
||||
export type TargetTypeFloorless = Exclude<TargetType, "floor">;
|
||||
@@ -35,6 +40,28 @@ export const extractFromTarget = async (
|
||||
target,
|
||||
});
|
||||
|
||||
export const getTriggersForTarget = async (
|
||||
callWS: HomeAssistant["callWS"],
|
||||
target: HassServiceTarget,
|
||||
expandGroup = true
|
||||
) =>
|
||||
callWS<string[]>({
|
||||
type: "get_triggers_for_target",
|
||||
target,
|
||||
expand_group: expandGroup,
|
||||
});
|
||||
|
||||
export const getServicesForTarget = async (
|
||||
callWS: HomeAssistant["callWS"],
|
||||
target: HassServiceTarget,
|
||||
expandGroup = true
|
||||
) =>
|
||||
callWS<string[]>({
|
||||
type: "get_services_for_target",
|
||||
target,
|
||||
expand_group: expandGroup,
|
||||
});
|
||||
|
||||
export const areaMeetsFilter = (
|
||||
area: AreaRegistryEntry,
|
||||
devices: HomeAssistant["devices"],
|
||||
@@ -162,3 +189,32 @@ export const entityRegMeetsFilter = (
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getTargetComboBoxItemType = (
|
||||
item:
|
||||
| PickerComboBoxItem
|
||||
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
) => {
|
||||
if (
|
||||
(item as FloorComboBoxItem).type === "area" ||
|
||||
(item as FloorComboBoxItem).type === "floor"
|
||||
) {
|
||||
return (item as FloorComboBoxItem).type;
|
||||
}
|
||||
|
||||
if ("domain" in item) {
|
||||
return "device";
|
||||
}
|
||||
|
||||
if ("stateObj" in item) {
|
||||
return "entity";
|
||||
}
|
||||
|
||||
if (item.id === "___EMPTY_SEARCH___") {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return "label";
|
||||
};
|
||||
|
||||
@@ -72,7 +72,6 @@ export type TranslationCategory =
|
||||
| "system_health"
|
||||
| "application_credentials"
|
||||
| "issues"
|
||||
| "preview_features"
|
||||
| "selector"
|
||||
| "services"
|
||||
| "triggers";
|
||||
|
||||
@@ -50,7 +50,7 @@ import { lightSupportsFavoriteColors } from "../../data/light";
|
||||
import type { ItemType } from "../../data/search";
|
||||
import { SearchableDomains } from "../../data/search";
|
||||
import { getSensorNumericDeviceClasses } from "../../data/sensor";
|
||||
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import "../../state-summary/state-card-content";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
@@ -707,9 +707,14 @@ export class MoreInfoDialog extends LitElement {
|
||||
static get styles() {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
|
||||
--vertical-align-dialog: flex-start;
|
||||
--dialog-surface-margin-top: max(
|
||||
var(--ha-space-10),
|
||||
var(--safe-area-inset-top, var(--ha-space-0))
|
||||
);
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@@ -732,6 +737,13 @@ export class MoreInfoDialog extends LitElement {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* When in fullscreen dialog should be attached to top */
|
||||
ha-dialog {
|
||||
--dialog-surface-margin-top: var(--ha-space-0);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 600px) and (min-height: 501px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 580px;
|
||||
|
||||
@@ -46,11 +46,7 @@ import { getPanelNameTranslationKey } from "../../data/panel";
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../../panels/config/ha-panel-config";
|
||||
import { HaFuse } from "../../resources/fuse";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
haStyleScrollbar,
|
||||
} from "../../resources/styles";
|
||||
import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
@@ -990,7 +986,6 @@ export class QuickBar extends LitElement {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-list {
|
||||
position: relative;
|
||||
@@ -1015,9 +1010,9 @@ export class QuickBar extends LitElement {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--mdc-dialog-min-width: 500px;
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--dialog-surface-position: fixed;
|
||||
--dialog-surface-top: var(--ha-space-10);
|
||||
--mdc-dialog-max-height: calc(100% - var(--ha-space-18));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from "../../data/frontend";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { getDefaultPanelUrlPath } from "../../data/panel";
|
||||
|
||||
@customElement("dialog-edit-sidebar")
|
||||
class DialogEditSidebar extends LitElement {
|
||||
@@ -95,11 +94,9 @@ class DialogEditSidebar extends LitElement {
|
||||
|
||||
const panels = this._panels(this.hass.panels);
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
defaultPanel,
|
||||
this.hass.defaultPanel,
|
||||
this._order,
|
||||
this._hidden,
|
||||
this.hass.locale
|
||||
@@ -123,12 +120,12 @@ class DialogEditSidebar extends LitElement {
|
||||
].map((panel) => ({
|
||||
value: panel.url_path,
|
||||
label:
|
||||
panel.url_path === defaultPanel
|
||||
panel.url_path === this.hass.defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
|
||||
icon: panel.icon || undefined,
|
||||
iconPath:
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -46,13 +46,10 @@ export class HomeAssistantMain extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const sidebarNarrow = this._sidebarNarrow || this._externalSidebar;
|
||||
|
||||
const isPanelReady =
|
||||
this.hass.panels && this.hass.userData && this.hass.systemData;
|
||||
|
||||
return html`
|
||||
<ha-drawer
|
||||
.type=${sidebarNarrow ? "modal" : ""}
|
||||
.open=${sidebarNarrow ? this._drawerOpen : false}
|
||||
.open=${sidebarNarrow ? this._drawerOpen : undefined}
|
||||
.direction=${computeRTLDirection(this.hass)}
|
||||
@MDCDrawer:closed=${this._drawerClosed}
|
||||
>
|
||||
@@ -62,14 +59,12 @@ export class HomeAssistantMain extends LitElement {
|
||||
.route=${this.route}
|
||||
.alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"}
|
||||
></ha-sidebar>
|
||||
${isPanelReady
|
||||
? html`<partial-panel-resolver
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
slot="appContent"
|
||||
></partial-panel-resolver>`
|
||||
: nothing}
|
||||
<partial-panel-resolver
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
slot="appContent"
|
||||
></partial-panel-resolver>
|
||||
</ha-drawer>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { getStorageDefaultPanelUrlPath } from "../data/panel";
|
||||
import type { WindowWithPreloads } from "../data/preloads";
|
||||
import type { RecorderInfo } from "../data/recorder";
|
||||
import { getRecorderInfo } from "../data/recorder";
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../util/register-service-worker";
|
||||
import "./ha-init-page";
|
||||
import "./home-assistant-main";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
|
||||
const useHash = __DEMO__;
|
||||
const curPath = () =>
|
||||
@@ -52,6 +53,11 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
super();
|
||||
const path = curPath();
|
||||
|
||||
if (["", "/"].includes(path)) {
|
||||
navigate(`/${getStorageDefaultPanelUrlPath()}${location.search}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
this._route = {
|
||||
prefix: "",
|
||||
path,
|
||||
|
||||
@@ -80,12 +80,10 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
${this._data!.rrule
|
||||
? this._renderRRuleAsText(this._data.rrule)
|
||||
: ""}
|
||||
${this._data.location
|
||||
? html`${this._data.location} <br />`
|
||||
: nothing}
|
||||
${this._data.description
|
||||
? html`<br />
|
||||
<div class="description">${this._data.description}</div>`
|
||||
<div class="description">${this._data.description}</div>
|
||||
<br />`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,7 +241,7 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
haStyleDialog,
|
||||
css`
|
||||
state-info {
|
||||
margin-top: 24px;
|
||||
line-height: 40px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
width: 40px;
|
||||
|
||||
@@ -63,8 +63,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
@state() private _description? = "";
|
||||
|
||||
@state() private _location? = "";
|
||||
|
||||
@state() private _rrule?: string;
|
||||
|
||||
@state() private _allDay = false;
|
||||
@@ -81,8 +79,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
// timezone, but floating without a timezone.
|
||||
private _timeZone?: string;
|
||||
|
||||
private _hasLocation = false;
|
||||
|
||||
public showDialog(params: CalendarEventEditDialogParams): void {
|
||||
this._error = undefined;
|
||||
this._info = undefined;
|
||||
@@ -103,10 +99,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
this._allDay = isDate(entry.dtstart);
|
||||
this._summary = entry.summary;
|
||||
this._description = entry.description;
|
||||
if (entry.location) {
|
||||
this._hasLocation = true;
|
||||
this._location = entry.location;
|
||||
}
|
||||
this._rrule = entry.rrule;
|
||||
if (this._allDay) {
|
||||
this._dtstart = new Date(entry.dtstart + "T00:00:00");
|
||||
@@ -138,8 +130,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
this._dtend = undefined;
|
||||
this._summary = "";
|
||||
this._description = "";
|
||||
this._location = "";
|
||||
this._hasLocation = false;
|
||||
this._rrule = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -191,15 +181,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
class="location"
|
||||
name="location"
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.calendar.event.location"
|
||||
)}
|
||||
.value=${this._location}
|
||||
@change=${this._handleLocationChanged}
|
||||
></ha-textfield>
|
||||
<ha-textarea
|
||||
class="description"
|
||||
name="description"
|
||||
@@ -345,10 +326,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
this._description = ev.target.value;
|
||||
}
|
||||
|
||||
private _handleLocationChanged(ev: Event) {
|
||||
this._location = (ev.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private _handleRRuleChanged(ev) {
|
||||
this._rrule = ev.detail.value;
|
||||
}
|
||||
@@ -422,7 +399,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
const data: CalendarEventMutableParams = {
|
||||
summary: this._summary,
|
||||
description: this._description,
|
||||
location: this._location || (this._hasLocation ? "" : undefined),
|
||||
rrule: this._rrule || undefined,
|
||||
dtstart: "",
|
||||
dtend: "",
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy";
|
||||
import {
|
||||
findEntities,
|
||||
generateEntityFilter,
|
||||
type EntityFilter,
|
||||
} from "../../../common/entity/entity_filter";
|
||||
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import {
|
||||
computeAreaTileCardConfig,
|
||||
getAreas,
|
||||
getFloors,
|
||||
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
|
||||
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
|
||||
|
||||
export interface ClimateViewStrategyConfig {
|
||||
type: "climate";
|
||||
@@ -135,9 +139,9 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
_config: ClimateViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
const floors = Object.values(hass.floors);
|
||||
const hierarchy = getAreasFloorHierarchy(floors, areas);
|
||||
const areas = getAreas(hass.areas);
|
||||
const floors = getFloors(hass.floors);
|
||||
const home = getHomeStructure(floors, areas);
|
||||
|
||||
const sections: LovelaceSectionRawConfig[] = [];
|
||||
|
||||
@@ -149,11 +153,10 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
|
||||
const entities = findEntities(allEntities, climateFilters);
|
||||
|
||||
const floorCount =
|
||||
hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0);
|
||||
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
||||
|
||||
// Process floors
|
||||
for (const floorStructure of hierarchy.floors) {
|
||||
for (const floorStructure of home.floors) {
|
||||
const floorId = floorStructure.id;
|
||||
const areaIds = floorStructure.areas;
|
||||
const floor = hass.floors[floorId];
|
||||
@@ -182,7 +185,7 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
// Process unassigned areas
|
||||
if (hierarchy.areas.length > 0) {
|
||||
if (home.areas.length > 0) {
|
||||
const section: LovelaceSectionRawConfig = {
|
||||
type: "grid",
|
||||
column_span: 2,
|
||||
@@ -197,7 +200,7 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForClimate(hierarchy.areas, hass, entities);
|
||||
const areaCards = processAreasForClimate(home.areas, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
|
||||
@@ -2,47 +2,38 @@ import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiDragHorizontalVariant,
|
||||
mdiHelpCircle,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
getAreasFloorHierarchy,
|
||||
getAreasOrder,
|
||||
getFloorOrder,
|
||||
type AreasFloorHierarchy,
|
||||
} from "../../../common/areas/areas-floor-hierarchy";
|
||||
import { formatListWithAnds } from "../../../common/string/format-list";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-sortable";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
createAreaRegistryEntry,
|
||||
reorderAreaRegistryEntries,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import {
|
||||
createFloorRegistryEntry,
|
||||
deleteFloorRegistryEntry,
|
||||
reorderFloorRegistryEntries,
|
||||
getFloorAreaLookup,
|
||||
updateFloorRegistryEntry,
|
||||
} from "../../../data/floor_registry";
|
||||
import {
|
||||
@@ -51,7 +42,6 @@ import {
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import {
|
||||
@@ -62,17 +52,7 @@ import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-deta
|
||||
|
||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
||||
|
||||
const SORT_OPTIONS: HaSortableOptions = {
|
||||
sort: true,
|
||||
delay: 500,
|
||||
delayOnTouchOnly: true,
|
||||
};
|
||||
|
||||
interface AreaStats {
|
||||
devices: number;
|
||||
services: number;
|
||||
entities: number;
|
||||
}
|
||||
const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
|
||||
|
||||
@customElement("ha-config-areas-dashboard")
|
||||
export class HaConfigAreasDashboard extends LitElement {
|
||||
@@ -84,50 +64,55 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _hierarchy?: AreasFloorHierarchy;
|
||||
@state() private _areas: AreaRegistryEntry[] = [];
|
||||
|
||||
private _blockHierarchyUpdate = false;
|
||||
|
||||
private _blockHierarchyUpdateTimeout?: number;
|
||||
|
||||
private _processAreasStats = memoizeOne(
|
||||
private _processAreas = memoizeOne(
|
||||
(
|
||||
areas: HomeAssistant["areas"],
|
||||
areas: AreaRegistryEntry[],
|
||||
devices: HomeAssistant["devices"],
|
||||
entities: HomeAssistant["entities"]
|
||||
): Map<string, AreaStats> => {
|
||||
const computeAreaStats = (area: AreaRegistryEntry) => {
|
||||
let devicesCount = 0;
|
||||
let servicesCount = 0;
|
||||
let entitiesCount = 0;
|
||||
entities: HomeAssistant["entities"],
|
||||
floors: HomeAssistant["floors"]
|
||||
) => {
|
||||
const processArea = (area: AreaRegistryEntry) => {
|
||||
let noDevicesInArea = 0;
|
||||
let noServicesInArea = 0;
|
||||
let noEntitiesInArea = 0;
|
||||
|
||||
for (const device of Object.values(devices)) {
|
||||
if (device.area_id === area.area_id) {
|
||||
if (device.entry_type === "service") {
|
||||
servicesCount++;
|
||||
noServicesInArea++;
|
||||
} else {
|
||||
devicesCount++;
|
||||
noDevicesInArea++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const entity of Object.values(entities)) {
|
||||
if (entity.area_id === area.area_id) {
|
||||
entitiesCount++;
|
||||
noEntitiesInArea++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
devices: devicesCount,
|
||||
services: servicesCount,
|
||||
entities: entitiesCount,
|
||||
...area,
|
||||
devices: noDevicesInArea,
|
||||
services: noServicesInArea,
|
||||
entities: noEntitiesInArea,
|
||||
};
|
||||
};
|
||||
const areaStats = new Map<string, AreaStats>();
|
||||
Object.values(areas).forEach((area) => {
|
||||
areaStats.set(area.area_id, computeAreaStats(area));
|
||||
});
|
||||
return areaStats;
|
||||
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
const unassignedAreas = areas.filter(
|
||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||
);
|
||||
return {
|
||||
floors: Object.values(floors).map((floor) => ({
|
||||
...floor,
|
||||
areas: (floorAreaLookup[floor.floor_id] || []).map(processArea),
|
||||
})),
|
||||
unassignedAreas: unassignedAreas.map(processArea),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -135,32 +120,25 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("hass")) {
|
||||
const oldHass = changedProperties.get("hass");
|
||||
if (
|
||||
(this.hass.areas !== oldHass?.areas ||
|
||||
this.hass.floors !== oldHass?.floors) &&
|
||||
!this._blockHierarchyUpdate
|
||||
) {
|
||||
this._computeHierarchy();
|
||||
if (this.hass.areas !== oldHass?.areas) {
|
||||
this._areas = Object.values(this.hass.areas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _computeHierarchy() {
|
||||
this._hierarchy = getAreasFloorHierarchy(
|
||||
Object.values(this.hass.floors),
|
||||
Object.values(this.hass.areas)
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult<1> | typeof nothing {
|
||||
if (!this._hierarchy) {
|
||||
return nothing;
|
||||
}
|
||||
const areasStats = this._processAreasStats(
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities
|
||||
);
|
||||
protected render(): TemplateResult {
|
||||
const areasAndFloors =
|
||||
!this.hass.areas ||
|
||||
!this.hass.devices ||
|
||||
!this.hass.entities ||
|
||||
!this.hass.floors
|
||||
? undefined
|
||||
: this._processAreas(
|
||||
this._areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@@ -179,120 +157,81 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
@click=${this._showHelp}
|
||||
></ha-icon-button>
|
||||
<div class="container">
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
draggable-selector=".floor"
|
||||
@item-moved=${this._floorMoved}
|
||||
.options=${SORT_OPTIONS}
|
||||
group="floors"
|
||||
invert-swap
|
||||
>
|
||||
<div class="floors">
|
||||
${this._hierarchy.floors.map(({ areas, id }) => {
|
||||
const floor = this.hass.floors[id];
|
||||
if (!floor) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
<ha-floor-icon .floor=${floor}></ha-floor-icon>
|
||||
${floor.name}
|
||||
</h2>
|
||||
<div class="actions">
|
||||
<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<ha-button-menu
|
||||
.floor=${floor}
|
||||
@action=${this._handleFloorAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
.path=${mdiPencil}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.edit_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item class="warning" graphic="icon"
|
||||
><ha-svg-icon
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.delete_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</div>
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-added=${this._areaAdded}
|
||||
@item-moved=${this._areaMoved}
|
||||
group="areas"
|
||||
.options=${SORT_OPTIONS}
|
||||
.floor=${floor.floor_id}
|
||||
>
|
||||
<div class="areas">
|
||||
${areas.map((areaId) => {
|
||||
const area = this.hass.areas[areaId];
|
||||
if (!area) {
|
||||
return nothing;
|
||||
}
|
||||
const stats = areasStats.get(area.area_id);
|
||||
return this._renderArea(area, stats);
|
||||
})}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
|
||||
${this._hierarchy.areas.length
|
||||
? html`
|
||||
<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.unassigned_areas"
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-added=${this._areaAdded}
|
||||
@item-moved=${this._areaMoved}
|
||||
group="areas"
|
||||
.options=${SORT_OPTIONS}
|
||||
.floor=${UNASSIGNED_FLOOR}
|
||||
${areasAndFloors?.floors.map(
|
||||
(floor) =>
|
||||
html`<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
<ha-floor-icon .floor=${floor}></ha-floor-icon>
|
||||
${floor.name}
|
||||
</h2>
|
||||
<ha-button-menu
|
||||
.floor=${floor}
|
||||
@action=${this._handleFloorAction}
|
||||
>
|
||||
<div class="areas">
|
||||
${this._hierarchy.areas.map((areaId) => {
|
||||
const area = this.hass.areas[areaId];
|
||||
if (!area) {
|
||||
return nothing;
|
||||
}
|
||||
const stats = areasStats.get(area.area_id);
|
||||
return this._renderArea(area, stats);
|
||||
})}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
.path=${mdiPencil}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.edit_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item class="warning" graphic="icon"
|
||||
><ha-svg-icon
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.delete_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
`
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-added=${this._areaAdded}
|
||||
group="floor"
|
||||
.options=${SORT_OPTIONS}
|
||||
.floor=${floor.floor_id}
|
||||
>
|
||||
<div class="areas">
|
||||
${floor.areas.map((area) => this._renderArea(area))}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
</div>`
|
||||
)}
|
||||
${areasAndFloors?.unassignedAreas.length
|
||||
? html`<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.unassigned_areas"
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-added=${this._areaAdded}
|
||||
group="floor"
|
||||
.options=${SORT_OPTIONS}
|
||||
.floor=${UNASSIGNED_FLOOR}
|
||||
>
|
||||
<div class="areas">
|
||||
${areasAndFloors?.unassignedAreas.map((area) =>
|
||||
this._renderArea(area)
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-fab
|
||||
@@ -320,60 +259,56 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderArea(
|
||||
area: AreaRegistryEntry,
|
||||
stats: AreaStats | undefined
|
||||
): TemplateResult<1> {
|
||||
return html`
|
||||
<a href=${`/config/areas/area/${area.area_id}`} .sortableData=${area}>
|
||||
<ha-card outlined>
|
||||
<div
|
||||
style=${styleMap({
|
||||
backgroundImage: area.picture
|
||||
? `url(${area.picture})`
|
||||
: undefined,
|
||||
})}
|
||||
class="picture ${!area.picture ? "placeholder" : ""}"
|
||||
>
|
||||
${!area.picture && area.icon
|
||||
? html`<ha-icon .icon=${area.icon}></ha-icon>`
|
||||
: ""}
|
||||
private _renderArea(area) {
|
||||
return html`<a
|
||||
href=${`/config/areas/area/${area.area_id}`}
|
||||
.sortableData=${area}
|
||||
>
|
||||
<ha-card outlined>
|
||||
<div
|
||||
style=${styleMap({
|
||||
backgroundImage: area.picture ? `url(${area.picture})` : undefined,
|
||||
})}
|
||||
class="picture ${!area.picture ? "placeholder" : ""}"
|
||||
>
|
||||
${!area.picture && area.icon
|
||||
? html`<ha-icon .icon=${area.icon}></ha-icon>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-header">
|
||||
${area.name}
|
||||
<ha-icon-button
|
||||
.area=${area}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._openAreaDetails}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
${formatListWithAnds(
|
||||
this.hass.locale,
|
||||
[
|
||||
area.devices &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices",
|
||||
{ count: area.devices }
|
||||
),
|
||||
area.services &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.services",
|
||||
{ count: area.services }
|
||||
),
|
||||
area.entities &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: area.entities }
|
||||
),
|
||||
].filter((v): v is string => Boolean(v))
|
||||
)}
|
||||
</div>
|
||||
<div class="card-header">
|
||||
${area.name}
|
||||
<ha-icon-button
|
||||
.area=${area}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._openAreaDetails}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
${formatListWithAnds(
|
||||
this.hass.locale,
|
||||
[
|
||||
stats?.devices &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices",
|
||||
{ count: stats.devices }
|
||||
),
|
||||
stats?.services &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.services",
|
||||
{ count: stats.services }
|
||||
),
|
||||
stats?.entities &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: stats.entities }
|
||||
),
|
||||
].filter((v): v is string => Boolean(v))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</a>
|
||||
`;
|
||||
</div>
|
||||
</ha-card>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
@@ -391,170 +326,24 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _floorMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.hass || !this._hierarchy) {
|
||||
return;
|
||||
}
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
const reorderFloors = (
|
||||
floors: AreasFloorHierarchy["floors"],
|
||||
oldIdx: number,
|
||||
newIdx: number
|
||||
) => {
|
||||
const newFloors = [...floors];
|
||||
const [movedFloor] = newFloors.splice(oldIdx, 1);
|
||||
newFloors.splice(newIdx, 0, movedFloor);
|
||||
return newFloors;
|
||||
};
|
||||
|
||||
// Optimistically update UI
|
||||
this._hierarchy = {
|
||||
...this._hierarchy,
|
||||
floors: reorderFloors(this._hierarchy.floors, oldIndex, newIndex),
|
||||
};
|
||||
|
||||
const areaOrder = getAreasOrder(this._hierarchy);
|
||||
const floorOrder = getFloorOrder(this._hierarchy);
|
||||
|
||||
// Block hierarchy updates for 500ms to avoid flickering
|
||||
// because of multiple async updates
|
||||
this._blockHierarchyUpdateFor(500);
|
||||
|
||||
try {
|
||||
await reorderAreaRegistryEntries(this.hass, areaOrder);
|
||||
await reorderFloorRegistryEntries(this.hass, floorOrder);
|
||||
} catch {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor_reorder_failed"
|
||||
),
|
||||
});
|
||||
// Revert on error
|
||||
this._computeHierarchy();
|
||||
}
|
||||
}
|
||||
|
||||
private async _areaMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.hass || !this._hierarchy) {
|
||||
return;
|
||||
}
|
||||
const { floor } = ev.currentTarget;
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
const floorId = floor === UNASSIGNED_FLOOR ? null : floor;
|
||||
|
||||
// Reorder areas within the same floor
|
||||
const reorderAreas = (areas: string[], oldIdx: number, newIdx: number) => {
|
||||
const newAreas = [...areas];
|
||||
const [movedArea] = newAreas.splice(oldIdx, 1);
|
||||
newAreas.splice(newIdx, 0, movedArea);
|
||||
return newAreas;
|
||||
};
|
||||
|
||||
// Optimistically update UI
|
||||
this._hierarchy = {
|
||||
...this._hierarchy,
|
||||
floors: this._hierarchy.floors.map((f) => {
|
||||
if (f.id === floorId) {
|
||||
return {
|
||||
...f,
|
||||
areas: reorderAreas(f.areas, oldIndex, newIndex),
|
||||
};
|
||||
}
|
||||
return f;
|
||||
}),
|
||||
areas:
|
||||
floorId === null
|
||||
? reorderAreas(this._hierarchy.areas, oldIndex, newIndex)
|
||||
: this._hierarchy.areas,
|
||||
};
|
||||
|
||||
const areaOrder = getAreasOrder(this._hierarchy);
|
||||
|
||||
try {
|
||||
await reorderAreaRegistryEntries(this.hass, areaOrder);
|
||||
} catch {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.areas.picker.area_move_failed"
|
||||
),
|
||||
});
|
||||
// Revert on error
|
||||
this._computeHierarchy();
|
||||
}
|
||||
}
|
||||
|
||||
private async _areaAdded(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.hass || !this._hierarchy) {
|
||||
return;
|
||||
}
|
||||
const { floor } = ev.currentTarget;
|
||||
const { data: area, index } = ev.detail;
|
||||
|
||||
const newFloorId = floor === UNASSIGNED_FLOOR ? null : floor;
|
||||
|
||||
// Insert area at the specified index
|
||||
const insertAtIndex = (areas: string[], areaId: string, idx: number) => {
|
||||
const newAreas = [...areas];
|
||||
newAreas.splice(idx, 0, areaId);
|
||||
return newAreas;
|
||||
};
|
||||
const { data: area } = ev.detail;
|
||||
|
||||
// Optimistically update UI
|
||||
this._hierarchy = {
|
||||
...this._hierarchy,
|
||||
floors: this._hierarchy.floors.map((f) => {
|
||||
if (f.id === newFloorId) {
|
||||
return {
|
||||
...f,
|
||||
areas: insertAtIndex(f.areas, area.area_id, index),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...f,
|
||||
areas: f.areas.filter((id) => id !== area.area_id),
|
||||
};
|
||||
}),
|
||||
areas:
|
||||
newFloorId === null
|
||||
? insertAtIndex(this._hierarchy.areas, area.area_id, index)
|
||||
: this._hierarchy.areas.filter((id) => id !== area.area_id),
|
||||
};
|
||||
this._areas = this._areas.map<AreaRegistryEntry>((a) => {
|
||||
if (a.area_id === area.area_id) {
|
||||
return { ...a, floor_id: newFloorId };
|
||||
}
|
||||
return a;
|
||||
});
|
||||
|
||||
const areaOrder = getAreasOrder(this._hierarchy);
|
||||
|
||||
// Block hierarchy updates for 500ms to avoid flickering
|
||||
// because of multiple async updates
|
||||
this._blockHierarchyUpdateFor(500);
|
||||
|
||||
try {
|
||||
await reorderAreaRegistryEntries(this.hass, areaOrder);
|
||||
await updateAreaRegistryEntry(this.hass, area.area_id, {
|
||||
floor_id: newFloorId,
|
||||
});
|
||||
} catch {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.areas.picker.area_move_failed"
|
||||
),
|
||||
});
|
||||
// Revert on error
|
||||
this._computeHierarchy();
|
||||
}
|
||||
}
|
||||
|
||||
private _blockHierarchyUpdateFor(time: number) {
|
||||
this._blockHierarchyUpdate = true;
|
||||
if (this._blockHierarchyUpdateTimeout) {
|
||||
window.clearTimeout(this._blockHierarchyUpdateTimeout);
|
||||
}
|
||||
this._blockHierarchyUpdateTimeout = window.setTimeout(() => {
|
||||
this._blockHierarchyUpdate = false;
|
||||
}, time);
|
||||
await updateAreaRegistryEntry(this.hass, area.area_id, {
|
||||
floor_id: newFloorId,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleFloorAction(ev: CustomEvent<ActionDetail>) {
|
||||
@@ -674,10 +463,6 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
.header ha-icon {
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.header .actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.areas {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
@@ -688,10 +473,6 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
.areas > * {
|
||||
max-width: 500px;
|
||||
}
|
||||
.handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1161,9 +1161,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
private async _delete(automation) {
|
||||
try {
|
||||
await deleteAutomation(this.hass, automation.attributes.id);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== automation.entity_id
|
||||
);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
|
||||
@@ -23,8 +23,6 @@ import {
|
||||
fetchHassioHassOsInfo,
|
||||
fetchHassioHostInfo,
|
||||
} from "../../../data/hassio/host";
|
||||
import type { LabPreviewFeature } from "../../../data/labs";
|
||||
import { fetchLabFeatures } from "../../../data/labs";
|
||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -52,8 +50,6 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
|
||||
@state() private _externalAccess = false;
|
||||
|
||||
@state() private _labFeatures?: LabPreviewFeature[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const pages = configSections.general
|
||||
.filter((page) => canShowPage(this.hass, page))
|
||||
@@ -98,12 +94,6 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
this._boardName ||
|
||||
this.hass.localize("ui.panel.config.hardware.description");
|
||||
break;
|
||||
case "labs":
|
||||
description =
|
||||
this._labFeatures && this._labFeatures.some((f) => f.enabled)
|
||||
? this.hass.localize("ui.panel.config.labs.description_enabled")
|
||||
: this.hass.localize("ui.panel.config.labs.description");
|
||||
break;
|
||||
|
||||
default:
|
||||
description = this.hass.localize(
|
||||
@@ -166,7 +156,6 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
|
||||
this._fetchBackupInfo();
|
||||
this._fetchHardwareInfo(isHassioLoaded);
|
||||
this._fetchLabFeatures();
|
||||
if (isHassioLoaded) {
|
||||
this._fetchStorageInfo();
|
||||
}
|
||||
@@ -222,12 +211,6 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
this._externalAccess = this.hass.config.external_url !== null;
|
||||
}
|
||||
|
||||
private async _fetchLabFeatures() {
|
||||
if (isComponentLoaded(this.hass, "labs")) {
|
||||
this._labFeatures = await fetchLabFeatures(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
private async _showRestartDialog() {
|
||||
showRestartDialog(this);
|
||||
}
|
||||
|
||||
@@ -281,12 +281,8 @@ class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
@media all and (min-width: 850px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 845px;
|
||||
--mdc-dialog-min-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--mdc-dialog-min-height: calc(100vh - 72px);
|
||||
--mdc-dialog-max-height: calc(100vh - 72px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
mdiCog,
|
||||
mdiDatabase,
|
||||
mdiDevices,
|
||||
mdiFlask,
|
||||
mdiInformation,
|
||||
mdiInformationOutline,
|
||||
mdiLabel,
|
||||
@@ -329,13 +328,6 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
iconPath: mdiShape,
|
||||
iconColor: "#f1c447",
|
||||
},
|
||||
{
|
||||
path: "/config/labs",
|
||||
translationKey: "labs",
|
||||
iconPath: mdiFlask,
|
||||
iconColor: "#b1b134",
|
||||
core: true,
|
||||
},
|
||||
{
|
||||
path: "/config/network",
|
||||
translationKey: "network",
|
||||
@@ -523,10 +515,6 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
|
||||
tag: "ha-config-section-general",
|
||||
load: () => import("./core/ha-config-section-general"),
|
||||
},
|
||||
labs: {
|
||||
tag: "ha-config-labs",
|
||||
load: () => import("./labs/ha-config-labs"),
|
||||
},
|
||||
zha: {
|
||||
tag: "zha-config-dashboard-router",
|
||||
load: () =>
|
||||
|
||||
@@ -462,7 +462,7 @@ class AddIntegrationDialog extends LitElement {
|
||||
style=${styleMap({
|
||||
width: `${this._width}px`,
|
||||
height: this._narrow
|
||||
? "calc(100vh - 184px - var(--safe-area-inset-top, var(--ha-space-0)) - var(--safe-area-inset-bottom, var(--ha-space-0)))"
|
||||
? "calc(100vh - 184px - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))"
|
||||
: "500px",
|
||||
})}
|
||||
@click=${this._integrationPicked}
|
||||
|
||||
@@ -87,7 +87,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
${!this.narrow
|
||||
? html`<ha-icon-button
|
||||
slot="end"
|
||||
@click=${this._handleEditDeviceButton}
|
||||
@click=${this._handleEditDevice}
|
||||
.path=${mdiPencil}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.edit"
|
||||
@@ -106,7 +106,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${this.narrow
|
||||
? html`<ha-md-menu-item .clickAction=${this._handleEditDevice}>
|
||||
? html`<ha-md-menu-item @click=${this._handleEditDevice}>
|
||||
<ha-svg-icon .path=${mdiPencil} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.edit"
|
||||
@@ -115,7 +115,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
: nothing}
|
||||
${entities.length
|
||||
? html`
|
||||
<ha-md-menu-item .clickAction=${this._handleNavigateToEntities}>
|
||||
<ha-md-menu-item @click=${this._handleNavigateToEntities}>
|
||||
<ha-svg-icon
|
||||
.path=${mdiShapeOutline}
|
||||
slot="start"
|
||||
@@ -130,7 +130,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
: nothing}
|
||||
<ha-md-menu-item
|
||||
class=${device.disabled_by !== "user" ? "warning" : ""}
|
||||
.clickAction=${this._handleDisableDevice}
|
||||
@click=${this._handleDisableDevice}
|
||||
.disabled=${device.disabled_by !== "user" && device.disabled_by}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiStopCircleOutline} slot="start"></ha-svg-icon>
|
||||
@@ -160,7 +160,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
${this.entry.supports_remove_device
|
||||
? html`<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._handleDeleteDevice}
|
||||
@click=${this._handleDeleteDevice}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
@@ -175,25 +175,21 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
private _getEntities = (): EntityRegistryEntry[] =>
|
||||
this.entities?.filter((entity) => entity.device_id === this.device.id);
|
||||
|
||||
private _handleEditDeviceButton(ev: MouseEvent) {
|
||||
private _handleEditDevice(ev: MouseEvent) {
|
||||
ev.stopPropagation(); // Prevent triggering the click handler on the list item
|
||||
this._handleEditDevice();
|
||||
}
|
||||
|
||||
private _handleEditDevice = () => {
|
||||
showDeviceRegistryDetailDialog(this, {
|
||||
device: this.device,
|
||||
updateEntry: async (updates) => {
|
||||
await updateDeviceRegistryEntry(this.hass, this.device.id, updates);
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleNavigateToEntities = () => {
|
||||
private _handleNavigateToEntities() {
|
||||
navigate(`/config/entities/?historyBack=1&device=${this.device.id}`);
|
||||
};
|
||||
}
|
||||
|
||||
private _handleDisableDevice = async () => {
|
||||
private async _handleDisableDevice() {
|
||||
const disable = this.device.disabled_by === null;
|
||||
|
||||
if (disable) {
|
||||
@@ -267,9 +263,9 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
await updateDeviceRegistryEntry(this.hass, this.device.id, {
|
||||
disabled_by: disable ? "user" : null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleDeleteDevice = async () => {
|
||||
private async _handleDeleteDevice() {
|
||||
const entry = this.entry;
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
|
||||
@@ -294,7 +290,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _handleNavigateToDevice() {
|
||||
navigate(`/config/devices/device/${this.device.id}`);
|
||||
|
||||
@@ -302,7 +302,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
item.supports_unload &&
|
||||
item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item .clickAction=${this._handleReload}>
|
||||
<ha-md-menu-item @click=${this._handleReload}>
|
||||
<ha-svg-icon slot="start" .path=${mdiReload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reload"
|
||||
@@ -311,14 +311,14 @@ class HaConfigEntryRow extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._handleRename} graphic="icon">
|
||||
<ha-md-menu-item @click=${this._handleRename} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._handleCopy} graphic="icon">
|
||||
<ha-md-menu-item @click=${this._handleCopy} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.copy"
|
||||
@@ -328,7 +328,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
${Object.keys(item.supported_subentry_types).map(
|
||||
(flowType) =>
|
||||
html`<ha-md-menu-item
|
||||
.clickAction=${this._addSubEntry}
|
||||
@click=${this._addSubEntry}
|
||||
.entry=${item}
|
||||
.flowType=${flowType}
|
||||
graphic="icon"
|
||||
@@ -360,7 +360,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
item.supports_reconfigure &&
|
||||
item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item .clickAction=${this._handleReconfigure}>
|
||||
<ha-md-menu-item @click=${this._handleReconfigure}>
|
||||
<ha-svg-icon slot="start" .path=${mdiWrench}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reconfigure"
|
||||
@@ -369,10 +369,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._handleSystemOptions}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-md-menu-item @click=${this._handleSystemOptions} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiCogOutline}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.system_options"
|
||||
@@ -380,7 +377,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
</ha-md-menu-item>
|
||||
${item.disabled_by === "user"
|
||||
? html`
|
||||
<ha-md-menu-item .clickAction=${this._handleEnable}>
|
||||
<ha-md-menu-item @click=${this._handleEnable}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
@@ -392,7 +389,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._handleDisable}
|
||||
@click=${this._handleDisable}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -406,10 +403,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
: nothing}
|
||||
${item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._handleDelete}
|
||||
>
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDelete}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
@@ -617,7 +611,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleReload = async () => {
|
||||
private async _handleReload() {
|
||||
const result = await reloadConfigEntry(this.hass, this.entry.entry_id);
|
||||
const locale_key = result.require_restart
|
||||
? "reload_restart_confirm"
|
||||
@@ -627,9 +621,9 @@ class HaConfigEntryRow extends LitElement {
|
||||
`ui.panel.config.integrations.config_entry.${locale_key}`
|
||||
),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleReconfigure = async () => {
|
||||
private async _handleReconfigure() {
|
||||
showConfigFlowDialog(this, {
|
||||
startFlowHandler: this.entry.domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
@@ -637,18 +631,18 @@ class HaConfigEntryRow extends LitElement {
|
||||
entryId: this.entry.entry_id,
|
||||
navigateToResult: true,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleCopy = async () => {
|
||||
private async _handleCopy() {
|
||||
await copyToClipboard(this.entry.entry_id);
|
||||
showToast(this, {
|
||||
message:
|
||||
this.hass?.localize("ui.common.copied_clipboard") ||
|
||||
"Copied to clipboard",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleRename = async () => {
|
||||
private async _handleRename() {
|
||||
const newName = await showPromptDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.integrations.rename_dialog"),
|
||||
defaultValue: this.entry.title,
|
||||
@@ -662,7 +656,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
await updateConfigEntry(this.hass, this.entry.entry_id, {
|
||||
title: newName,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private async _signUrl(ev) {
|
||||
const anchor = ev.currentTarget;
|
||||
@@ -674,7 +668,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
fileDownload(signedUrl.path);
|
||||
}
|
||||
|
||||
private _handleDisable = async () => {
|
||||
private async _handleDisable() {
|
||||
const entryId = this.entry.entry_id;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
@@ -712,9 +706,9 @@ class HaConfigEntryRow extends LitElement {
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _handleEnable = async () => {
|
||||
private async _handleEnable() {
|
||||
const entryId = this.entry.entry_id;
|
||||
|
||||
let result: DisableConfigEntryResult;
|
||||
@@ -737,9 +731,9 @@ class HaConfigEntryRow extends LitElement {
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _handleDelete = async () => {
|
||||
private async _handleDelete() {
|
||||
const entryId = this.entry.entry_id;
|
||||
|
||||
const applicationCredentialsId =
|
||||
@@ -773,20 +767,20 @@ class HaConfigEntryRow extends LitElement {
|
||||
if (applicationCredentialsId) {
|
||||
this._removeApplicationCredential(applicationCredentialsId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _handleSystemOptions = () => {
|
||||
private _handleSystemOptions() {
|
||||
showConfigEntrySystemOptionsDialog(this, {
|
||||
entry: this.entry,
|
||||
manifest: this.manifest,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _addSubEntry = (item) => {
|
||||
showSubConfigFlowDialog(this, this.entry, item.flowType, {
|
||||
private _addSubEntry(ev) {
|
||||
showSubConfigFlowDialog(this, this.entry, ev.target.flowType, {
|
||||
startFlowHandler: this.entry.entry_id,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
|
||||
@@ -145,16 +145,13 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-menu-item .clickAction=${this._handleRenameSub}>
|
||||
<ha-md-menu-item @click=${this._handleRenameSub}>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._handleDeleteSub}
|
||||
>
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDeleteSub}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
@@ -225,7 +222,7 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRenameSub = async (): Promise<void> => {
|
||||
private async _handleRenameSub(): Promise<void> {
|
||||
const newName = await showPromptDialog(this, {
|
||||
title: this.hass.localize("ui.common.rename"),
|
||||
defaultValue: this.subEntry.title,
|
||||
@@ -242,9 +239,9 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
this.subEntry.subentry_id,
|
||||
{ title: newName }
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private _handleDeleteSub = async (): Promise<void> => {
|
||||
private async _handleDeleteSub(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_title",
|
||||
@@ -266,7 +263,7 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
this.entry.entry_id,
|
||||
this.subEntry.subentry_id
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.expand-button {
|
||||
|
||||
@@ -12,10 +12,7 @@ import "../../../../../components/ha-tab-group";
|
||||
import "../../../../../components/ha-tab-group-tab";
|
||||
import type { ZHADevice, ZHAGroup } from "../../../../../data/zha";
|
||||
import { fetchBindableDevices, fetchGroups } from "../../../../../data/zha";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../../resources/styles";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { sortZHADevices, sortZHAGroups } from "./functions";
|
||||
import type {
|
||||
@@ -214,11 +211,11 @@ class DialogZHAManageZigbeeDevice extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-surface-position: static;
|
||||
--dialog-content-position: static;
|
||||
--vertical-align-dialog: flex-start;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -232,9 +229,8 @@ class DialogZHAManageZigbeeDevice extends LitElement {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 560px;
|
||||
--mdc-dialog-max-width: 560px;
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--dialog-surface-margin-top: 40px;
|
||||
--mdc-dialog-max-height: calc(100% - 72px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-button";
|
||||
import type { HaMdDialog } from "../../../components/ha-md-dialog";
|
||||
import "../../../components/ha-md-dialog";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import "../../../components/ha-switch";
|
||||
import type { BackupConfig } from "../../../data/backup";
|
||||
import { fetchBackupConfig } from "../../../data/backup";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LabsPreviewFeatureEnableDialogParams } from "./show-dialog-labs-preview-feature-enable";
|
||||
|
||||
@customElement("dialog-labs-preview-feature-enable")
|
||||
export class DialogLabsPreviewFeatureEnable
|
||||
extends LitElement
|
||||
implements HassDialog<LabsPreviewFeatureEnableDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LabsPreviewFeatureEnableDialogParams;
|
||||
|
||||
@state() private _backupConfig?: BackupConfig;
|
||||
|
||||
@state() private _createBackup = false;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(
|
||||
params: LabsPreviewFeatureEnableDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._createBackup = false;
|
||||
await this._fetchBackupConfig();
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._dialog?.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._backupConfig = undefined;
|
||||
this._createBackup = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private async _fetchBackupConfig() {
|
||||
try {
|
||||
const { config } = await fetchBackupConfig(this.hass);
|
||||
this._backupConfig = config;
|
||||
|
||||
// Default to enabled if automatic backups are configured, disabled otherwise
|
||||
this._createBackup =
|
||||
config.automatic_backups_configured &&
|
||||
!!config.create_backup.password &&
|
||||
config.create_backup.agent_ids.length > 0;
|
||||
} catch {
|
||||
// User will get manual backup option if fetch fails
|
||||
this._createBackup = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _computeCreateBackupTexts():
|
||||
| { title: string; description?: string }
|
||||
| undefined {
|
||||
if (
|
||||
!this._backupConfig ||
|
||||
!this._backupConfig.automatic_backups_configured ||
|
||||
!this._backupConfig.create_backup.password ||
|
||||
this._backupConfig.create_backup.agent_ids.length === 0
|
||||
) {
|
||||
return {
|
||||
title: this.hass.localize("ui.panel.config.labs.create_backup.manual"),
|
||||
description: this.hass.localize(
|
||||
"ui.panel.config.labs.create_backup.manual_description"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const lastAutomaticBackupDate = this._backupConfig
|
||||
.last_completed_automatic_backup
|
||||
? new Date(this._backupConfig.last_completed_automatic_backup)
|
||||
: null;
|
||||
const now = new Date();
|
||||
|
||||
return {
|
||||
title: this.hass.localize("ui.panel.config.labs.create_backup.automatic"),
|
||||
description: lastAutomaticBackupDate
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.labs.create_backup.automatic_description_last",
|
||||
{
|
||||
relative_time: relativeTime(
|
||||
lastAutomaticBackupDate,
|
||||
this.hass.locale,
|
||||
now,
|
||||
true
|
||||
),
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.labs.create_backup.automatic_description_none"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private _createBackupChanged(ev: Event): void {
|
||||
this._createBackup = (ev.target as HaSwitch).checked;
|
||||
}
|
||||
|
||||
private _handleCancel(): void {
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _handleConfirm(): void {
|
||||
if (this._params) {
|
||||
this._params.onConfirm(this._createBackup);
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const createBackupTexts = this._computeCreateBackupTexts();
|
||||
|
||||
return html`
|
||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||
<span slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labs.enable_title")}
|
||||
</span>
|
||||
<div slot="content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
`component.${this._params.preview_feature.domain}.preview_features.${this._params.preview_feature.preview_feature}.enable_confirmation`
|
||||
) || this.hass.localize("ui.panel.config.labs.enable_confirmation")}
|
||||
</p>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
${createBackupTexts
|
||||
? html`
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">${createBackupTexts.title}</span>
|
||||
${createBackupTexts.description
|
||||
? html`
|
||||
<span slot="supporting-text">
|
||||
${createBackupTexts.description}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${this._createBackup}
|
||||
@change=${this._createBackupChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
`
|
||||
: nothing}
|
||||
<div>
|
||||
<ha-button appearance="plain" @click=${this._handleCancel}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._handleConfirm}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labs.enable")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static readonly styles = css`
|
||||
ha-md-dialog {
|
||||
--dialog-content-padding: var(--ha-space-6);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
div[slot="actions"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: var(--ha-space-6);
|
||||
--md-list-item-trailing-space: var(--ha-space-6);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
div[slot="actions"] > div {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-4) var(--ha-space-6);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-labs-preview-feature-enable": DialogLabsPreviewFeatureEnable;
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-md-dialog";
|
||||
import "../../../components/ha-spinner";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LabsProgressDialogParams } from "./show-dialog-labs-progress";
|
||||
|
||||
@customElement("dialog-labs-progress")
|
||||
export class DialogLabsProgress
|
||||
extends LitElement
|
||||
implements HassDialog<LabsProgressDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LabsProgressDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(params: LabsProgressDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
.open=${this._open}
|
||||
hideActions
|
||||
scrimClickAction=""
|
||||
escapeKeyAction=""
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div slot="content">
|
||||
<div class="summary">
|
||||
<ha-spinner></ha-spinner>
|
||||
<div class="content">
|
||||
<p class="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.labs.progress.creating_backup"
|
||||
)}
|
||||
</p>
|
||||
<p class="description">
|
||||
${this.hass.localize(
|
||||
this._params.enabled
|
||||
? "ui.panel.config.labs.progress.backing_up_before_enabling"
|
||||
: "ui.panel.config.labs.progress.backing_up_before_disabling"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static readonly styles = css`
|
||||
ha-md-dialog {
|
||||
--dialog-content-padding: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: var(--ha-space-4);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
ha-spinner {
|
||||
--ha-spinner-size: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.heading {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
color: var(--primary-text-color);
|
||||
margin: 0 0 var(--ha-space-1);
|
||||
}
|
||||
.description {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
letter-spacing: 0.25px;
|
||||
color: var(--secondary-text-color);
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-labs-progress": DialogLabsProgress;
|
||||
}
|
||||
}
|
||||
@@ -1,549 +0,0 @@
|
||||
import { mdiFlask, mdiHelpCircle, mdiOpenInNew } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
labsUpdatePreviewFeature,
|
||||
subscribeLabFeatures,
|
||||
} from "../../../data/labs";
|
||||
import type { LabPreviewFeature } from "../../../data/labs";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { showLabsPreviewFeatureEnableDialog } from "./show-dialog-labs-preview-feature-enable";
|
||||
import {
|
||||
showLabsProgressDialog,
|
||||
closeLabsProgressDialog,
|
||||
} from "./show-dialog-labs-progress";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../layouts/hass-subpage";
|
||||
|
||||
@customElement("ha-config-labs")
|
||||
class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _preview_features: LabPreviewFeature[] = [];
|
||||
|
||||
@state() private _highlightedPreviewFeature?: string;
|
||||
|
||||
private _sortedPreviewFeatures = memoizeOne(
|
||||
(localize: LocalizeFunc, features: LabPreviewFeature[]) =>
|
||||
// Sort by localized integration name alphabetically
|
||||
[...features].sort((a, b) =>
|
||||
domainToName(localize, a.domain).localeCompare(
|
||||
domainToName(localize, b.domain)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeLabFeatures(this.hass.connection, (features) => {
|
||||
// Load title translations for integrations with preview features
|
||||
const domains = [...new Set(features.map((f) => f.domain))];
|
||||
this.hass.loadBackendTranslation("title", domains);
|
||||
|
||||
this._preview_features = features;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
// Load preview_features translations
|
||||
this.hass.loadBackendTranslation("preview_features");
|
||||
this._handleUrlParams();
|
||||
}
|
||||
|
||||
private _handleUrlParams(): void {
|
||||
// Check for feature parameters in URL
|
||||
const domain = extractSearchParam("domain");
|
||||
const previewFeature = extractSearchParam("preview_feature");
|
||||
if (domain && previewFeature) {
|
||||
const previewFeatureId = `${domain}.${previewFeature}`;
|
||||
this._highlightedPreviewFeature = previewFeatureId;
|
||||
// Wait for next render to ensure cards are in DOM
|
||||
this.updateComplete.then(() => {
|
||||
this._scrollToPreviewFeature(previewFeatureId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const sortedFeatures = this._sortedPreviewFeatures(
|
||||
this.hass.localize,
|
||||
this._preview_features
|
||||
);
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.header=${this.hass.localize("ui.panel.config.labs.caption")}
|
||||
>
|
||||
${sortedFeatures.length
|
||||
? html`
|
||||
<a
|
||||
slot="toolbar-icon"
|
||||
href="https://www.home-assistant.io/integrations/labs/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
.title=${this.hass.localize("ui.common.help")}
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.help")}
|
||||
.path=${mdiHelpCircle}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
`
|
||||
: nothing}
|
||||
<div class="content">
|
||||
${!sortedFeatures.length
|
||||
? html`
|
||||
<div class="empty">
|
||||
<ha-svg-icon .path=${mdiFlask}></ha-svg-icon>
|
||||
<h1>
|
||||
${this.hass.localize("ui.panel.config.labs.empty.title")}
|
||||
</h1>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.labs.empty.description"
|
||||
)}
|
||||
<a
|
||||
href="https://www.home-assistant.io/integrations/labs/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labs.learn_more")}
|
||||
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</a>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content intro-card">
|
||||
<h1>
|
||||
${this.hass.localize("ui.panel.config.labs.intro_title")}
|
||||
</h1>
|
||||
<p class="intro-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.labs.intro_description"
|
||||
)}
|
||||
</p>
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.labs.intro_warning"
|
||||
)}
|
||||
</ha-alert>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
${sortedFeatures.map((preview_feature) =>
|
||||
this._renderPreviewFeature(preview_feature)
|
||||
)}
|
||||
`}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPreviewFeature(
|
||||
preview_feature: LabPreviewFeature
|
||||
): TemplateResult {
|
||||
const featureName = this.hass.localize(
|
||||
`component.${preview_feature.domain}.preview_features.${preview_feature.preview_feature}.name`
|
||||
);
|
||||
|
||||
const description = this.hass.localize(
|
||||
`component.${preview_feature.domain}.preview_features.${preview_feature.preview_feature}.description`
|
||||
);
|
||||
|
||||
const integrationName = domainToName(
|
||||
this.hass.localize,
|
||||
preview_feature.domain
|
||||
);
|
||||
|
||||
const integrationNameWithCustomLabel = !preview_feature.is_built_in
|
||||
? `${integrationName} • ${this.hass.localize("ui.panel.config.labs.custom_integration")}`
|
||||
: integrationName;
|
||||
|
||||
const previewFeatureId = `${preview_feature.domain}.${preview_feature.preview_feature}`;
|
||||
const isHighlighted = this._highlightedPreviewFeature === previewFeatureId;
|
||||
|
||||
// Build description with learn more link if available
|
||||
const descriptionWithLink = preview_feature.learn_more_url
|
||||
? `${description}\n\n[${this.hass.localize("ui.panel.config.labs.learn_more")}](${preview_feature.learn_more_url})`
|
||||
: description;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
data-feature-id=${previewFeatureId}
|
||||
class=${isHighlighted ? "highlighted" : ""}
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="card-header">
|
||||
<img
|
||||
alt=""
|
||||
src=${brandsUrl({
|
||||
domain: preview_feature.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<div class="feature-title">
|
||||
<span class="integration-name"
|
||||
>${integrationNameWithCustomLabel}</span
|
||||
>
|
||||
<h2>${featureName}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<ha-markdown .content=${descriptionWithLink} breaks></ha-markdown>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div>
|
||||
${preview_feature.feedback_url
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
href=${preview_feature.feedback_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.labs.provide_feedback"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
${preview_feature.report_issue_url
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
href=${preview_feature.report_issue_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labs.report_issue")}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
.variant=${preview_feature.enabled ? "danger" : "brand"}
|
||||
@click=${this._handleToggle}
|
||||
.preview_feature=${preview_feature}
|
||||
>
|
||||
${this.hass.localize(
|
||||
preview_feature.enabled
|
||||
? "ui.panel.config.labs.disable"
|
||||
: "ui.panel.config.labs.enable"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _scrollToPreviewFeature(previewFeatureId: string): void {
|
||||
const card = this.shadowRoot?.querySelector(
|
||||
`[data-feature-id="${previewFeatureId}"]`
|
||||
) as HTMLElement;
|
||||
if (card) {
|
||||
card.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
// Clear highlight after animation
|
||||
setTimeout(() => {
|
||||
this._highlightedPreviewFeature = undefined;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleToggle(ev: Event): Promise<void> {
|
||||
const buttonEl = ev.currentTarget as HTMLElement & {
|
||||
preview_feature: LabPreviewFeature;
|
||||
};
|
||||
const preview_feature = buttonEl.preview_feature;
|
||||
const enabled = !preview_feature.enabled;
|
||||
const previewFeatureId = `${preview_feature.domain}.${preview_feature.preview_feature}`;
|
||||
|
||||
if (enabled) {
|
||||
// Show custom enable dialog with backup option
|
||||
showLabsPreviewFeatureEnableDialog(this, {
|
||||
preview_feature,
|
||||
previewFeatureId,
|
||||
onConfirm: async (shouldCreateBackup) => {
|
||||
await this._performToggle(
|
||||
previewFeatureId,
|
||||
enabled,
|
||||
shouldCreateBackup
|
||||
);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Show simple confirmation dialog for disable
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.labs.disable_title"),
|
||||
text:
|
||||
this.hass.localize(
|
||||
`component.${preview_feature.domain}.preview_features.${preview_feature.preview_feature}.disable_confirmation`
|
||||
) || this.hass.localize("ui.panel.config.labs.disable_confirmation"),
|
||||
confirmText: this.hass.localize("ui.panel.config.labs.disable"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._performToggle(previewFeatureId, enabled, false);
|
||||
}
|
||||
|
||||
private async _performToggle(
|
||||
previewFeatureId: string,
|
||||
enabled: boolean,
|
||||
createBackup: boolean
|
||||
): Promise<void> {
|
||||
if (createBackup) {
|
||||
showLabsProgressDialog(this, { enabled });
|
||||
}
|
||||
|
||||
const parts = previewFeatureId.split(".", 2);
|
||||
if (parts.length !== 2) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.unknown_error"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const [domain, preview_feature] = parts;
|
||||
|
||||
try {
|
||||
await labsUpdatePreviewFeature(
|
||||
this.hass,
|
||||
domain,
|
||||
preview_feature,
|
||||
enabled,
|
||||
createBackup
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (createBackup) {
|
||||
closeLabsProgressDialog();
|
||||
}
|
||||
const errorMessage =
|
||||
err?.message || this.hass.localize("ui.common.unknown_error");
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
enabled
|
||||
? "ui.panel.config.labs.enable_failed"
|
||||
: "ui.panel.config.labs.disable_failed",
|
||||
{ error: errorMessage }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Close dialog before showing success toast
|
||||
if (createBackup) {
|
||||
closeLabsProgressDialog();
|
||||
}
|
||||
|
||||
// Show success toast - collection will auto-update via labs_updated event
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
enabled
|
||||
? "ui.panel.config.labs.enabled_success"
|
||||
: "ui.panel.config.labs.disabled_success"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content:has(.empty) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
ha-card.highlighted {
|
||||
animation: highlight-fade 2.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes highlight-fade {
|
||||
0% {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--primary-color),
|
||||
0 0 12px rgba(var(--rgb-primary-color), 0.4);
|
||||
}
|
||||
100% {
|
||||
box-shadow:
|
||||
0 0 0 2px transparent,
|
||||
0 0 0 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* Intro card */
|
||||
.intro-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.intro-card h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
/* Feature cards */
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-header img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.feature-title h2 {
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.integration-name {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
font-size: 14px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty ha-svg-icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty h1 {
|
||||
margin: 24px 0 16px;
|
||||
}
|
||||
|
||||
.empty p {
|
||||
margin: 0 0 24px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.empty a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.empty a:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.empty a ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Card actions */
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.card-actions > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-labs": HaConfigLabs;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LabPreviewFeature } from "../../../data/labs";
|
||||
|
||||
export interface LabsPreviewFeatureEnableDialogParams {
|
||||
preview_feature: LabPreviewFeature;
|
||||
previewFeatureId: string;
|
||||
onConfirm: (createBackup: boolean) => void;
|
||||
}
|
||||
|
||||
export const loadLabsPreviewFeatureEnableDialog = () =>
|
||||
import("./dialog-labs-preview-feature-enable");
|
||||
|
||||
export const showLabsPreviewFeatureEnableDialog = (
|
||||
element: HTMLElement,
|
||||
params: LabsPreviewFeatureEnableDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-labs-preview-feature-enable",
|
||||
dialogImport: loadLabsPreviewFeatureEnableDialog,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { closeDialog } from "../../../dialogs/make-dialog-manager";
|
||||
|
||||
export interface LabsProgressDialogParams {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export const loadLabsProgressDialog = () => import("./dialog-labs-progress");
|
||||
|
||||
export const showLabsProgressDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: LabsProgressDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-labs-progress",
|
||||
dialogImport: loadLabsProgressDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
|
||||
export const closeLabsProgressDialog = () =>
|
||||
closeDialog("dialog-labs-progress");
|
||||
@@ -4,20 +4,18 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { slugify } from "../../../../common/string/slugify";
|
||||
import "../../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-button";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import { saveFrontendSystemData } from "../../../../data/frontend";
|
||||
import type {
|
||||
LovelaceDashboard,
|
||||
LovelaceDashboardCreateParams,
|
||||
LovelaceDashboardMutableParams,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import { DEFAULT_PANEL, setDefaultPanel } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-detail")
|
||||
@@ -61,8 +59,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
if (!this._params || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
const defaultPanelUrlPath =
|
||||
this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
const defaultPanelUrlPath = this.hass.defaultPanel;
|
||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||
|
||||
return html`
|
||||
@@ -254,38 +251,15 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private async _toggleDefault() {
|
||||
private _toggleDefault() {
|
||||
const urlPath = this._params?.urlPath;
|
||||
if (!urlPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
// Add warning dialog to saying that this will change the default dashboard for all users
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_title"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_text"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: false,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveFrontendSystemData(this.hass.connection, "core", {
|
||||
...this.hass.systemData,
|
||||
defaultPanel: urlPath === defaultPanel ? undefined : urlPath,
|
||||
});
|
||||
setDefaultPanel(
|
||||
this,
|
||||
urlPath === this.hass.defaultPanel ? DEFAULT_PANEL : urlPath
|
||||
);
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
fetchDashboards,
|
||||
updateDashboard,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
@@ -287,7 +286,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
);
|
||||
|
||||
private _getItems = memoize(
|
||||
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
|
||||
(dashboards: LovelaceDashboard[], defaultUrlPath: string) => {
|
||||
const defaultMode = (
|
||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
||||
).mode;
|
||||
@@ -404,8 +403,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
@@ -419,7 +416,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._dashboards,
|
||||
this.hass.localize
|
||||
)}
|
||||
.data=${this._getItems(this._dashboards, defaultPanel)}
|
||||
.data=${this._getItems(this._dashboards, this.hass.defaultPanel)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
|
||||
@@ -1112,9 +1112,6 @@ ${rejected
|
||||
private async _delete(scene: SceneEntity): Promise<void> {
|
||||
if (scene.attributes.id) {
|
||||
await deleteScene(this.hass, scene.attributes.id);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== scene.entity_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1183,9 +1183,6 @@ ${rejected
|
||||
);
|
||||
if (entry) {
|
||||
await deleteScript(this.hass, entry.unique_id);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== script.entity_id
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
|
||||
@@ -61,7 +61,7 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
protected render() {
|
||||
const showAttributes = !this.narrow && this.showAttributes;
|
||||
return html`
|
||||
<div
|
||||
<div
|
||||
class=${classMap({ entities: true, "hide-attributes": !showAttributes })}
|
||||
role="table"
|
||||
>
|
||||
@@ -245,7 +245,6 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
:host([virtualize]) {
|
||||
display: block;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.entities {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { mdiPencil, mdiDownload } from "@mdi/js";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -6,7 +6,6 @@ import "../../components/ha-menu-button";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import "../../components/ha-alert";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -22,7 +21,6 @@ import type {
|
||||
GasSourceTypeEnergyPreference,
|
||||
WaterSourceTypeEnergyPreference,
|
||||
DeviceConsumptionEnergyPreference,
|
||||
EnergyCollection,
|
||||
} from "../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
@@ -32,28 +30,13 @@ import {
|
||||
import { fileDownload } from "../../util/file_download";
|
||||
import type { StatisticValue } from "../../data/recorder";
|
||||
|
||||
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
|
||||
|
||||
const ENERGY_LOVELACE_CONFIG: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
strategy: {
|
||||
type: "energy-overview",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
type: "energy",
|
||||
},
|
||||
},
|
||||
{
|
||||
strategy: {
|
||||
type: "energy-electricity",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
},
|
||||
path: "electricity",
|
||||
},
|
||||
{
|
||||
type: "panel",
|
||||
path: "setup",
|
||||
cards: [{ type: "custom:energy-setup-wizard-card" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -63,30 +46,13 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@state() private _viewIndex = 0;
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@property({ attribute: false }) public route?: {
|
||||
path: string;
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
private _energyCollection?: EnergyCollection;
|
||||
|
||||
get _viewPath(): string | undefined {
|
||||
const viewPath: string | undefined = this.route!.path.split("/")[1];
|
||||
return viewPath ? decodeURI(viewPath) : undefined;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._loadPrefs();
|
||||
}
|
||||
|
||||
public async willUpdate(changedProps: PropertyValues) {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
}
|
||||
@@ -96,36 +62,9 @@ class PanelEnergy extends LitElement {
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (oldHass?.locale !== this.hass.locale) {
|
||||
this._setLovelace();
|
||||
} else if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._reloadView();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadPrefs() {
|
||||
if (this._viewPath === "setup") {
|
||||
await import("./cards/energy-setup-wizard-card");
|
||||
} else {
|
||||
this._energyCollection = getEnergyDataCollection(this.hass, {
|
||||
key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
});
|
||||
try {
|
||||
// Have to manually refresh here as we don't want to subscribe yet
|
||||
await this._energyCollection.refresh();
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
navigate("/energy/setup");
|
||||
}
|
||||
this._error = err.message;
|
||||
return;
|
||||
}
|
||||
const prefs = this._energyCollection.prefs!;
|
||||
if (
|
||||
prefs.device_consumption.length === 0 &&
|
||||
prefs.energy_sources.length === 0
|
||||
) {
|
||||
// No energy sources available, start from scratch
|
||||
navigate("/energy/setup");
|
||||
}
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._reloadView();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,33 +73,11 @@ class PanelEnergy extends LitElement {
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._energyCollection?.prefs) {
|
||||
// Still loading
|
||||
return html`<div class="centered">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</div>`;
|
||||
}
|
||||
const { prefs } = this._energyCollection;
|
||||
const isSingleView = prefs.energy_sources.every((source) =>
|
||||
["grid", "solar", "battery"].includes(source.type)
|
||||
);
|
||||
let viewPath = this._viewPath;
|
||||
if (isSingleView) {
|
||||
// if only electricity sources, show electricity view directly
|
||||
viewPath = "electricity";
|
||||
}
|
||||
const viewIndex = Math.max(
|
||||
ENERGY_LOVELACE_CONFIG.views.findIndex((view) => view.path === viewPath),
|
||||
0
|
||||
);
|
||||
const showBack =
|
||||
this._searchParms.has("historyBack") || (!isSingleView && viewIndex > 0);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="toolbar">
|
||||
${showBack
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
@@ -182,7 +99,7 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
<hui-energy-period-selector
|
||||
.hass=${this.hass}
|
||||
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
|
||||
collection-key="energy_dashboard"
|
||||
>
|
||||
${this.hass.user?.is_admin
|
||||
? html` <ha-list-item
|
||||
@@ -210,21 +127,12 @@ class PanelEnergy extends LitElement {
|
||||
.hass=${this.hass}
|
||||
@reload-energy-panel=${this._reloadView}
|
||||
>
|
||||
${this._error
|
||||
? html`<div class="centered">
|
||||
<ha-alert alert-type="error">
|
||||
An error occurred while fetching your energy preferences:
|
||||
${this._error}
|
||||
</ha-alert>
|
||||
</div>`
|
||||
: this._lovelace
|
||||
? html`<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${viewIndex}
|
||||
></hui-view>`
|
||||
: nothing}
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`;
|
||||
}
|
||||
@@ -252,7 +160,9 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
private async _dumpCSV(ev) {
|
||||
ev.stopPropagation();
|
||||
const energyData = this._energyCollection!;
|
||||
const energyData = getEnergyDataCollection(this.hass, {
|
||||
key: "energy_dashboard",
|
||||
});
|
||||
|
||||
if (!energyData.prefs || !energyData.state.stats) {
|
||||
return;
|
||||
@@ -549,11 +459,11 @@ class PanelEnergy extends LitElement {
|
||||
}
|
||||
|
||||
private _reloadView() {
|
||||
// Force strategy to be re-run by making a copy of the view
|
||||
// Force strategy to be re-run by make a copy of the view
|
||||
const config = this._lovelace!.config;
|
||||
this._lovelace = {
|
||||
...this._lovelace!,
|
||||
config: { ...config, views: config.views.map((view) => ({ ...view })) },
|
||||
config: { ...config, views: [{ ...config.views[0] }] },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -655,13 +565,6 @@ class PanelEnergy extends LitElement {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.centered {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
|
||||
|
||||
@customElement("energy-overview-view-strategy")
|
||||
export class EnergyViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
sections: [],
|
||||
dense_section_placement: true,
|
||||
max_columns: 2,
|
||||
};
|
||||
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
});
|
||||
const prefs = energyCollection.prefs;
|
||||
|
||||
// No energy sources available
|
||||
if (
|
||||
!prefs ||
|
||||
(prefs.device_consumption.length === 0 &&
|
||||
prefs.energy_sources.length === 0)
|
||||
) {
|
||||
return view;
|
||||
}
|
||||
|
||||
const hasGrid = prefs.energy_sources.find(
|
||||
(source) =>
|
||||
source.type === "grid" &&
|
||||
(source.flow_from?.length || source.flow_to?.length)
|
||||
) as GridSourceTypeEnergyPreference;
|
||||
const hasReturn = hasGrid && hasGrid.flow_to.length > 0;
|
||||
const hasSolar = prefs.energy_sources.some(
|
||||
(source) => source.type === "solar"
|
||||
);
|
||||
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
|
||||
const hasBattery = prefs.energy_sources.some(
|
||||
(source) => source.type === "battery"
|
||||
);
|
||||
const hasWater = prefs.energy_sources.some(
|
||||
(source) => source.type === "water"
|
||||
);
|
||||
const hasPowerSources = prefs.energy_sources.find(
|
||||
(source) =>
|
||||
(source.type === "solar" && source.stat_rate) ||
|
||||
(source.type === "battery" && source.stat_rate) ||
|
||||
(source.type === "grid" && source.power?.length)
|
||||
);
|
||||
const hasPowerDevices = prefs.device_consumption.find(
|
||||
(device) => device.stat_rate
|
||||
);
|
||||
|
||||
const overviewSection: LovelaceSectionConfig = {
|
||||
type: "grid",
|
||||
column_span: 24,
|
||||
cards: [],
|
||||
};
|
||||
if (hasPowerSources && hasPowerDevices) {
|
||||
overviewSection.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
|
||||
type: "power-sankey",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 24,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Only include if we have a grid or battery.
|
||||
if (hasGrid || hasBattery) {
|
||||
overviewSection.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
|
||||
type: "energy-distribution",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
if (hasGrid || hasSolar || hasBattery || hasGas || hasWater) {
|
||||
overviewSection.cards!.push({
|
||||
type: "energy-sources-table",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
view.sections!.push(overviewSection);
|
||||
|
||||
const electricitySection: LovelaceSectionConfig = {
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize("ui.panel.energy.overview.electricity"),
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/energy/electricity",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (hasPowerSources) {
|
||||
electricitySection.cards!.push({
|
||||
type: "power-sources-graph",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
if (prefs!.device_consumption.length > 3) {
|
||||
electricitySection.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_top_consumers_title"
|
||||
),
|
||||
type: "energy-devices-graph",
|
||||
collection_key: collectionKey,
|
||||
max_devices: 3,
|
||||
modes: ["bar"],
|
||||
});
|
||||
} else if (hasGrid) {
|
||||
const gauges: LovelaceCardConfig[] = [];
|
||||
// Only include if we have a grid source & return.
|
||||
if (hasReturn) {
|
||||
gauges.push({
|
||||
type: "energy-grid-neutrality-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
|
||||
gauges.push({
|
||||
type: "energy-carbon-consumed-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
|
||||
// Only include if we have a solar source.
|
||||
if (hasSolar) {
|
||||
if (hasReturn) {
|
||||
gauges.push({
|
||||
type: "energy-solar-consumed-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
gauges.push({
|
||||
type: "energy-self-sufficiency-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
|
||||
electricitySection.cards!.push({
|
||||
type: "grid",
|
||||
columns: 2,
|
||||
square: false,
|
||||
cards: gauges,
|
||||
});
|
||||
}
|
||||
|
||||
view.sections!.push(electricitySection);
|
||||
|
||||
if (hasGas) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize("ui.panel.energy.overview.gas"),
|
||||
},
|
||||
{
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_gas_graph_title"
|
||||
),
|
||||
type: "energy-gas-graph",
|
||||
collection_key: collectionKey,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWater) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize("ui.panel.energy.overview.water"),
|
||||
},
|
||||
{
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_water_graph_title"
|
||||
),
|
||||
type: "energy-water-graph",
|
||||
collection_key: collectionKey,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"energy-overview-view-strategy": EnergyViewStrategy;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,57 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type {
|
||||
EnergyPreferences,
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../data/energy";
|
||||
import { getEnergyPreferences } from "../../../data/energy";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
|
||||
|
||||
@customElement("energy-electricity-view-strategy")
|
||||
export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
const setupWizard = async (): Promise<LovelaceViewConfig> => {
|
||||
await import("../cards/energy-setup-wizard-card");
|
||||
return {
|
||||
type: "panel",
|
||||
cards: [
|
||||
{
|
||||
type: "custom:energy-setup-wizard-card",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@customElement("energy-view-strategy")
|
||||
export class EnergyViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const view: LovelaceViewConfig = { cards: [] };
|
||||
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
let prefs: EnergyPreferences;
|
||||
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
});
|
||||
const prefs = energyCollection.prefs;
|
||||
|
||||
// No energy sources available
|
||||
if (
|
||||
!prefs ||
|
||||
(prefs.device_consumption.length === 0 &&
|
||||
prefs.energy_sources.length === 0)
|
||||
) {
|
||||
try {
|
||||
prefs = await getEnergyPreferences(hass);
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
return setupWizard();
|
||||
}
|
||||
view.cards!.push({
|
||||
type: "markdown",
|
||||
content: `An error occurred while fetching your energy preferences: ${err.message}.`,
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
// No energy sources available, start from scratch
|
||||
if (
|
||||
prefs!.device_consumption.length === 0 &&
|
||||
prefs!.energy_sources.length === 0
|
||||
) {
|
||||
return setupWizard();
|
||||
}
|
||||
|
||||
view.type = "sidebar";
|
||||
|
||||
const hasGrid = prefs.energy_sources.find(
|
||||
@@ -43,17 +63,12 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
const hasSolar = prefs.energy_sources.some(
|
||||
(source) => source.type === "solar"
|
||||
);
|
||||
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
|
||||
const hasBattery = prefs.energy_sources.some(
|
||||
(source) => source.type === "battery"
|
||||
);
|
||||
const hasPowerSources = prefs.energy_sources.find(
|
||||
(source) =>
|
||||
(source.type === "solar" && source.stat_rate) ||
|
||||
(source.type === "battery" && source.stat_rate) ||
|
||||
(source.type === "grid" && source.power?.length)
|
||||
);
|
||||
const hasPowerDevices = prefs.device_consumption.find(
|
||||
(device) => device.stat_rate
|
||||
const hasWater = prefs.energy_sources.some(
|
||||
(source) => source.type === "water"
|
||||
);
|
||||
|
||||
view.cards!.push({
|
||||
@@ -61,24 +76,6 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
|
||||
if (hasPowerSources) {
|
||||
if (hasPowerDevices) {
|
||||
view.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
|
||||
type: "power-sankey",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 24,
|
||||
},
|
||||
});
|
||||
}
|
||||
view.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.power_sources_graph_title"),
|
||||
type: "power-sources-graph",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a grid or battery.
|
||||
if (hasGrid || hasBattery) {
|
||||
view.cards!.push({
|
||||
@@ -97,6 +94,24 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a gas source.
|
||||
if (hasGas) {
|
||||
view.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"),
|
||||
type: "energy-gas-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a water source.
|
||||
if (hasWater) {
|
||||
view.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
|
||||
type: "energy-water-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a grid or battery.
|
||||
if (hasGrid || hasBattery) {
|
||||
view.cards!.push({
|
||||
@@ -107,14 +122,13 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasGrid || hasSolar || hasBattery) {
|
||||
if (hasGrid || hasSolar || hasGas || hasWater || hasBattery) {
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_sources_table_title"
|
||||
),
|
||||
type: "energy-sources-table",
|
||||
collection_key: "energy_dashboard",
|
||||
types: ["grid", "solar", "battery"],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,6 +170,20 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
|
||||
// Only include if we have at least 1 device in the config.
|
||||
if (prefs.device_consumption.length) {
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_detail_graph_title"
|
||||
),
|
||||
type: "energy-devices-detail-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_graph_title"
|
||||
),
|
||||
type: "energy-devices-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
const showFloorsNAreas = !prefs.device_consumption.some(
|
||||
(d) => d.included_in_stat
|
||||
);
|
||||
@@ -166,20 +194,6 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
group_by_floor: showFloorsNAreas,
|
||||
group_by_area: showFloorsNAreas,
|
||||
});
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_graph_title"
|
||||
),
|
||||
type: "energy-devices-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_detail_graph_title"
|
||||
),
|
||||
type: "energy-devices-detail-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
@@ -188,6 +202,6 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"energy-electricity-view-strategy": EnergyElectricityViewStrategy;
|
||||
"energy-view-strategy": EnergyViewStrategy;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy";
|
||||
import {
|
||||
findEntities,
|
||||
generateEntityFilter,
|
||||
@@ -11,7 +10,12 @@ import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import {
|
||||
computeAreaTileCardConfig,
|
||||
getAreas,
|
||||
getFloors,
|
||||
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
|
||||
|
||||
export interface LightViewStrategyConfig {
|
||||
type: "light";
|
||||
@@ -81,9 +85,9 @@ export class LightViewStrategy extends ReactiveElement {
|
||||
_config: LightViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
const floors = Object.values(hass.floors);
|
||||
const hierarchy = getAreasFloorHierarchy(floors, areas);
|
||||
const areas = getAreas(hass.areas);
|
||||
const floors = getFloors(hass.floors);
|
||||
const home = getHomeStructure(floors, areas);
|
||||
|
||||
const sections: LovelaceSectionRawConfig[] = [];
|
||||
|
||||
@@ -95,11 +99,10 @@ export class LightViewStrategy extends ReactiveElement {
|
||||
|
||||
const entities = findEntities(allEntities, lightFilters);
|
||||
|
||||
const floorCount =
|
||||
hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0);
|
||||
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
||||
|
||||
// Process floors
|
||||
for (const floorStructure of hierarchy.floors) {
|
||||
for (const floorStructure of home.floors) {
|
||||
const floorId = floorStructure.id;
|
||||
const areaIds = floorStructure.areas;
|
||||
const floor = hass.floors[floorId];
|
||||
@@ -128,7 +131,7 @@ export class LightViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
// Process unassigned areas
|
||||
if (hierarchy.areas.length > 0) {
|
||||
if (home.areas.length > 0) {
|
||||
const section: LovelaceSectionRawConfig = {
|
||||
type: "grid",
|
||||
column_span: 2,
|
||||
@@ -143,7 +146,7 @@ export class LightViewStrategy extends ReactiveElement {
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForLight(hierarchy.areas, hass, entities);
|
||||
const areaCards = processAreasForLight(home.areas, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { css, LitElement, nothing, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { isNumericFromAttributes } from "../../../common/number/format_number";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import type {
|
||||
LovelaceCardFeatureContext,
|
||||
BarGaugeCardFeatureConfig,
|
||||
@@ -18,7 +17,7 @@ export const supportsBarGaugeCardFeature = (
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "sensor" && isNumericFromAttributes(stateObj.attributes);
|
||||
return domain === "sensor" && stateObj.attributes.unit_of_measurement === "%";
|
||||
};
|
||||
|
||||
@customElement("hui-bar-gauge-card-feature")
|
||||
@@ -35,11 +34,6 @@ class HuiBarGaugeCardFeature extends LitElement implements LovelaceCardFeature {
|
||||
};
|
||||
}
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import("../editor/config-elements/hui-bar-gauge-card-feature-editor");
|
||||
return document.createElement("hui-bar-gauge-card-feature-editor");
|
||||
}
|
||||
|
||||
public setConfig(config: BarGaugeCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
@@ -59,20 +53,8 @@ class HuiBarGaugeCardFeature extends LitElement implements LovelaceCardFeature {
|
||||
return nothing;
|
||||
}
|
||||
const stateObj = this.hass.states[this.context.entity_id];
|
||||
const min = this._config.min ?? 0;
|
||||
const max = this._config.max ?? 100;
|
||||
const value = parseFloat(stateObj.state);
|
||||
|
||||
if (isNaN(value) || min >= max) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const percentage = Math.max(
|
||||
0,
|
||||
Math.min(100, ((value - min) / (max - min)) * 100)
|
||||
);
|
||||
|
||||
return html`<div style="width: ${percentage}%"></div>
|
||||
const value = stateObj.state;
|
||||
return html`<div style="width: ${value}%"></div>
|
||||
<div class="bar-gauge-background"></div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,24 +124,16 @@ class HuiHistoryChartCardFeature
|
||||
}
|
||||
|
||||
const hourToShow = this._config.hours_to_show ?? DEFAULT_HOURS_TO_SHOW;
|
||||
const detail = this._config.detail !== false; // default to true (high detail)
|
||||
|
||||
return subscribeHistoryStatesTimeWindow(
|
||||
this.hass!,
|
||||
(historyStates) => {
|
||||
// sample to 1 point per hour for low detail or 1 point per 5 pixels for high detail
|
||||
const maxDetails = detail
|
||||
? Math.max(10, this.clientWidth / 5, hourToShow)
|
||||
: Math.max(10, hourToShow);
|
||||
const useMean = !detail;
|
||||
const { points, yAxisOrigin } =
|
||||
coordinatesMinimalResponseCompressedState(
|
||||
historyStates[this.context!.entity_id!],
|
||||
this.clientWidth,
|
||||
this.clientHeight,
|
||||
maxDetails,
|
||||
undefined,
|
||||
useMean
|
||||
this.clientWidth / 5 // sample to 1 point per 5 pixels
|
||||
);
|
||||
this._coordinates = points;
|
||||
this._yAxisOrigin = yAxisOrigin;
|
||||
|
||||
@@ -199,7 +199,6 @@ export interface UpdateActionsCardFeatureConfig {
|
||||
export interface TrendGraphCardFeatureConfig {
|
||||
type: "trend-graph";
|
||||
hours_to_show?: number;
|
||||
detail?: boolean;
|
||||
}
|
||||
|
||||
export const AREA_CONTROLS = [
|
||||
@@ -227,8 +226,6 @@ export interface AreaControlsCardFeatureConfig {
|
||||
|
||||
export interface BarGaugeCardFeatureConfig {
|
||||
type: "bar-gauge";
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export type LovelaceCardFeaturePosition = "bottom" | "inline";
|
||||
|
||||
@@ -135,13 +135,11 @@ export class HuiEnergyDevicesGraphCard
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const modes = this._getAllowedModes();
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||
${modes.length > 1
|
||||
${this._getAllowedModes().length > 1
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${this._chartType === "pie"
|
||||
@@ -168,7 +166,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
this._chartType,
|
||||
this._legendData
|
||||
)}
|
||||
.height=${`${Math.max(modes.includes("pie") ? 300 : 100, (this._legendData?.length || 0) * 28 + 50)}px`}
|
||||
.height=${`${Math.max(300, (this._legendData?.length || 0) * 28 + 50)}px`}
|
||||
.extraComponents=${[PieChart]}
|
||||
@chart-click=${this._handleChartClick}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@@ -494,7 +492,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
show: true,
|
||||
position: "center",
|
||||
color: computedStyle.getPropertyValue("--secondary-text-color"),
|
||||
fontSize: computedStyle.getPropertyValue("--ha-font-size-m"),
|
||||
fontSize: computedStyle.getPropertyValue("--ha-font-size-l"),
|
||||
lineHeight: 24,
|
||||
fontWeight: "bold",
|
||||
formatter: `{a}\n${formatNumber(totalChart, this.hass.locale)} kWh`,
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
@@ -39,8 +38,6 @@ class HuiEnergySankeyCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public layout?: string;
|
||||
|
||||
@state() private _config?: EnergySankeyCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
@@ -388,14 +385,7 @@ class HuiEnergySankeyCard
|
||||
(this._config.layout !== "horizontal" && this._isMobileSize);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this._config.title}
|
||||
class=${classMap({
|
||||
"is-grid": this.layout === "grid",
|
||||
"is-panel": this.layout === "panel",
|
||||
"is-vertical": vertical,
|
||||
})}
|
||||
>
|
||||
<ha-card .header=${this._config.title}>
|
||||
<div class="card-content">
|
||||
${hasData
|
||||
? html`<ha-sankey-chart
|
||||
@@ -520,18 +510,17 @@ class HuiEnergySankeyCard
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
height: calc(
|
||||
var(--row-size, 8) *
|
||||
(var(--row-height, 50px) + var(--row-gap, 0px)) - var(--row-gap, 0px)
|
||||
);
|
||||
}
|
||||
ha-card {
|
||||
height: 400px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--chart-max-height: none;
|
||||
}
|
||||
ha-card.is-vertical {
|
||||
height: 500px;
|
||||
}
|
||||
ha-card.is-grid,
|
||||
ha-card.is-panel {
|
||||
height: 100%;
|
||||
}
|
||||
.card-content {
|
||||
flex: 1;
|
||||
|
||||
@@ -1,739 +0,0 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../../data/energy";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
|
||||
import type { PowerSankeyCardConfig } from "../types";
|
||||
import "../../../../components/chart/ha-sankey-chart";
|
||||
import type { Link, Node } from "../../../../components/chart/ha-sankey-chart";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||
import { MobileAwareMixin } from "../../../../mixins/mobile-aware-mixin";
|
||||
|
||||
const DEFAULT_CONFIG: Partial<PowerSankeyCardConfig> = {
|
||||
group_by_floor: true,
|
||||
group_by_area: true,
|
||||
};
|
||||
|
||||
interface PowerData {
|
||||
solar: number;
|
||||
from_grid: number;
|
||||
to_grid: number;
|
||||
from_battery: number;
|
||||
to_battery: number;
|
||||
grid_to_battery: number;
|
||||
battery_to_grid: number;
|
||||
solar_to_battery: number;
|
||||
solar_to_grid: number;
|
||||
used_solar: number;
|
||||
used_grid: number;
|
||||
used_battery: number;
|
||||
used_total: number;
|
||||
}
|
||||
|
||||
@customElement("hui-power-sankey-card")
|
||||
class HuiPowerSankeyCard
|
||||
extends SubscribeMixin(MobileAwareMixin(LitElement))
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public layout?: string;
|
||||
|
||||
@state() private _config?: PowerSankeyCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
private _entities = new Set<string>();
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public setConfig(config: PowerSankeyCardConfig): void {
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
getEnergyDataCollection(this.hass, {
|
||||
key: this._config?.collection_key,
|
||||
}).subscribe((data) => {
|
||||
this._data = data;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
min_columns: 6,
|
||||
rows: 6,
|
||||
min_rows: 2,
|
||||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (
|
||||
changedProps.has("_config") ||
|
||||
changedProps.has("_data") ||
|
||||
changedProps.has("_isMobileSize")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any of the tracked entity states have changed
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || !this._entities.size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only update if one of our tracked entities changed
|
||||
for (const entityId of this._entities) {
|
||||
if (oldHass.states[entityId] !== this.hass.states[entityId]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._data) {
|
||||
return html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.loading"
|
||||
)}`;
|
||||
}
|
||||
|
||||
const prefs = this._data.prefs;
|
||||
const powerData = this._computePowerData(prefs);
|
||||
const computedStyle = getComputedStyle(this);
|
||||
|
||||
const nodes: Node[] = [];
|
||||
const links: Link[] = [];
|
||||
|
||||
// Create home node
|
||||
const homeNode: Node = {
|
||||
id: "home",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.home"
|
||||
),
|
||||
value: Math.max(0, powerData.used_total),
|
||||
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||
index: 1,
|
||||
};
|
||||
nodes.push(homeNode);
|
||||
|
||||
// Add battery source and sink if available
|
||||
if (powerData.from_battery > 0) {
|
||||
nodes.push({
|
||||
id: "battery",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
||||
),
|
||||
value: powerData.from_battery,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--energy-battery-out-color")
|
||||
.trim(),
|
||||
index: 0,
|
||||
});
|
||||
links.push({
|
||||
source: "battery",
|
||||
target: "home",
|
||||
});
|
||||
}
|
||||
|
||||
if (powerData.to_battery > 0) {
|
||||
nodes.push({
|
||||
id: "battery_in",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
||||
),
|
||||
value: powerData.to_battery,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--energy-battery-in-color")
|
||||
.trim(),
|
||||
index: 1,
|
||||
});
|
||||
if (powerData.grid_to_battery > 0) {
|
||||
links.push({
|
||||
source: "grid",
|
||||
target: "battery_in",
|
||||
});
|
||||
}
|
||||
if (powerData.solar_to_battery > 0) {
|
||||
links.push({
|
||||
source: "solar",
|
||||
target: "battery_in",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add grid source if available
|
||||
if (powerData.from_grid > 0) {
|
||||
nodes.push({
|
||||
id: "grid",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.grid"
|
||||
),
|
||||
value: powerData.from_grid,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--energy-grid-consumption-color")
|
||||
.trim(),
|
||||
index: 0,
|
||||
});
|
||||
links.push({
|
||||
source: "grid",
|
||||
target: "home",
|
||||
});
|
||||
}
|
||||
|
||||
// Add solar if available
|
||||
if (powerData.solar > 0) {
|
||||
nodes.push({
|
||||
id: "solar",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.solar"
|
||||
),
|
||||
value: powerData.solar,
|
||||
color: computedStyle.getPropertyValue("--energy-solar-color").trim(),
|
||||
index: 0,
|
||||
});
|
||||
links.push({
|
||||
source: "solar",
|
||||
target: "home",
|
||||
});
|
||||
}
|
||||
|
||||
// Add grid return if available
|
||||
if (powerData.to_grid > 0) {
|
||||
nodes.push({
|
||||
id: "grid_return",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.grid"
|
||||
),
|
||||
value: powerData.to_grid,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--energy-grid-return-color")
|
||||
.trim(),
|
||||
index: 2,
|
||||
});
|
||||
if (powerData.battery_to_grid > 0) {
|
||||
links.push({
|
||||
source: "battery",
|
||||
target: "grid_return",
|
||||
});
|
||||
}
|
||||
if (powerData.solar_to_grid > 0) {
|
||||
links.push({
|
||||
source: "solar",
|
||||
target: "grid_return",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let untrackedConsumption = homeNode.value;
|
||||
const deviceNodes: Node[] = [];
|
||||
const parentLinks: Record<string, string> = {};
|
||||
prefs.device_consumption.forEach((device, idx) => {
|
||||
if (!device.stat_rate) {
|
||||
return;
|
||||
}
|
||||
const value = this._getCurrentPower(device.stat_rate);
|
||||
|
||||
if (value < 0.01) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = {
|
||||
id: device.stat_rate,
|
||||
label: device.name || this._getEntityLabel(device.stat_rate),
|
||||
value,
|
||||
color: getGraphColorByIndex(idx, computedStyle),
|
||||
index: 4,
|
||||
parent: device.included_in_stat,
|
||||
};
|
||||
if (node.parent) {
|
||||
parentLinks[node.id] = node.parent;
|
||||
links.push({
|
||||
source: node.parent,
|
||||
target: node.id,
|
||||
});
|
||||
} else {
|
||||
untrackedConsumption -= value;
|
||||
}
|
||||
deviceNodes.push(node);
|
||||
});
|
||||
const devicesWithoutParent = deviceNodes.filter(
|
||||
(node) => !parentLinks[node.id]
|
||||
);
|
||||
|
||||
const { group_by_area, group_by_floor } = this._config;
|
||||
if (group_by_area || group_by_floor) {
|
||||
const { areas, floors } = this._groupByFloorAndArea(devicesWithoutParent);
|
||||
|
||||
Object.keys(floors)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(this.hass.floors[b]?.level ?? -Infinity) -
|
||||
(this.hass.floors[a]?.level ?? -Infinity)
|
||||
)
|
||||
.forEach((floorId) => {
|
||||
let floorNodeId = `floor_${floorId}`;
|
||||
if (floorId === "no_floor" || !group_by_floor) {
|
||||
// link "no_floor" areas to home
|
||||
floorNodeId = "home";
|
||||
} else {
|
||||
nodes.push({
|
||||
id: floorNodeId,
|
||||
label: this.hass.floors[floorId].name,
|
||||
value: floors[floorId].value,
|
||||
index: 2,
|
||||
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||
});
|
||||
links.push({
|
||||
source: "home",
|
||||
target: floorNodeId,
|
||||
});
|
||||
}
|
||||
floors[floorId].areas.forEach((areaId) => {
|
||||
let targetNodeId: string;
|
||||
|
||||
if (areaId === "no_area" || !group_by_area) {
|
||||
// If group_by_area is false, link devices to floor or home
|
||||
targetNodeId = floorNodeId;
|
||||
} else {
|
||||
// Create area node and link it to floor
|
||||
const areaNodeId = `area_${areaId}`;
|
||||
nodes.push({
|
||||
id: areaNodeId,
|
||||
label: this.hass.areas[areaId]?.name || areaId,
|
||||
value: areas[areaId].value,
|
||||
index: 3,
|
||||
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||
});
|
||||
links.push({
|
||||
source: floorNodeId,
|
||||
target: areaNodeId,
|
||||
value: areas[areaId].value,
|
||||
});
|
||||
targetNodeId = areaNodeId;
|
||||
}
|
||||
|
||||
// Link devices to the appropriate target (area, floor, or home)
|
||||
areas[areaId].devices.forEach((device) => {
|
||||
links.push({
|
||||
source: targetNodeId,
|
||||
target: device.id,
|
||||
value: device.value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
devicesWithoutParent.forEach((deviceNode) => {
|
||||
links.push({
|
||||
source: "home",
|
||||
target: deviceNode.id,
|
||||
value: deviceNode.value,
|
||||
});
|
||||
});
|
||||
}
|
||||
const deviceSections = this._getDeviceSections(parentLinks, deviceNodes);
|
||||
deviceSections.forEach((section, index) => {
|
||||
section.forEach((node: Node) => {
|
||||
nodes.push({ ...node, index: 4 + index });
|
||||
});
|
||||
});
|
||||
|
||||
// untracked consumption
|
||||
if (untrackedConsumption > 0) {
|
||||
nodes.push({
|
||||
id: "untracked",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked_consumption"
|
||||
),
|
||||
value: untrackedConsumption,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--state-unavailable-color")
|
||||
.trim(),
|
||||
index: 3 + deviceSections.length,
|
||||
});
|
||||
links.push({
|
||||
source: "home",
|
||||
target: "untracked",
|
||||
value: untrackedConsumption,
|
||||
});
|
||||
}
|
||||
|
||||
const hasData = nodes.some((node) => node.value > 0);
|
||||
|
||||
const vertical =
|
||||
this._config.layout === "vertical" ||
|
||||
(this._config.layout !== "horizontal" && this._isMobileSize);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this._config.title}
|
||||
class=${classMap({
|
||||
"is-grid": this.layout === "grid",
|
||||
"is-panel": this.layout === "panel",
|
||||
"is-vertical": vertical,
|
||||
})}
|
||||
>
|
||||
<div class="card-content">
|
||||
${hasData
|
||||
? html`<ha-sankey-chart
|
||||
.data=${{ nodes, links }}
|
||||
.vertical=${vertical}
|
||||
.valueFormatter=${this._valueFormatter}
|
||||
></ha-sankey-chart>`
|
||||
: html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.no_data"
|
||||
)}`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||
kW</div>`;
|
||||
|
||||
/**
|
||||
* Compute real-time power data from current entity states.
|
||||
* Similar to computeConsumptionData but for instantaneous power.
|
||||
*/
|
||||
private _computePowerData(prefs: EnergyPreferences): PowerData {
|
||||
// Clear tracked entities and rebuild the set
|
||||
this._entities.clear();
|
||||
|
||||
let solar = 0;
|
||||
let from_grid = 0;
|
||||
let to_grid = 0;
|
||||
let from_battery = 0;
|
||||
let to_battery = 0;
|
||||
|
||||
// Collect solar power
|
||||
prefs.energy_sources
|
||||
.filter((source) => source.type === "solar")
|
||||
.forEach((source) => {
|
||||
if (source.type === "solar" && source.stat_rate) {
|
||||
const value = this._getCurrentPower(source.stat_rate);
|
||||
if (value > 0) {
|
||||
solar += value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Collect grid power (positive = import, negative = export)
|
||||
prefs.energy_sources
|
||||
.filter((source) => source.type === "grid" && source.power)
|
||||
.forEach((source) => {
|
||||
if (source.type === "grid" && source.power) {
|
||||
source.power.forEach((powerSource) => {
|
||||
const value = this._getCurrentPower(powerSource.stat_rate);
|
||||
if (value > 0) {
|
||||
from_grid += value;
|
||||
} else if (value < 0) {
|
||||
to_grid += Math.abs(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Collect battery power (positive = discharge, negative = charge)
|
||||
prefs.energy_sources
|
||||
.filter((source) => source.type === "battery")
|
||||
.forEach((source) => {
|
||||
if (source.type === "battery" && source.stat_rate) {
|
||||
const value = this._getCurrentPower(source.stat_rate);
|
||||
if (value > 0) {
|
||||
from_battery += value;
|
||||
} else if (value < 0) {
|
||||
to_battery += Math.abs(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate total consumption
|
||||
const used_total = from_grid + solar + from_battery - to_grid - to_battery;
|
||||
|
||||
// Determine power routing using priority logic
|
||||
// Priority: Solar -> Battery_In, Solar -> Grid_Out, Battery_Out -> Grid_Out,
|
||||
// Grid_In -> Battery_In, Solar -> Consumption, Battery_Out -> Consumption, Grid_In -> Consumption
|
||||
|
||||
let solar_remaining = solar;
|
||||
let grid_remaining = from_grid;
|
||||
let battery_remaining = from_battery;
|
||||
let to_battery_remaining = to_battery;
|
||||
let to_grid_remaining = to_grid;
|
||||
let used_total_remaining = Math.max(used_total, 0);
|
||||
|
||||
let grid_to_battery = 0;
|
||||
let battery_to_grid = 0;
|
||||
let solar_to_battery = 0;
|
||||
let solar_to_grid = 0;
|
||||
let used_solar = 0;
|
||||
let used_battery = 0;
|
||||
let used_grid = 0;
|
||||
|
||||
// Handle excess grid input to battery first
|
||||
const excess_grid_in_after_consumption = Math.max(
|
||||
0,
|
||||
Math.min(to_battery_remaining, grid_remaining - used_total_remaining)
|
||||
);
|
||||
grid_to_battery += excess_grid_in_after_consumption;
|
||||
to_battery_remaining -= excess_grid_in_after_consumption;
|
||||
grid_remaining -= excess_grid_in_after_consumption;
|
||||
|
||||
// Solar -> Battery_In
|
||||
solar_to_battery = Math.min(solar_remaining, to_battery_remaining);
|
||||
to_battery_remaining -= solar_to_battery;
|
||||
solar_remaining -= solar_to_battery;
|
||||
|
||||
// Solar -> Grid_Out
|
||||
solar_to_grid = Math.min(solar_remaining, to_grid_remaining);
|
||||
to_grid_remaining -= solar_to_grid;
|
||||
solar_remaining -= solar_to_grid;
|
||||
|
||||
// Battery_Out -> Grid_Out
|
||||
battery_to_grid = Math.min(battery_remaining, to_grid_remaining);
|
||||
battery_remaining -= battery_to_grid;
|
||||
to_grid_remaining -= battery_to_grid;
|
||||
|
||||
// Grid_In -> Battery_In (second pass)
|
||||
const grid_to_battery_2 = Math.min(grid_remaining, to_battery_remaining);
|
||||
grid_to_battery += grid_to_battery_2;
|
||||
grid_remaining -= grid_to_battery_2;
|
||||
to_battery_remaining -= grid_to_battery_2;
|
||||
|
||||
// Solar -> Consumption
|
||||
used_solar = Math.min(used_total_remaining, solar_remaining);
|
||||
used_total_remaining -= used_solar;
|
||||
solar_remaining -= used_solar;
|
||||
|
||||
// Battery_Out -> Consumption
|
||||
used_battery = Math.min(battery_remaining, used_total_remaining);
|
||||
battery_remaining -= used_battery;
|
||||
used_total_remaining -= used_battery;
|
||||
|
||||
// Grid_In -> Consumption
|
||||
used_grid = Math.min(used_total_remaining, grid_remaining);
|
||||
grid_remaining -= used_grid;
|
||||
used_total_remaining -= used_grid;
|
||||
|
||||
return {
|
||||
solar,
|
||||
from_grid,
|
||||
to_grid,
|
||||
from_battery,
|
||||
to_battery,
|
||||
grid_to_battery,
|
||||
battery_to_grid,
|
||||
solar_to_battery,
|
||||
solar_to_grid,
|
||||
used_solar,
|
||||
used_grid,
|
||||
used_battery,
|
||||
used_total: Math.max(0, used_total),
|
||||
};
|
||||
}
|
||||
|
||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||
no_area: {
|
||||
value: 0,
|
||||
devices: [],
|
||||
},
|
||||
};
|
||||
const floors: Record<string, { value: number; areas: string[] }> = {
|
||||
no_floor: {
|
||||
value: 0,
|
||||
areas: ["no_area"],
|
||||
},
|
||||
};
|
||||
deviceNodes.forEach((deviceNode) => {
|
||||
const entity = this.hass.states[deviceNode.id];
|
||||
const { area, floor } = entity
|
||||
? getEntityContext(
|
||||
entity,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: { area: null, floor: null };
|
||||
if (area) {
|
||||
if (area.area_id in areas) {
|
||||
areas[area.area_id].value += deviceNode.value;
|
||||
areas[area.area_id].devices.push(deviceNode);
|
||||
} else {
|
||||
areas[area.area_id] = {
|
||||
value: deviceNode.value,
|
||||
devices: [deviceNode],
|
||||
};
|
||||
}
|
||||
// see if the area has a floor
|
||||
if (floor) {
|
||||
if (floor.floor_id in floors) {
|
||||
floors[floor.floor_id].value += deviceNode.value;
|
||||
if (!floors[floor.floor_id].areas.includes(area.area_id)) {
|
||||
floors[floor.floor_id].areas.push(area.area_id);
|
||||
}
|
||||
} else {
|
||||
floors[floor.floor_id] = {
|
||||
value: deviceNode.value,
|
||||
areas: [area.area_id],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
floors.no_floor.value += deviceNode.value;
|
||||
if (!floors.no_floor.areas.includes(area.area_id)) {
|
||||
floors.no_floor.areas.unshift(area.area_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
areas.no_area.value += deviceNode.value;
|
||||
areas.no_area.devices.push(deviceNode);
|
||||
}
|
||||
});
|
||||
return { areas, floors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Organizes device nodes into hierarchical sections based on parent-child relationships.
|
||||
*/
|
||||
protected _getDeviceSections(
|
||||
parentLinks: Record<string, string>,
|
||||
deviceNodes: Node[]
|
||||
): Node[][] {
|
||||
const parentSection: Node[] = [];
|
||||
const childSection: Node[] = [];
|
||||
const parentIds = Object.values(parentLinks);
|
||||
const remainingLinks: typeof parentLinks = {};
|
||||
|
||||
deviceNodes.forEach((deviceNode) => {
|
||||
const isChild = deviceNode.id in parentLinks;
|
||||
const isParent = parentIds.includes(deviceNode.id);
|
||||
if (isParent && !isChild) {
|
||||
// Top-level parents (have children but no parents themselves)
|
||||
parentSection.push(deviceNode);
|
||||
} else {
|
||||
childSection.push(deviceNode);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter out links where parent is already in current parent section
|
||||
Object.entries(parentLinks).forEach(([child, parent]) => {
|
||||
if (!parentSection.some((node) => node.id === parent)) {
|
||||
remainingLinks[child] = parent;
|
||||
}
|
||||
});
|
||||
|
||||
if (parentSection.length > 0) {
|
||||
// Recursively process child section with remaining links
|
||||
return [
|
||||
parentSection,
|
||||
...this._getDeviceSections(remainingLinks, childSection),
|
||||
];
|
||||
}
|
||||
|
||||
// Base case: no more parent-child relationships to process
|
||||
return [deviceNodes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current power value from entity state, normalized to kW
|
||||
* @param entityId - The entity ID to get power value from
|
||||
* @returns Power value in kW, or 0 if entity not found or invalid
|
||||
*/
|
||||
private _getCurrentPower(entityId: string): number {
|
||||
// Track this entity for state change detection
|
||||
this._entities.add(entityId);
|
||||
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return 0;
|
||||
}
|
||||
const value = parseFloat(stateObj.state);
|
||||
if (isNaN(value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Normalize to kW based on unit of measurement (case-sensitive)
|
||||
// Supported units: GW, kW, MW, mW, TW, W
|
||||
const unit = stateObj.attributes.unit_of_measurement;
|
||||
switch (unit) {
|
||||
case "W":
|
||||
return value / 1000;
|
||||
case "mW":
|
||||
return value / 1000000;
|
||||
case "MW":
|
||||
return value * 1000;
|
||||
case "GW":
|
||||
return value * 1000000;
|
||||
case "TW":
|
||||
return value * 1000000000;
|
||||
default:
|
||||
// Assume kW if no unit or unit is kW
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity label (friendly name or entity ID)
|
||||
* @param entityId - The entity ID to get label for
|
||||
* @returns Friendly name if available, otherwise the entity ID
|
||||
*/
|
||||
private _getEntityLabel(entityId: string): string {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return entityId;
|
||||
}
|
||||
return stateObj.attributes.friendly_name || entityId;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--chart-max-height: none;
|
||||
}
|
||||
ha-card.is-vertical {
|
||||
height: 500px;
|
||||
}
|
||||
ha-card.is-grid,
|
||||
ha-card.is-panel {
|
||||
height: 100%;
|
||||
}
|
||||
.card-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-power-sankey-card": HuiPowerSankeyCard;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import { hex2rgb } from "../../../../common/color/convert-color";
|
||||
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";
|
||||
|
||||
@customElement("hui-power-sources-graph-card")
|
||||
export class HuiPowerSourcesGraphCard
|
||||
@@ -34,8 +33,6 @@ export class HuiPowerSourcesGraphCard
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@state() private _legendData?: CustomLegendOption["data"];
|
||||
|
||||
@state() private _start = startOfToday();
|
||||
|
||||
@state() private _end = endOfToday();
|
||||
@@ -94,8 +91,7 @@ export class HuiPowerSourcesGraphCard
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this._compareStart,
|
||||
this._compareEnd,
|
||||
this._legendData
|
||||
this._compareEnd
|
||||
)}
|
||||
></ha-chart-base>
|
||||
${!this._chartData.some((dataset) => dataset.data!.length)
|
||||
@@ -119,10 +115,9 @@ export class HuiPowerSourcesGraphCard
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
compareStart?: Date,
|
||||
compareEnd?: Date,
|
||||
legendData?: CustomLegendOption["data"]
|
||||
): ECOption => ({
|
||||
...getCommonOptions(
|
||||
compareEnd?: Date
|
||||
): ECOption =>
|
||||
getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
locale,
|
||||
@@ -130,18 +125,11 @@ export class HuiPowerSourcesGraphCard
|
||||
"kW",
|
||||
compareStart,
|
||||
compareEnd
|
||||
),
|
||||
legend: {
|
||||
show: true,
|
||||
type: "custom",
|
||||
data: legendData,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||
const datasets: LineSeriesOption[] = [];
|
||||
this._legendData = [];
|
||||
|
||||
const statIds = {
|
||||
solar: {
|
||||
@@ -250,15 +238,6 @@ export class HuiPowerSourcesGraphCard
|
||||
z: 4 - keyIndex, // draw in reverse order but above positive series
|
||||
});
|
||||
}
|
||||
this._legendData!.push({
|
||||
id: key,
|
||||
secondaryIds: key !== "solar" ? [`${key}-negative`] : [],
|
||||
name: statIds[key].name,
|
||||
itemStyle: {
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
||||
borderColor: colorHex,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -289,23 +268,11 @@ export class HuiPowerSourcesGraphCard
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.usage"
|
||||
),
|
||||
color: computedStyles.getPropertyValue("--primary-text-color"),
|
||||
lineStyle: {
|
||||
type: [7, 2],
|
||||
width: 1.5,
|
||||
},
|
||||
color: computedStyles.getPropertyValue("--primary-color"),
|
||||
lineStyle: { width: 2 },
|
||||
data: usageData,
|
||||
z: 5,
|
||||
});
|
||||
this._legendData!.push({
|
||||
id: "usage",
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.usage"
|
||||
),
|
||||
itemStyle: {
|
||||
color: computedStyles.getPropertyValue("--primary-text-color"),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _processData(stats: StatisticValue[][]) {
|
||||
|
||||
@@ -150,6 +150,11 @@ export interface EnergyCardBaseConfig extends LovelaceCardConfig {
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergySummaryCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-summary";
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface EnergyDistributionCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-distribution";
|
||||
title?: string;
|
||||
@@ -231,14 +236,6 @@ export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface PowerSankeyCardConfig extends EnergyCardBaseConfig {
|
||||
type: "power-sankey";
|
||||
title?: string;
|
||||
layout?: "vertical" | "horizontal" | "auto";
|
||||
group_by_floor?: boolean;
|
||||
group_by_area?: boolean;
|
||||
}
|
||||
|
||||
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
||||
type: "entity-filter";
|
||||
entities: (EntityFilterEntityConfig | string)[];
|
||||
|
||||
@@ -68,7 +68,6 @@ const LAZY_LOAD_TYPES = {
|
||||
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
||||
"power-sources-graph": () =>
|
||||
import("../cards/energy/hui-power-sources-graph-card"),
|
||||
"power-sankey": () => import("../cards/energy/hui-power-sankey-card"),
|
||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||
error: () => import("../cards/hui-error-card"),
|
||||
"home-summary": () => import("../cards/hui-home-summary-card"),
|
||||
|
||||
@@ -21,10 +21,7 @@ import {
|
||||
} from "../../../../data/lovelace_custom_cards";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../resources/styles";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../badges/hui-badge";
|
||||
@@ -398,7 +395,6 @@ export class HuiDialogEditBadge
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
:host {
|
||||
--code-mirror-max-height: calc(100vh - 176px);
|
||||
@@ -407,6 +403,8 @@ export class HuiDialogEditBadge
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 100px;
|
||||
--dialog-z-index: 6;
|
||||
--dialog-surface-position: fixed;
|
||||
--dialog-surface-top: 40px;
|
||||
--mdc-dialog-max-width: 90vw;
|
||||
--dialog-content-padding: 24px 12px;
|
||||
}
|
||||
|
||||
@@ -184,15 +184,19 @@ export class HuiCreateDialogCard
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 850px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 845px;
|
||||
--mdc-dialog-min-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--mdc-dialog-min-height: calc(100vh - 72px);
|
||||
--mdc-dialog-max-height: calc(100vh - 72px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,7 @@ import {
|
||||
} from "../../../../data/lovelace_custom_cards";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../resources/styles";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
@@ -374,7 +371,6 @@ export class HuiDialogEditCard
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
:host {
|
||||
--code-mirror-max-height: calc(100vh - 176px);
|
||||
@@ -383,6 +379,8 @@ export class HuiDialogEditCard
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 100px;
|
||||
--dialog-z-index: 6;
|
||||
--dialog-surface-position: fixed;
|
||||
--dialog-surface-top: 40px;
|
||||
--mdc-dialog-max-width: 90vw;
|
||||
--dialog-content-padding: 24px 12px;
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
BarGaugeCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
@customElement("hui-bar-gauge-card-feature-editor")
|
||||
export class HuiBarGaugeCardFeatureEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: BarGaugeCardFeatureConfig;
|
||||
|
||||
public setConfig(config: BarGaugeCardFeatureConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: "min",
|
||||
default: 0,
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
default: 100,
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema();
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.bar-gauge.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-bar-gauge-card-feature-editor": HuiBarGaugeCardFeatureEditor;
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,6 @@ type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number];
|
||||
const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
||||
"alarm-modes",
|
||||
"area-controls",
|
||||
"bar-gauge",
|
||||
"button",
|
||||
"climate-fan-modes",
|
||||
"climate-hvac-modes",
|
||||
|
||||
@@ -20,10 +20,6 @@ const SCHEMA = [
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "detail",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const satisfies HaFormSchema[];
|
||||
|
||||
@customElement("hui-trend-graph-card-feature-editor")
|
||||
@@ -52,10 +48,6 @@ export class HuiTrendGraphCardFeatureEditor
|
||||
data.hours_to_show = DEFAULT_HOURS_TO_SHOW;
|
||||
}
|
||||
|
||||
if (this._config.detail === undefined) {
|
||||
data.detail = true;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
@@ -77,10 +69,6 @@ export class HuiTrendGraphCardFeatureEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
case "detail":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.features.types.trend-graph.detail"
|
||||
);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -17,10 +17,7 @@ import "../../../../../components/ha-dialog-header";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import type { LovelaceStrategyConfig } from "../../../../../data/lovelace/config/strategy";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../../resources/styles";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../../util/toast-saved-success";
|
||||
import { cleanLegacyStrategyConfig } from "../../../strategies/legacy-strategy";
|
||||
@@ -222,21 +219,14 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0 24px;
|
||||
--mdc-dialog-min-width: min(
|
||||
640px,
|
||||
calc(100vw - var(--safe-area-inset-x))
|
||||
);
|
||||
--mdc-dialog-max-width: min(
|
||||
640px,
|
||||
calc(100vw - var(--safe-area-inset-x))
|
||||
);
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-20) - var(--safe-area-inset-y)
|
||||
);
|
||||
--dialog-surface-position: fixed;
|
||||
--dialog-surface-top: 40px;
|
||||
--mdc-dialog-min-width: min(640px, calc(100% - 32px));
|
||||
--mdc-dialog-max-width: min(640px, calc(100% - 32px));
|
||||
--mdc-dialog-max-height: calc(100% - 80px);
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
@@ -244,12 +234,9 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
ha-dialog {
|
||||
height: 100%;
|
||||
--dialog-surface-top: 0px;
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-width: 100vw;
|
||||
--mdc-dialog-min-height: 100vh;
|
||||
--mdc-dialog-min-height: 100svh;
|
||||
--mdc-dialog-max-height: 100vh;
|
||||
--mdc-dialog-max-height: 100svh;
|
||||
--mdc-dialog-min-width: 100%;
|
||||
--mdc-dialog-max-width: 100%;
|
||||
--mdc-dialog-max-height: 100%;
|
||||
--dialog-content-padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,7 @@ import {
|
||||
} from "../../../../data/lovelace/config/view";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../resources/styles";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Lovelace } from "../../types";
|
||||
import { addSection, deleteSection, moveSection } from "../config-util";
|
||||
@@ -421,8 +418,19 @@ export class HuiDialogEditSection
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
|
||||
--vertical-align-dialog: flex-start;
|
||||
--dialog-surface-margin-top: 40px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* When in fullscreen dialog should be attached to top */
|
||||
ha-dialog {
|
||||
--dialog-surface-margin-top: 0px;
|
||||
}
|
||||
}
|
||||
ha-dialog.yaml-mode {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@@ -3,21 +3,20 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-md-select";
|
||||
import "../../../../components/ha-md-select-option";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-spinner";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceDashboard } from "../../../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../../../data/lovelace/dashboard";
|
||||
import { getDefaultPanelUrlPath } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { SelectDashboardDialogParams } from "./show-select-dashboard-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
|
||||
@customElement("hui-dialog-select-dashboard")
|
||||
export class HuiDialogSelectDashboard extends LitElement {
|
||||
@@ -144,9 +143,7 @@ export class HuiDialogSelectDashboard extends LitElement {
|
||||
...(this._params!.dashboards || (await fetchDashboards(this.hass))),
|
||||
];
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
const currentPath = this._fromUrlPath || defaultPanel;
|
||||
const currentPath = this._fromUrlPath || this.hass.defaultPanel;
|
||||
for (const dashboard of this._dashboards!) {
|
||||
if (dashboard.url_path !== currentPath) {
|
||||
this._toUrlPath = dashboard.url_path;
|
||||
|
||||
@@ -16,7 +16,6 @@ import { fetchConfig } from "../../../../data/lovelace/config/types";
|
||||
import { isStrategyView } from "../../../../data/lovelace/config/view";
|
||||
import type { LovelaceDashboard } from "../../../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../../../data/lovelace/dashboard";
|
||||
import { getDefaultPanelUrlPath } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { SelectViewDialogParams } from "./show-select-view-dialog";
|
||||
@@ -61,9 +60,6 @@ export class HuiDialogSelectView extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@@ -80,7 +76,7 @@ export class HuiDialogSelectView extends LitElement {
|
||||
"ui.panel.lovelace.editor.select_view.dashboard_label"
|
||||
)}
|
||||
.disabled=${!this._dashboards.length}
|
||||
.value=${this._urlPath || defaultPanel}
|
||||
.value=${this._urlPath || this.hass.defaultPanel}
|
||||
@selected=${this._dashboardChanged}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
|
||||
@@ -36,10 +36,7 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../resources/styles";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../components/hui-entity-editor";
|
||||
import type { Lovelace } from "../../types";
|
||||
@@ -634,8 +631,19 @@ export class HuiDialogEditView extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
|
||||
--vertical-align-dialog: flex-start;
|
||||
--dialog-surface-margin-top: 40px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* When in fullscreen dialog should be attached to top */
|
||||
ha-dialog {
|
||||
--dialog-surface-margin-top: 0px;
|
||||
}
|
||||
}
|
||||
ha-dialog.yaml-mode {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@@ -16,10 +16,7 @@ import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { LovelaceViewHeaderConfig } from "../../../../data/lovelace/config/view";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../resources/styles";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./hui-view-header-settings-editor";
|
||||
import type { EditViewHeaderDialogParams } from "./show-edit-view-header-dialog";
|
||||
@@ -204,8 +201,19 @@ export class HuiDialogEditViewHeader extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
|
||||
--vertical-align-dialog: flex-start;
|
||||
--dialog-surface-margin-top: 40px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* When in fullscreen dialog should be attached to top */
|
||||
ha-dialog {
|
||||
--dialog-surface-margin-top: 0px;
|
||||
}
|
||||
}
|
||||
ha-dialog.yaml-mode {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@@ -38,10 +38,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
||||
view: {
|
||||
"original-states": () =>
|
||||
import("./original-states/original-states-view-strategy"),
|
||||
"energy-overview": () =>
|
||||
import("../../energy/strategies/energy-overview-view-strategy"),
|
||||
"energy-electricity": () =>
|
||||
import("../../energy/strategies/energy-electricity-view-strategy"),
|
||||
energy: () => import("../../energy/strategies/energy-view-strategy"),
|
||||
map: () => import("./map/map-view-strategy"),
|
||||
iframe: () => import("./iframe/iframe-view-strategy"),
|
||||
area: () => import("./areas/area-view-strategy"),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { AreaRegistryEntry } from "../../../../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../../data/floor_registry";
|
||||
|
||||
interface HomeStructure {
|
||||
floors: {
|
||||
id: string;
|
||||
areas: string[];
|
||||
}[];
|
||||
areas: string[];
|
||||
}
|
||||
|
||||
export const getHomeStructure = (
|
||||
floors: FloorRegistryEntry[],
|
||||
areas: AreaRegistryEntry[]
|
||||
): HomeStructure => {
|
||||
const floorAreas = new Map<string, string[]>();
|
||||
const unassignedAreas: string[] = [];
|
||||
|
||||
for (const area of areas) {
|
||||
if (area.floor_id) {
|
||||
if (!floorAreas.has(area.floor_id)) {
|
||||
floorAreas.set(area.floor_id, []);
|
||||
}
|
||||
floorAreas.get(area.floor_id)!.push(area.area_id);
|
||||
} else {
|
||||
unassignedAreas.push(area.area_id);
|
||||
}
|
||||
}
|
||||
|
||||
const homeStructure: HomeStructure = {
|
||||
floors: floors.map((floor) => ({
|
||||
id: floor.floor_id,
|
||||
areas: floorAreas.get(floor.floor_id) || [],
|
||||
})),
|
||||
areas: unassignedAreas,
|
||||
};
|
||||
|
||||
return homeStructure;
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { customElement } from "lit/decorators";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getAreas } from "../areas/helpers/areas-strategy-helper";
|
||||
import type { LovelaceStrategyEditor } from "../types";
|
||||
import {
|
||||
getSummaryLabel,
|
||||
@@ -45,7 +46,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
||||
};
|
||||
}
|
||||
|
||||
const areas = Object.values(hass.areas);
|
||||
const areas = getAreas(hass.areas);
|
||||
|
||||
const areaViews = areas.map<LovelaceViewRawConfig>((area) => {
|
||||
const path = `areas-${area.area_id}`;
|
||||
|
||||
@@ -22,8 +22,9 @@ import type {
|
||||
MarkdownCardConfig,
|
||||
WeatherForecastCardConfig,
|
||||
} from "../../cards/types";
|
||||
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
|
||||
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
||||
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
||||
import { getHomeStructure } from "./helpers/home-structure";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
|
||||
export interface HomeMainViewStrategyConfig {
|
||||
@@ -63,10 +64,10 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
||||
config: HomeMainViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
const floors = Object.values(hass.floors);
|
||||
const areas = getAreas(hass.areas);
|
||||
const floors = getFloors(hass.floors);
|
||||
|
||||
const home = getAreasFloorHierarchy(floors, areas);
|
||||
const home = getHomeStructure(floors, areas);
|
||||
|
||||
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { MediaControlCardConfig } from "../../cards/types";
|
||||
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
||||
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
|
||||
import { getHomeStructure } from "./helpers/home-structure";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
|
||||
export interface HomeMediaPlayersViewStrategyConfig {
|
||||
@@ -84,9 +85,9 @@ export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
|
||||
_config: HomeMediaPlayersViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
const floors = Object.values(hass.floors);
|
||||
const home = getAreasFloorHierarchy(floors, areas);
|
||||
const areas = getAreas(hass.areas);
|
||||
const floors = getFloors(hass.floors);
|
||||
const home = getHomeStructure(floors, areas);
|
||||
|
||||
const sections: LovelaceSectionRawConfig[] = [];
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user