mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-30 04:36:36 +00:00
Compare commits
214 Commits
20250626.0
...
dev
Author | SHA1 | Date | |
---|---|---|---|
![]() |
641e406502 | ||
![]() |
073ba22233 | ||
![]() |
6b4a4e6024 | ||
![]() |
7d8b418a81 | ||
![]() |
c14425b2d1 | ||
![]() |
4740a71bdd | ||
![]() |
6d0e0158ea | ||
![]() |
e966d6f4f4 | ||
![]() |
b99bb60cd0 | ||
![]() |
080c79234c | ||
![]() |
9ad887942e | ||
![]() |
27dfa514e7 | ||
![]() |
ab5c5389e8 | ||
![]() |
219679bce9 | ||
![]() |
aca4a1f86d | ||
![]() |
f428d6b3f2 | ||
![]() |
109c3e86d9 | ||
![]() |
e4b6c3fd4d | ||
![]() |
43f1d9be44 | ||
![]() |
5c405201b2 | ||
![]() |
9fc91fbbcc | ||
![]() |
d9e3f2c15f | ||
![]() |
3e2f5b0dd3 | ||
![]() |
aae1a3604c | ||
![]() |
e31b9c4264 | ||
![]() |
90a9dbafbf | ||
![]() |
7041557ee2 | ||
![]() |
3d6e5ef1f0 | ||
![]() |
d9bf605c3f | ||
![]() |
20dab92ad8 | ||
![]() |
98ed3bdd4d | ||
![]() |
f5bc6309ae | ||
![]() |
620ebd8a61 | ||
![]() |
ca30af5c8a | ||
![]() |
9d30ce348f | ||
![]() |
07c7b07362 | ||
![]() |
c13a80ce5e | ||
![]() |
13868478f7 | ||
![]() |
77aca59dda | ||
![]() |
b86605949b | ||
![]() |
cd19022e2e | ||
![]() |
03368c1859 | ||
![]() |
8e0ed288e1 | ||
![]() |
879e0ed3d5 | ||
![]() |
657275fd17 | ||
![]() |
3d1c908a01 | ||
![]() |
713e8e7b71 | ||
![]() |
9e597d22a5 | ||
![]() |
259e8a14da | ||
![]() |
4de4243b55 | ||
![]() |
a667cb627b | ||
![]() |
9461634670 | ||
![]() |
51b79b33fb | ||
![]() |
539884295b | ||
![]() |
e1192403d9 | ||
![]() |
f5c49c83a0 | ||
![]() |
faae7a2322 | ||
![]() |
f6aa55ef74 | ||
![]() |
1a0afc5079 | ||
![]() |
4a50ca4ea5 | ||
![]() |
6cb27ede09 | ||
![]() |
1b68c51a05 | ||
![]() |
5f2b11ca9f | ||
![]() |
ec6666a4ea | ||
![]() |
a10dbb64f0 | ||
![]() |
aa17be0e33 | ||
![]() |
a8919703ee | ||
![]() |
1c66a5e437 | ||
![]() |
767d785d04 | ||
![]() |
0839528e22 | ||
![]() |
1012245ef7 | ||
![]() |
0d6db8b834 | ||
![]() |
adea2efb01 | ||
![]() |
818914b837 | ||
![]() |
b207528ecf | ||
![]() |
039ef18d8c | ||
![]() |
db387834f2 | ||
![]() |
1b7d9f9e3b | ||
![]() |
ed8c9f5ce5 | ||
![]() |
c3bf1d8770 | ||
![]() |
7db4693082 | ||
![]() |
72a12a4ba4 | ||
![]() |
aee9e4b0a5 | ||
![]() |
55f6affc9e | ||
![]() |
1e5f2f7215 | ||
![]() |
da864d5bb7 | ||
![]() |
8d60f39cf4 | ||
![]() |
2045519814 | ||
![]() |
3f70e88a4f | ||
![]() |
6ed5fbe102 | ||
![]() |
4e72d5083d | ||
![]() |
f9e102e537 | ||
![]() |
e54363875b | ||
![]() |
1ce4e2a799 | ||
![]() |
80b86a89f0 | ||
![]() |
1a316d251e | ||
![]() |
a74f90b768 | ||
![]() |
fc6e457581 | ||
![]() |
ad7b8b66f2 | ||
![]() |
0714677a8a | ||
![]() |
0a946a5c43 | ||
![]() |
4930a8d88e | ||
![]() |
8b781cec8e | ||
![]() |
065c98c5d7 | ||
![]() |
69d8eeb7db | ||
![]() |
3b7d2869e5 | ||
![]() |
bcda5cd0cf | ||
![]() |
eeb64a25ff | ||
![]() |
9134132ba9 | ||
![]() |
1ded254e5a | ||
![]() |
fc104a7992 | ||
![]() |
e7e062a222 | ||
![]() |
5233086efb | ||
![]() |
8d95f0d95d | ||
![]() |
5cf8b39703 | ||
![]() |
15dabe372c | ||
![]() |
aab52a8bb2 | ||
![]() |
aa52825b40 | ||
![]() |
2809a306e6 | ||
![]() |
a6304d6284 | ||
![]() |
8e866e86d6 | ||
![]() |
2e8203f666 | ||
![]() |
b60f2e3201 | ||
![]() |
c5f57f436c | ||
![]() |
3bb930b906 | ||
![]() |
e75331e159 | ||
![]() |
d6b66a7145 | ||
![]() |
5c346798c8 | ||
![]() |
5ffe37407a | ||
![]() |
2b056c0434 | ||
![]() |
27b36707e5 | ||
![]() |
5760614b65 | ||
![]() |
3835912b01 | ||
![]() |
a385655c85 | ||
![]() |
e177012108 | ||
![]() |
cc3234ad8f | ||
![]() |
4d932f0b4a | ||
![]() |
257769cdc7 | ||
![]() |
6619f064eb | ||
![]() |
382a47a082 | ||
![]() |
ad4be75fe1 | ||
![]() |
d605b67b41 | ||
![]() |
dba6a3c756 | ||
![]() |
002e9ad071 | ||
![]() |
6e7874c2c9 | ||
![]() |
978f9b0f83 | ||
![]() |
2b88669a72 | ||
![]() |
252fd2bb6c | ||
![]() |
cc68a087a2 | ||
![]() |
3e1341a731 | ||
![]() |
6be25270fd | ||
![]() |
ce929aea46 | ||
![]() |
8853bf6ea2 | ||
![]() |
2241807745 | ||
![]() |
50d705c943 | ||
![]() |
eb111d3c32 | ||
![]() |
1e59f9f4be | ||
![]() |
523eb9522f | ||
![]() |
f6cb322819 | ||
![]() |
4f97756f4e | ||
![]() |
8644dd5271 | ||
![]() |
26d842f432 | ||
![]() |
ad4f14ffaf | ||
![]() |
948c858e78 | ||
![]() |
49099223d3 | ||
![]() |
0fbd430594 | ||
![]() |
8cc762d839 | ||
![]() |
89d9dd2893 | ||
![]() |
b7d1ce1c37 | ||
![]() |
869d10ca3f | ||
![]() |
f338089148 | ||
![]() |
06b0f9fcaf | ||
![]() |
7ad07e4c55 | ||
![]() |
ad65600d11 | ||
![]() |
e91d907e56 | ||
![]() |
b35a1fc9e0 | ||
![]() |
dd18ad96f3 | ||
![]() |
62eec56e5f | ||
![]() |
7187e25cad | ||
![]() |
6d9e6a616d | ||
![]() |
44d87e3c66 | ||
![]() |
085e2460bc | ||
![]() |
1f8a9e4caf | ||
![]() |
f08877437e | ||
![]() |
6690d1ef22 | ||
![]() |
9d8a5b366e | ||
![]() |
22c798c9d6 | ||
![]() |
8aabb1f32f | ||
![]() |
33d5cecc85 | ||
![]() |
7693a4dc24 | ||
![]() |
9ec38c7dd9 | ||
![]() |
e8cb85f7ff | ||
![]() |
ef964a2717 | ||
![]() |
369881f8a6 | ||
![]() |
68e22d23f1 | ||
![]() |
696ba69a9e | ||
![]() |
e2ab52e10e | ||
![]() |
b154bc1502 | ||
![]() |
a952b880d8 | ||
![]() |
018aceb542 | ||
![]() |
2fb86f118e | ||
![]() |
6c8caccfec | ||
![]() |
3dd3a80054 | ||
![]() |
675310afdf | ||
![]() |
f5df91d4c7 | ||
![]() |
d8ab9b73ba | ||
![]() |
89ab0b4a3d | ||
![]() |
dd4cb1df72 | ||
![]() |
b81cd37776 | ||
![]() |
34112e7446 | ||
![]() |
2dee45b465 | ||
![]() |
10eb0a8b87 | ||
![]() |
551035238f | ||
![]() |
eb9359e9e1 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -11,7 +11,7 @@ body:
|
||||
|
||||
**Please do not report issues for custom cards.**
|
||||
|
||||
[fr]: https://github.com/home-assistant/frontend/discussions
|
||||
[fr]: https://github.com/orgs/home-assistant/discussions
|
||||
[releases]: https://github.com/home-assistant/home-assistant/releases
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,7 +1,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Request a feature for the UI / Dashboards
|
||||
url: https://github.com/home-assistant/frontend/discussions/category_choices
|
||||
url: https://github.com/orgs/home-assistant/discussions
|
||||
about: Request a new feature for the Home Assistant frontend.
|
||||
- name: Report a bug that is NOT related to the UI / Dashboards
|
||||
url: https://github.com/home-assistant/core/issues
|
||||
|
53
.github/ISSUE_TEMPLATE/task.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/task.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
name: Task
|
||||
description: For staff only - Create a task
|
||||
type: Task
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## ⚠️ RESTRICTED ACCESS
|
||||
|
||||
**This form is restricted to Open Home Foundation staff and authorized contributors only.**
|
||||
|
||||
If you are a community member wanting to contribute, please:
|
||||
- For bug reports: Use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)
|
||||
- For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)
|
||||
|
||||
---
|
||||
|
||||
### For authorized contributors
|
||||
|
||||
Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
Provide a clear and detailed description of the task that needs to be accomplished.
|
||||
|
||||
Be specific about what needs to be done, why it's important, and any constraints or requirements.
|
||||
placeholder: |
|
||||
Describe the task, including:
|
||||
- What needs to be done
|
||||
- Why this task is needed
|
||||
- Expected outcome
|
||||
- Any constraints or requirements
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional_context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
Any additional information, links, research, or context that would be helpful.
|
||||
|
||||
Include links to related issues, research, prototypes, roadmap opportunities etc.
|
||||
placeholder: |
|
||||
- Roadmap opportunity: [link]
|
||||
- Epic: [link]
|
||||
- Feature request: [link]
|
||||
- Technical design documents: [link]
|
||||
- Prototype/mockup: [link]
|
||||
- Dependencies: [links]
|
||||
validations:
|
||||
required: false
|
592
.github/copilot-instructions.md
vendored
Normal file
592
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,592 @@
|
||||
# GitHub Copilot & Claude Code Instructions
|
||||
|
||||
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.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Reference](#quick-reference)
|
||||
- [Core Architecture](#core-architecture)
|
||||
- [Development Standards](#development-standards)
|
||||
- [Component Library](#component-library)
|
||||
- [Common Patterns](#common-patterns)
|
||||
- [Text and Copy Guidelines](#text-and-copy-guidelines)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Review Guidelines](#review-guidelines)
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
yarn lint # ESLint + Prettier + TypeScript + Lit
|
||||
yarn format # Auto-fix ESLint + Prettier
|
||||
yarn lint:types # TypeScript compiler
|
||||
yarn test # Vitest
|
||||
script/develop # Development server
|
||||
```
|
||||
|
||||
### Component Prefixes
|
||||
|
||||
- `ha-` - Home Assistant components
|
||||
- `hui-` - Lovelace UI components
|
||||
- `dialog-` - Dialog components
|
||||
|
||||
### Import Patterns
|
||||
|
||||
```typescript
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-alert-dialog";
|
||||
```
|
||||
|
||||
## Core Architecture
|
||||
|
||||
The Home Assistant frontend is a modern web application that:
|
||||
|
||||
- Uses Web Components (custom elements) built with Lit framework
|
||||
- Is written entirely in TypeScript with strict type checking
|
||||
- Communicates with the backend via WebSocket API
|
||||
- Provides comprehensive theming and internationalization
|
||||
|
||||
## Development Standards
|
||||
|
||||
### Code Quality Requirements
|
||||
|
||||
**Linting and Formatting (Enforced by Tools)**
|
||||
|
||||
- ESLint config extends Airbnb, TypeScript strict, Lit, Web Components, Accessibility
|
||||
- Prettier with ES5 trailing commas enforced
|
||||
- No console statements (`no-console: "error"`) - use proper logging
|
||||
- Import organization: No unused imports, consistent type imports
|
||||
|
||||
**Naming Conventions**
|
||||
|
||||
- PascalCase for types and classes
|
||||
- camelCase for variables, methods
|
||||
- Private methods require leading underscore
|
||||
- Public methods forbid leading underscore
|
||||
|
||||
### TypeScript Usage
|
||||
|
||||
- **Always use strict TypeScript**: Enable all strict flags, avoid `any` types
|
||||
- **Proper type imports**: Use `import type` for type-only imports
|
||||
- **Define interfaces**: Create proper interfaces for data structures
|
||||
- **Type component properties**: All Lit properties must be properly typed
|
||||
- **No unused variables**: Prefix with `_` if intentionally unused
|
||||
- **Consistent imports**: Use `@typescript-eslint/consistent-type-imports`
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface EntityConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
@property({ type: Object })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
// Bad
|
||||
@property()
|
||||
hass: any;
|
||||
```
|
||||
|
||||
### Web Components with Lit
|
||||
|
||||
- **Use Lit 3.x patterns**: Follow modern Lit practices
|
||||
- **Extend appropriate base classes**: Use `LitElement`, `SubscribeMixin`, or other mixins as needed
|
||||
- **Define custom element names**: Use `ha-` prefix for components
|
||||
|
||||
```typescript
|
||||
@customElement("ha-my-component")
|
||||
export class HaMyComponent extends LitElement {
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
private _config?: MyComponentConfig;
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div>Content</div>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component Guidelines
|
||||
|
||||
- **Use composition**: Prefer composition over inheritance
|
||||
- **Lazy load panels**: Heavy panels should be dynamically imported
|
||||
- **Optimize renders**: Use `@state()` for internal state, `@property()` for public API
|
||||
- **Handle loading states**: Always show appropriate loading indicators
|
||||
- **Support themes**: Use CSS custom properties from theme
|
||||
|
||||
### Data Management
|
||||
|
||||
- **Use WebSocket API**: All backend communication via home-assistant-js-websocket
|
||||
- **Cache appropriately**: Use collections and caching for frequently accessed data
|
||||
- **Handle errors gracefully**: All API calls should have error handling
|
||||
- **Update real-time**: Subscribe to state changes for live updates
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
try {
|
||||
const result = await fetchEntityRegistry(this.hass.connection);
|
||||
this._processResult(result);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
text: `Failed to load: ${err.message}`,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Styling Guidelines
|
||||
|
||||
- **Use CSS custom properties**: Leverage the theme system
|
||||
- **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
|
||||
|
||||
```typescript
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
--spacing: 16px;
|
||||
padding: var(--spacing);
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
:host {
|
||||
--spacing: 8px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Best Practices
|
||||
|
||||
- **Code split**: Split code at the panel/dialog level
|
||||
- **Lazy load**: Use dynamic imports for heavy components
|
||||
- **Optimize bundle**: Keep initial bundle size minimal
|
||||
- **Use virtual scrolling**: For long lists, implement virtual scrolling
|
||||
- **Memoize computations**: Cache expensive calculations
|
||||
|
||||
### Testing Requirements
|
||||
|
||||
- **Write tests**: Add tests for data processing and utilities
|
||||
- **Test with Vitest**: Use the established test framework
|
||||
- **Mock appropriately**: Mock WebSocket connections and API calls
|
||||
- **Test accessibility**: Ensure components are accessible
|
||||
|
||||
## Component Library
|
||||
|
||||
### Dialog Components
|
||||
|
||||
**Available Dialog Types:**
|
||||
|
||||
- `ha-md-dialog` - Preferred for new code (Material Design 3)
|
||||
- `ha-dialog` - Legacy component still widely used
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
|
||||
```typescript
|
||||
fireEvent(this, "show-dialog", {
|
||||
dialogTag: "dialog-example",
|
||||
dialogImport: () => import("./dialog-example"),
|
||||
dialogParams: { title: "Example", data: someData },
|
||||
});
|
||||
```
|
||||
|
||||
**Dialog Implementation Requirements:**
|
||||
|
||||
- Implement `HassDialog<T>` interface
|
||||
- Use `createCloseHeading()` for standard headers
|
||||
- Import `haStyleDialog` for consistent styling
|
||||
- Return `nothing` when no params (loading state)
|
||||
- Fire `dialog-closed` event when closing
|
||||
- Add `dialogInitialFocus` for accessibility
|
||||
|
||||
````
|
||||
|
||||
### 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
|
||||
- Use `dialogInitialFocus` in dialogs
|
||||
- Use `computeLabel`, `computeError`, `computeHelper` for translations
|
||||
|
||||
```typescript
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._data}
|
||||
.schema=${this._schema}
|
||||
.error=${this._errors}
|
||||
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
````
|
||||
|
||||
### Alert Component (ha-alert)
|
||||
|
||||
- Types: `error`, `warning`, `info`, `success`
|
||||
- Properties: `title`, `alert-type`, `dismissable`, `icon`, `action`, `rtl`
|
||||
- Content announced by screen readers when dynamically displayed
|
||||
|
||||
```html
|
||||
<ha-alert alert-type="error">Error message</ha-alert>
|
||||
<ha-alert alert-type="warning" title="Warning">Description</ha-alert>
|
||||
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Creating a Panel
|
||||
|
||||
```typescript
|
||||
@customElement("ha-panel-myfeature")
|
||||
export class HaPanelMyFeature extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
narrow!: boolean;
|
||||
|
||||
@property()
|
||||
route!: Route;
|
||||
|
||||
hassSubscribe() {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Creating a Dialog
|
||||
|
||||
```typescript
|
||||
@customElement("dialog-my-feature")
|
||||
export class DialogMyFeature
|
||||
extends LitElement
|
||||
implements HassDialog<MyDialogParams>
|
||||
{
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
private _params?: MyDialogParams;
|
||||
|
||||
public async showDialog(params: MyDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, this._params.title)}
|
||||
>
|
||||
<!-- Dialog content -->
|
||||
<ha-button @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>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [haStyleDialog, css``];
|
||||
}
|
||||
```
|
||||
|
||||
### Dialog Design Guidelines
|
||||
|
||||
- Max width: 560px (Alert/confirmation: 320px fixed width)
|
||||
- Close X-icon on top left (all screen sizes)
|
||||
- Submit button grouped with cancel at bottom right
|
||||
- Keep button labels short: "Save", "Delete", "Enable"
|
||||
- Destructive actions use red warning button
|
||||
- Always use a title (best practice)
|
||||
- Strive for minimalism
|
||||
|
||||
#### Creating a Lovelace Card
|
||||
|
||||
**Purpose**: Cards allow users to tell different stories about their house (based on gallery)
|
||||
|
||||
```typescript
|
||||
@customElement("hui-my-card")
|
||||
export class HuiMyCard extends LitElement implements LovelaceCard {
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
private _config?: MyCardConfig;
|
||||
|
||||
public setConfig(config: MyCardConfig): void {
|
||||
if (!config.entity) {
|
||||
throw new Error("Entity required");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 3; // Height in grid units
|
||||
}
|
||||
|
||||
// Optional: Editor for card configuration
|
||||
public static getConfigElement(): LovelaceCardEditor {
|
||||
return document.createElement("hui-my-card-editor");
|
||||
}
|
||||
|
||||
// Optional: Stub config for card picker
|
||||
public static getStubConfig(): object {
|
||||
return { entity: "" };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Card Guidelines:**
|
||||
|
||||
- Cards are highly customizable for different households
|
||||
- Implement `LovelaceCard` interface with `setConfig()` and `getCardSize()`
|
||||
- Use proper error handling in `setConfig()`
|
||||
- Consider all possible states (loading, error, unavailable)
|
||||
- Support different entity types and states
|
||||
- Follow responsive design principles
|
||||
- Add configuration editor when needed
|
||||
|
||||
### Internationalization
|
||||
|
||||
- **Use localize**: Always use the localization system
|
||||
- **Add translation keys**: Add keys to src/translations/en.json
|
||||
- **Support placeholders**: Use proper placeholder syntax
|
||||
|
||||
```typescript
|
||||
this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
count: 5,
|
||||
});
|
||||
```
|
||||
|
||||
### Accessibility
|
||||
|
||||
- **ARIA labels**: Add appropriate ARIA labels
|
||||
- **Keyboard navigation**: Ensure all interactions work with keyboard
|
||||
- **Screen reader support**: Test with screen readers
|
||||
- **Color contrast**: Meet WCAG AA standards
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Setup and Commands
|
||||
|
||||
1. **Setup**: `script/setup` - Install dependencies
|
||||
2. **Develop**: `script/develop` - Development server
|
||||
3. **Lint**: `yarn lint` - Run all linting before committing
|
||||
4. **Test**: `yarn test` - Add and run tests
|
||||
5. **Build**: `script/build_frontend` - Test production build
|
||||
|
||||
### Common Pitfalls to Avoid
|
||||
|
||||
- Don't use `querySelector` - Use refs or component properties
|
||||
- Don't manipulate DOM directly - Let Lit handle rendering
|
||||
- Don't use global styles - Scope styles to components
|
||||
- Don't block the main thread - Use web workers for heavy computation
|
||||
- Don't ignore TypeScript errors - Fix all type issues
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
- Sanitize HTML - Never use `unsafeHTML` with user content
|
||||
- Validate inputs - Always validate user inputs
|
||||
- Use HTTPS - All external resources must use HTTPS
|
||||
- CSP compliance - Ensure code works with Content Security Policy
|
||||
|
||||
### Text and Copy Guidelines
|
||||
|
||||
#### Terminology Standards
|
||||
|
||||
**Delete vs Remove** (Based on gallery/src/pages/Text/remove-delete-add-create.markdown)
|
||||
|
||||
- **Use "Remove"** for actions that can be restored or reapplied:
|
||||
- Removing a user's permission
|
||||
- Removing a user from a group
|
||||
- Removing links between items
|
||||
- Removing a widget from dashboard
|
||||
- Removing an item from a cart
|
||||
- **Use "Delete"** for permanent, non-recoverable actions:
|
||||
- Deleting a field
|
||||
- Deleting a value in a field
|
||||
- Deleting a task
|
||||
- Deleting a group
|
||||
- Deleting a permission
|
||||
- Deleting a calendar event
|
||||
|
||||
**Create vs Add** (Create pairs with Delete, Add pairs with Remove)
|
||||
|
||||
- **Use "Add"** for already-existing items:
|
||||
- Adding a permission to a user
|
||||
- Adding a user to a group
|
||||
- Adding links between items
|
||||
- Adding a widget to dashboard
|
||||
- Adding an item to a cart
|
||||
- **Use "Create"** for something made from scratch:
|
||||
- Creating a new field
|
||||
- Creating a new task
|
||||
- Creating a new group
|
||||
- Creating a new permission
|
||||
- Creating a new calendar event
|
||||
|
||||
#### Writing Style (Consistent with Home Assistant Documentation)
|
||||
|
||||
- **Use American English**: Standard spelling and terminology
|
||||
- **Friendly, informational tone**: Be inspiring, personal, comforting, engaging
|
||||
- **Address users directly**: Use "you" and "your"
|
||||
- **Be inclusive**: Objective, non-discriminatory language
|
||||
- **Be concise**: Use clear, direct language
|
||||
- **Be consistent**: Follow established terminology patterns
|
||||
- **Use active voice**: "Delete the automation" not "The automation should be deleted"
|
||||
- **Avoid jargon**: Use terms familiar to home automation users
|
||||
|
||||
#### Language Standards
|
||||
|
||||
- **Always use "Home Assistant"** in full, never "HA" or "HASS"
|
||||
- **Avoid abbreviations**: Spell out terms when possible
|
||||
- **Use sentence case everywhere**: Titles, headings, buttons, labels, UI elements
|
||||
- ✅ "Create new automation"
|
||||
- ❌ "Create New Automation"
|
||||
- ✅ "Device settings"
|
||||
- ❌ "Device Settings"
|
||||
- **Oxford comma**: Use in lists (item 1, item 2, and item 3)
|
||||
- **Replace Latin terms**: Use "like" instead of "e.g.", "for example" instead of "i.e."
|
||||
- **Avoid CAPS for emphasis**: Use bold or italics instead
|
||||
- **Write for all skill levels**: Both technical and non-technical users
|
||||
|
||||
#### Key Terminology
|
||||
|
||||
- **"add-on"** (hyphenated, not "addon")
|
||||
- **"integration"** (preferred over "component")
|
||||
- **Technical terms**: Use lowercase (automation, entity, device, service)
|
||||
|
||||
#### Translation Considerations
|
||||
|
||||
- **Add translation keys**: All user-facing text must be translatable
|
||||
- **Use placeholders**: Support dynamic content in translations
|
||||
- **Keep context**: Provide enough context for translators
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
this.hass.localize("ui.panel.config.automation.delete_confirm", {
|
||||
name: automation.alias,
|
||||
});
|
||||
|
||||
// Bad - hardcoded text
|
||||
("Are you sure you want to delete this automation?");
|
||||
```
|
||||
|
||||
### Common Review Issues (From PR Analysis)
|
||||
|
||||
#### User Experience and Accessibility
|
||||
|
||||
- **Form validation**: Always provide proper field labels and validation feedback
|
||||
- **Form accessibility**: Prevent password managers from incorrectly identifying fields
|
||||
- **Loading states**: Show clear progress indicators during async operations
|
||||
- **Error handling**: Display meaningful error messages when operations fail
|
||||
- **Mobile responsiveness**: Ensure components work well on small screens
|
||||
- **Hit targets**: Make clickable areas large enough for touch interaction
|
||||
- **Visual feedback**: Provide clear indication of interactive states
|
||||
|
||||
#### Dialog and Modal Patterns
|
||||
|
||||
- **Dialog width constraints**: Respect minimum and maximum width requirements
|
||||
- **Interview progress**: Show clear progress for multi-step operations
|
||||
- **State persistence**: Handle dialog state properly during background operations
|
||||
- **Cancel behavior**: Ensure cancel/close buttons work consistently
|
||||
- **Form prefilling**: Use smart defaults but allow user override
|
||||
|
||||
#### Component Design Patterns
|
||||
|
||||
- **Terminology consistency**: Use "Join"/"Apply" instead of "Group" when appropriate
|
||||
- **Visual hierarchy**: Ensure proper font sizes and spacing ratios
|
||||
- **Grid alignment**: Components should align to the design grid system
|
||||
- **Badge placement**: Position badges and indicators consistently
|
||||
- **Color theming**: Respect theme variables and design system colors
|
||||
|
||||
#### Code Quality Issues
|
||||
|
||||
- **Null checking**: Always check if entities exist before accessing properties
|
||||
- **TypeScript safety**: Handle potentially undefined array/object access
|
||||
- **Import organization**: Remove unused imports and use proper type imports
|
||||
- **Event handling**: Properly subscribe and unsubscribe from events
|
||||
- **Memory leaks**: Clean up subscriptions and event listeners
|
||||
|
||||
#### Configuration and Props
|
||||
|
||||
- **Optional parameters**: Make configuration fields optional when sensible
|
||||
- **Smart defaults**: Provide reasonable default values
|
||||
- **Future extensibility**: Design APIs that can be extended later
|
||||
- **Validation**: Validate configuration before applying changes
|
||||
|
||||
## Review Guidelines
|
||||
|
||||
### Core Requirements Checklist
|
||||
|
||||
- [ ] TypeScript strict mode passes (`yarn lint:types`)
|
||||
- [ ] No ESLint errors or warnings (`yarn lint:eslint`)
|
||||
- [ ] Prettier formatting applied (`yarn lint:prettier`)
|
||||
- [ ] Lit analyzer passes (`yarn lint:lit`)
|
||||
- [ ] Component follows Lit best practices
|
||||
- [ ] Proper error handling implemented
|
||||
- [ ] Loading states handled
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Theme variables used
|
||||
- [ ] Translations added
|
||||
- [ ] Accessible to screen readers
|
||||
- [ ] Tests added (where applicable)
|
||||
- [ ] No console statements (use proper logging)
|
||||
- [ ] Unused imports removed
|
||||
- [ ] Proper naming conventions
|
||||
|
||||
### Text and Copy Checklist
|
||||
|
||||
- [ ] Follows terminology guidelines (Delete vs Remove, Create vs Add)
|
||||
- [ ] Localization keys added for all user-facing text
|
||||
- [ ] Uses "Home Assistant" (never "HA" or "HASS")
|
||||
- [ ] Sentence case for ALL text (titles, headings, buttons, labels)
|
||||
- [ ] American English spelling
|
||||
- [ ] Friendly, informational tone
|
||||
- [ ] Avoids abbreviations and jargon
|
||||
- [ ] Correct terminology (add-on not addon, integration not component)
|
||||
|
||||
### Component-Specific Checks
|
||||
|
||||
- [ ] Dialogs implement HassDialog interface
|
||||
- [ ] Dialog styling uses haStyleDialog
|
||||
- [ ] Dialog accessibility includes dialogInitialFocus
|
||||
- [ ] ha-alert used correctly for messages
|
||||
- [ ] ha-form uses proper schema structure
|
||||
- [ ] Components handle all states (loading, error, unavailable)
|
||||
- [ ] Entity existence checked before property access
|
||||
- [ ] Event subscriptions properly cleaned up
|
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@v3.0.0
|
||||
uses: relative-ci/agent-action@v3.0.1
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
58
.github/workflows/restrict-task-creation.yml
vendored
Normal file
58
.github/workflows/restrict-task-creation.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
name: Restrict task creation
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-authorization:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.issue_type == 'Task'
|
||||
steps:
|
||||
- name: Check if user is authorized
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issueAuthor = context.payload.issue.user.login;
|
||||
|
||||
// Check if user is an organization member
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: 'home-assistant',
|
||||
username: issueAuthor
|
||||
});
|
||||
console.log(`✅ ${issueAuthor} is an organization member`);
|
||||
return; // Authorized
|
||||
} catch (error) {
|
||||
console.log(`❌ ${issueAuthor} is not authorized to create Task issues`);
|
||||
}
|
||||
|
||||
// Close the issue with a comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` +
|
||||
`Task issues are restricted to Open Home Foundation staff and authorized contributors.\n\n` +
|
||||
`If you would like to:\n` +
|
||||
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)\n` +
|
||||
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
|
||||
`If you believe you should have access to create Task issues, please contact the maintainers.`
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
// Add a label to indicate this was auto-closed
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['auto-closed']
|
||||
});
|
1
.github/workflows/translations.yaml
vendored
1
.github/workflows/translations.yaml
vendored
@ -1,6 +1,7 @@
|
||||
name: Translations
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -53,3 +53,7 @@ src/cast/dev_const.ts
|
||||
|
||||
# test coverage
|
||||
test/coverage/
|
||||
|
||||
# AI tooling
|
||||
.claude
|
||||
|
||||
|
@ -135,7 +135,7 @@ export class DemoHaControlSelect extends LitElement {
|
||||
.options=${options}
|
||||
class=${ifDefined(config.class)}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
aria-labelledby=${id}
|
||||
.label=${label}
|
||||
?disabled=${config.disabled}
|
||||
>
|
||||
</ha-control-select>
|
||||
@ -156,7 +156,7 @@ export class DemoHaControlSelect extends LitElement {
|
||||
vertical
|
||||
class=${ifDefined(config.class)}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
aria-labelledby=${id}
|
||||
.label=${label}
|
||||
?disabled=${config.disabled}
|
||||
>
|
||||
</ha-control-select>
|
||||
|
@ -97,7 +97,7 @@ export class DemoHaBarSlider extends LitElement {
|
||||
class=${ifDefined(config.class)}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
@slider-moved=${this.handleSliderMoved}
|
||||
aria-labelledby=${id}
|
||||
.label=${label}
|
||||
.unit=${config.unit}
|
||||
>
|
||||
</ha-control-slider>
|
||||
@ -119,7 +119,7 @@ export class DemoHaBarSlider extends LitElement {
|
||||
class=${ifDefined(config.class)}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
@slider-moved=${this.handleSliderMoved}
|
||||
aria-label=${label}
|
||||
.label=${label}
|
||||
.unit=${config.unit}
|
||||
>
|
||||
</ha-control-slider>
|
||||
|
@ -63,7 +63,7 @@ export class DemoHaControlSwitch extends LitElement {
|
||||
@change=${this.handleValueChanged}
|
||||
.pathOn=${mdiLightbulb}
|
||||
.pathOff=${mdiLightbulbOff}
|
||||
aria-labelledby=${id}
|
||||
.label=${label}
|
||||
?disabled=${config.disabled}
|
||||
?reversed=${config.reversed}
|
||||
>
|
||||
@ -84,7 +84,7 @@ export class DemoHaControlSwitch extends LitElement {
|
||||
vertical
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
aria-label=${label}
|
||||
.label=${label}
|
||||
.pathOn=${mdiGarageOpen}
|
||||
.pathOff=${mdiGarage}
|
||||
?disabled=${config.disabled}
|
||||
|
@ -249,6 +249,8 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
await addStoreRepository(this.hass, input.value);
|
||||
await this._loadData();
|
||||
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "store" });
|
||||
|
||||
input.value = "";
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
@ -261,6 +263,8 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
try {
|
||||
await removeStoreRepository(this.hass, slug);
|
||||
await this._loadData();
|
||||
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "store" });
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
|
95
package.json
95
package.json
@ -26,15 +26,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.27.6",
|
||||
"@babel/runtime": "7.28.2",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.1",
|
||||
"@codemirror/language": "6.11.1",
|
||||
"@codemirror/language": "6.11.2",
|
||||
"@codemirror/legacy-modes": "6.5.1",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.37.2",
|
||||
"@codemirror/view": "6.38.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
||||
"@formatjs/intl-displaynames": "6.8.11",
|
||||
@ -45,18 +45,18 @@
|
||||
"@formatjs/intl-numberformat": "8.15.4",
|
||||
"@formatjs/intl-pluralrules": "5.4.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.11",
|
||||
"@fullcalendar/core": "6.1.17",
|
||||
"@fullcalendar/daygrid": "6.1.17",
|
||||
"@fullcalendar/interaction": "6.1.17",
|
||||
"@fullcalendar/list": "6.1.17",
|
||||
"@fullcalendar/luxon3": "6.1.17",
|
||||
"@fullcalendar/timegrid": "6.1.17",
|
||||
"@fullcalendar/core": "6.1.18",
|
||||
"@fullcalendar/daygrid": "6.1.18",
|
||||
"@fullcalendar/interaction": "6.1.18",
|
||||
"@fullcalendar/list": "6.1.18",
|
||||
"@fullcalendar/luxon3": "6.1.18",
|
||||
"@fullcalendar/timegrid": "6.1.18",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lit-labs/motion": "1.0.8",
|
||||
"@lit-labs/observers": "2.0.5",
|
||||
"@lit-labs/virtualizer": "2.1.0",
|
||||
"@lit/context": "1.1.5",
|
||||
"@lit/reactive-element": "2.1.0",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
"@lit-labs/virtualizer": "2.1.1",
|
||||
"@lit/context": "1.1.6",
|
||||
"@lit/reactive-element": "2.1.1",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
@ -89,17 +89,17 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.8.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.8.0",
|
||||
"@vaadin/vaadin-themable-mixin": "24.8.0",
|
||||
"@vaadin/combo-box": "24.7.9",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.9",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "3.0.4",
|
||||
"barcode-detector": "3.0.5",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.43.0",
|
||||
"core-js": "3.44.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.2.0",
|
||||
@ -111,7 +111,7 @@
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "1.6.5",
|
||||
"hls.js": "1.6.7",
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.16",
|
||||
@ -119,10 +119,10 @@
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "3.3.0",
|
||||
"lit-html": "3.3.0",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "15.0.12",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"luxon": "3.7.1",
|
||||
"marked": "16.1.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
@ -135,8 +135,7 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.3",
|
||||
"vis-data": "7.1.9",
|
||||
"ua-parser-js": "2.0.4",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@ -149,26 +148,25 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.27.4",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.4",
|
||||
"@babel/plugin-transform-runtime": "7.27.4",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.20.2",
|
||||
"@lokalise/node-api": "14.8.0",
|
||||
"@babel/core": "7.28.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.0",
|
||||
"@babel/preset-env": "7.28.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.1",
|
||||
"@lokalise/node-api": "15.0.0",
|
||||
"@octokit/auth-oauth-device": "8.0.1",
|
||||
"@octokit/plugin-retry": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@rsdoctor/rspack-plugin": "1.1.4",
|
||||
"@rspack/cli": "1.3.12",
|
||||
"@rspack/core": "1.3.12",
|
||||
"@rsdoctor/rspack-plugin": "1.1.10",
|
||||
"@rspack/cli": "1.4.10",
|
||||
"@rspack/core": "1.4.10",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.18",
|
||||
"@types/leaflet": "1.9.20",
|
||||
"@types/leaflet-draw": "1.0.12",
|
||||
"@types/leaflet.markercluster": "1.5.5",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
@ -184,13 +182,13 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.29.0",
|
||||
"eslint": "9.32.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-lit": "2.1.1",
|
||||
"eslint-plugin-lit-a11y": "5.0.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "3.0.1",
|
||||
"fancy-log": "2.0.0",
|
||||
@ -199,7 +197,7 @@
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "26.1.0",
|
||||
@ -210,7 +208,7 @@
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.5.3",
|
||||
"prettier": "3.6.2",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "21.0.0",
|
||||
@ -218,7 +216,7 @@
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.34.1",
|
||||
"typescript-eslint": "8.38.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
@ -227,14 +225,15 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@material/mwc-button@^0.25.3": "^0.27.0",
|
||||
"lit": "3.3.0",
|
||||
"lit-html": "3.3.0",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.0",
|
||||
"@fullcalendar/daygrid": "6.1.17",
|
||||
"globals": "16.2.0",
|
||||
"@lit/reactive-element": "2.1.1",
|
||||
"@fullcalendar/daygrid": "6.1.18",
|
||||
"globals": "16.3.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"
|
||||
"@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",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.9"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2"
|
||||
}
|
||||
|
@ -1,35 +1,31 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1738_5532)">
|
||||
<g clip-path="url(#clip0_3969_57097)">
|
||||
<path d="M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V4C20 6.20914 18.2091 8 16 8H4C1.79086 8 0 6.20914 0 4V4Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M40 4C40 1.79086 41.7909 0 44 0V0C46.2091 0 48 1.79086 48 4V4C48 6.20914 46.2091 8 44 8V8C41.7909 8 40 6.20914 40 4V4Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M0 20C0 15.5817 3.58172 12 8 12H40C44.4183 12 48 15.5817 48 20V64C48 68.4183 44.4183 72 40 72H8C3.58172 72 0 68.4183 0 64V20Z" fill="#1C1C1C"/>
|
||||
<path d="M8 12.5H40C44.1421 12.5 47.5 15.8579 47.5 20V64C47.5 68.1421 44.1421 71.5 40 71.5H8C3.85787 71.5 0.5 68.1421 0.5 64V20L0.509766 19.6143C0.704063 15.7792 3.77915 12.7041 7.61426 12.5098L8 12.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M18.9844 41.0156C18.9844 40.0781 18.7031 39.2656 18.1406 38.5781C17.5781 37.8594 16.8594 37.375 15.9844 37.125V36C15.9844 35.4375 16.125 34.9375 16.4062 34.5C16.6875 34.0312 17.0469 33.6719 17.4844 33.4219C17.9531 33.1406 18.4531 33 18.9844 33H29.0156C29.5469 33 30.0312 33.1406 30.4688 33.4219C30.9375 33.6719 31.3125 34.0312 31.5938 34.5C31.875 34.9375 32.0156 35.4375 32.0156 36V37.125C31.1406 37.375 30.4219 37.8594 29.8594 38.5781C29.2969 39.2656 29.0156 40.0781 29.0156 41.0156V42.9844H18.9844V41.0156ZM33 39C33.5625 39 34.0312 39.2031 34.4062 39.6094C34.8125 39.9844 35.0156 40.4531 35.0156 41.0156V45.9844C35.0156 46.5469 34.875 47.0625 34.5938 47.5312C34.3125 47.9688 33.9375 48.3281 33.4688 48.6094C33.0312 48.8594 32.5469 48.9844 32.0156 48.9844V50.0156C32.0156 50.2656 31.9062 50.5 31.6875 50.7188C31.5 50.9062 31.2656 51 30.9844 51C30.7344 51 30.5 50.9062 30.2812 50.7188C30.0938 50.5 30 50.2656 30 50.0156V48.9844H18V50.0156C18 50.2656 17.8906 50.5 17.6719 50.7188C17.4844 50.9062 17.2656 51 17.0156 51C16.7344 51 16.4844 50.9062 16.2656 50.7188C16.0781 50.5 15.9844 50.2656 15.9844 50.0156V48.9844C15.4531 48.9844 14.9531 48.8594 14.4844 48.6094C14.0469 48.3281 13.6875 47.9688 13.4062 47.5312C13.125 47.0625 12.9844 46.5469 12.9844 45.9844V41.0156C12.9844 40.4531 13.1719 39.9844 13.5469 39.6094C13.9531 39.2031 14.4375 39 15 39C15.5625 39 16.0312 39.2031 16.4062 39.6094C16.8125 39.9844 17.0156 40.4531 17.0156 41.0156V45H30.9844V41.0156C30.9844 40.4531 31.1719 39.9844 31.5469 39.6094C31.9531 39.2031 32.4375 39 33 39Z" fill="#03A9F4"/>
|
||||
<path d="M56 4C56 1.79086 57.7909 0 60 0H72C74.2091 0 76 1.79086 76 4V4C76 6.20914 74.2091 8 72 8H60C57.7909 8 56 6.20914 56 4V4Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M96 4C96 1.79086 97.7909 0 100 0V0C102.209 0 104 1.79086 104 4V4C104 6.20914 102.209 8 100 8V8C97.7909 8 96 6.20914 96 4V4Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M56 20C56 15.5817 59.5817 12 64 12H96C100.418 12 104 15.5817 104 20V64C104 68.4183 100.418 72 96 72H64C59.5817 72 56 68.4183 56 64V20Z" fill="#1C1C1C"/>
|
||||
<path d="M64 12.5H96C100.142 12.5 103.5 15.8579 103.5 20V64C103.5 68.1421 100.142 71.5 96 71.5H64C59.8579 71.5 56.5 68.1421 56.5 64V20L56.5098 19.6143C56.7041 15.7792 59.7792 12.7041 63.6143 12.5098L64 12.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M86 39.9844H89.9844V42H88.0156V50.0156H71.9844V42H70.0156V39.9844H74C73.4375 39.9844 72.9531 39.7969 72.5469 39.4219C72.1719 39.0156 71.9844 38.5469 71.9844 38.0156V33.9844H77.9844V38.0156C77.9844 38.5469 77.7812 39.0156 77.375 39.4219C77 39.7969 76.5469 39.9844 76.0156 39.9844H83.9844V36.9844C83.9844 36.7344 83.8906 36.5156 83.7031 36.3281C83.5156 36.1094 83.2812 36 83 36C82.7188 36 82.4844 36.1094 82.2969 36.3281C82.1094 36.5156 82.0156 36.7344 82.0156 36.9844H80C80 36.4531 80.125 35.9688 80.375 35.5312C80.6562 35.0625 81.0156 34.6875 81.4531 34.4062C81.9219 34.125 82.4375 33.9844 83 33.9844C83.5625 33.9844 84.0625 34.125 84.5 34.4062C84.9688 34.6875 85.3281 35.0625 85.5781 35.5312C85.8594 35.9688 86 36.4531 86 36.9844V39.9844ZM80.9844 48V42H79.0156V48H80.9844Z" fill="#03A9F4"/>
|
||||
<path d="M112 4C112 1.79086 113.791 0 116 0H128C130.209 0 132 1.79086 132 4V4C132 6.20914 130.209 8 128 8H116C113.791 8 112 6.20914 112 4V4Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M152 4C152 1.79086 153.791 0 156 0V0C158.209 0 160 1.79086 160 4V4C160 6.20914 158.209 8 156 8V8C153.791 8 152 6.20914 152 4V4Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M112 20C112 15.5817 115.582 12 120 12H152C156.418 12 160 15.5817 160 20V64C160 68.4183 156.418 72 152 72H120C115.582 72 112 68.4183 112 64V20Z" fill="#1C1C1C"/>
|
||||
<path d="M120 12.5H152C156.142 12.5 159.5 15.8579 159.5 20V64C159.5 68.1421 156.142 71.5 152 71.5H120C115.858 71.5 112.5 68.1421 112.5 64V20L112.51 19.6143C112.704 15.7792 115.779 12.7041 119.614 12.5098L120 12.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M142.984 36.9844C144.078 36.9844 145.016 37.3906 145.797 38.2031C146.609 38.9844 147.016 39.9219 147.016 41.0156V50.0156H145V47.0156H127V50.0156H124.984V35.0156H127V44.0156H135.016V36.9844H142.984ZM133.094 42.0938C132.5 42.6875 131.797 42.9844 130.984 42.9844C130.172 42.9844 129.469 42.6875 128.875 42.0938C128.281 41.5 127.984 40.7969 127.984 39.9844C127.984 39.1719 128.281 38.4688 128.875 37.875C129.469 37.2812 130.172 36.9844 130.984 36.9844C131.797 36.9844 132.5 37.2812 133.094 37.875C133.688 38.4688 133.984 39.1719 133.984 39.9844C133.984 40.7969 133.688 41.5 133.094 42.0938Z" fill="#03A9F4"/>
|
||||
<path d="M0 84C0 81.7909 1.79086 80 4 80H16C18.2091 80 20 81.7909 20 84V84C20 86.2091 18.2091 88 16 88H4C1.79086 88 0 86.2091 0 84V84Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M28 84C28 81.7909 29.7909 80 32 80V80C34.2091 80 36 81.7909 36 84V84C36 86.2091 34.2091 88 32 88V88C29.7909 88 28 86.2091 28 84V84Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M40 84C40 81.7909 41.7909 80 44 80V80C46.2091 80 48 81.7909 48 84V84C48 86.2091 46.2091 88 44 88V88C41.7909 88 40 86.2091 40 84V84Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M0 100C0 95.5817 3.58172 92 8 92H40C44.4183 92 48 95.5817 48 100V144C48 148.418 44.4183 152 40 152H8C3.58172 152 0 148.418 0 144V100Z" fill="#1C1C1C"/>
|
||||
<path d="M8 92.5H40C44.1421 92.5 47.5 95.8579 47.5 100V144C47.5 148.142 44.1421 151.5 40 151.5H8C3.85787 151.5 0.5 148.142 0.5 144V100L0.509766 99.6143C0.704063 95.7792 3.77915 92.7041 7.61426 92.5098L8 92.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M14.0156 116H33.9844V128H32.0156V125.984H27.9844V128H26.0156V118.016H15.9844V128H14.0156V116ZM32.0156 118.016H27.9844V119.984H32.0156V118.016ZM27.9844 124.016H32.0156V122H27.9844V124.016Z" fill="#03A9F4"/>
|
||||
<path d="M56 84C56 81.7909 57.7909 80 60 80H72C74.2091 80 76 81.7909 76 84V84C76 86.2091 74.2091 88 72 88H60C57.7909 88 56 86.2091 56 84V84Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M84 84C84 81.7909 85.7909 80 88 80V80C90.2091 80 92 81.7909 92 84V84C92 86.2091 90.2091 88 88 88V88C85.7909 88 84 86.2091 84 84V84Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M96 84C96 81.7909 97.7909 80 100 80V80C102.209 80 104 81.7909 104 84V84C104 86.2091 102.209 88 100 88V88C97.7909 88 96 86.2091 96 84V84Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M56 100C56 95.5817 59.5817 92 64 92H96C100.418 92 104 95.5817 104 100V144C104 148.418 100.418 152 96 152H64C59.5817 152 56 148.418 56 144V100Z" fill="#1C1C1C"/>
|
||||
<path d="M64 92.5H96C100.142 92.5 103.5 95.8579 103.5 100V144C103.5 148.142 100.142 151.5 96 151.5H64C59.8579 151.5 56.5 148.142 56.5 144V100L56.5098 99.6143C56.7041 95.7792 59.7792 92.7041 63.6143 92.5098L64 92.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M77.9844 121.016V122.984H80V121.016H77.9844ZM82.0156 116V131H71V128.984H73.0156V113H82.0156V113.984H86.9844V128.984H89V131H85.0156V116H82.0156Z" fill="#03A9F4"/>
|
||||
<path d="M0 20C0 15.5817 3.58172 12 8 12H68C72.4183 12 76 15.5817 76 20V36C76 40.4183 72.4183 44 68 44H8C3.58172 44 0 40.4183 0 36V20Z" fill="#1C1C1C"/>
|
||||
<path d="M8 12.5H68C72.1421 12.5 75.5 15.8579 75.5 20V36C75.5 40.1421 72.1421 43.5 68 43.5H8C3.85786 43.5 0.5 40.1421 0.5 36V20C0.5 15.8579 3.85786 12.5 8 12.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M32.9844 27.0156C32.9844 26.0781 32.7031 25.2656 32.1406 24.5781C31.5781 23.8594 30.8594 23.375 29.9844 23.125V22C29.9844 21.4375 30.125 20.9375 30.4062 20.5C30.6875 20.0312 31.0469 19.6719 31.4844 19.4219C31.9531 19.1406 32.4531 19 32.9844 19H43.0156C43.5469 19 44.0312 19.1406 44.4688 19.4219C44.9375 19.6719 45.3125 20.0312 45.5938 20.5C45.875 20.9375 46.0156 21.4375 46.0156 22V23.125C45.1406 23.375 44.4219 23.8594 43.8594 24.5781C43.2969 25.2656 43.0156 26.0781 43.0156 27.0156V28.9844H32.9844V27.0156ZM47 25C47.5625 25 48.0312 25.2031 48.4062 25.6094C48.8125 25.9844 49.0156 26.4531 49.0156 27.0156V31.9844C49.0156 32.5469 48.875 33.0625 48.5938 33.5312C48.3125 33.9688 47.9375 34.3281 47.4688 34.6094C47.0312 34.8594 46.5469 34.9844 46.0156 34.9844V36.0156C46.0156 36.2656 45.9062 36.5 45.6875 36.7188C45.5 36.9062 45.2656 37 44.9844 37C44.7344 37 44.5 36.9062 44.2812 36.7188C44.0938 36.5 44 36.2656 44 36.0156V34.9844H32V36.0156C32 36.2656 31.8906 36.5 31.6719 36.7188C31.4844 36.9062 31.2656 37 31.0156 37C30.7344 37 30.4844 36.9062 30.2656 36.7188C30.0781 36.5 29.9844 36.2656 29.9844 36.0156V34.9844C29.4531 34.9844 28.9531 34.8594 28.4844 34.6094C28.0469 34.3281 27.6875 33.9688 27.4062 33.5312C27.125 33.0625 26.9844 32.5469 26.9844 31.9844V27.0156C26.9844 26.4531 27.1719 25.9844 27.5469 25.6094C27.9531 25.2031 28.4375 25 29 25C29.5625 25 30.0312 25.2031 30.4062 25.6094C30.8125 25.9844 31.0156 26.4531 31.0156 27.0156V31H44.9844V27.0156C44.9844 26.4531 45.1719 25.9844 45.5469 25.6094C45.9531 25.2031 46.4375 25 47 25Z" fill="#03A9F4"/>
|
||||
<path d="M0 56C0 51.5817 3.58172 48 8 48H68C72.4183 48 76 51.5817 76 56V72C76 76.4183 72.4183 80 68 80H8C3.58172 80 0 76.4183 0 72V56Z" fill="#1C1C1C"/>
|
||||
<path d="M8 48.5H68C72.1421 48.5 75.5 51.8579 75.5 56V72C75.5 76.1421 72.1421 79.5 68 79.5H8C3.85786 79.5 0.5 76.1421 0.5 72V56C0.5 51.8579 3.85786 48.5 8 48.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M44 61.9844H47.9844V64H46.0156V72.0156H29.9844V64H28.0156V61.9844H32C31.4375 61.9844 30.9531 61.7969 30.5469 61.4219C30.1719 61.0156 29.9844 60.5469 29.9844 60.0156V55.9844H35.9844V60.0156C35.9844 60.5469 35.7812 61.0156 35.375 61.4219C35 61.7969 34.5469 61.9844 34.0156 61.9844H41.9844V58.9844C41.9844 58.7344 41.8906 58.5156 41.7031 58.3281C41.5156 58.1094 41.2812 58 41 58C40.7188 58 40.4844 58.1094 40.2969 58.3281C40.1094 58.5156 40.0156 58.7344 40.0156 58.9844H38C38 58.4531 38.125 57.9688 38.375 57.5312C38.6562 57.0625 39.0156 56.6875 39.4531 56.4062C39.9219 56.125 40.4375 55.9844 41 55.9844C41.5625 55.9844 42.0625 56.125 42.5 56.4062C42.9688 56.6875 43.3281 57.0625 43.5781 57.5312C43.8594 57.9688 44 58.4531 44 58.9844V61.9844ZM38.9844 70V64H37.0156V70H38.9844Z" fill="#03A9F4"/>
|
||||
<path d="M0 92C0 87.5817 3.58172 84 8 84H68C72.4183 84 76 87.5817 76 92V108C76 112.418 72.4183 116 68 116H8C3.58172 116 0 112.418 0 108V92Z" fill="#1C1C1C"/>
|
||||
<path d="M8 84.5H68C72.1421 84.5 75.5 87.8579 75.5 92V108C75.5 112.142 72.1421 115.5 68 115.5H8C3.85786 115.5 0.5 112.142 0.5 108V92C0.5 87.8579 3.85786 84.5 8 84.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M44.9844 94.9844C46.0781 94.9844 47.0156 95.3906 47.7969 96.2031C48.6094 96.9844 49.0156 97.9219 49.0156 99.0156V108.016H47V105.016H29V108.016H26.9844V93.0156H29V102.016H37.0156V94.9844H44.9844ZM35.0938 100.094C34.5 100.688 33.7969 100.984 32.9844 100.984C32.1719 100.984 31.4688 100.688 30.875 100.094C30.2812 99.5 29.9844 98.7969 29.9844 97.9844C29.9844 97.1719 30.2812 96.4688 30.875 95.875C31.4688 95.2812 32.1719 94.9844 32.9844 94.9844C33.7969 94.9844 34.5 95.2812 35.0938 95.875C35.6875 96.4688 35.9844 97.1719 35.9844 97.9844C35.9844 98.7969 35.6875 99.5 35.0938 100.094Z" fill="#03A9F4"/>
|
||||
<path d="M0 128C0 123.582 3.58172 120 8 120H68C72.4183 120 76 123.582 76 128V144C76 148.418 72.4183 152 68 152H8C3.58172 152 0 148.418 0 144V128Z" fill="#1C1C1C"/>
|
||||
<path d="M8 120.5H68C72.1421 120.5 75.5 123.858 75.5 128V144C75.5 148.142 72.1421 151.5 68 151.5H8C3.85786 151.5 0.5 148.142 0.5 144V128C0.5 123.858 3.85786 120.5 8 120.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M46.0156 136.984H47.9844V142.984C47.9844 143.516 47.7812 143.984 47.375 144.391C47 144.797 46.5469 145 46.0156 145C46.0156 145.281 45.9062 145.516 45.6875 145.703C45.5 145.891 45.2656 145.984 44.9844 145.984H31.0156C30.7344 145.984 30.4844 145.891 30.2656 145.703C30.0781 145.516 29.9844 145.281 29.9844 145C29.4531 145 28.9844 144.797 28.5781 144.391C28.2031 143.984 28.0156 143.516 28.0156 142.984V136.984H31.0156V136.234C31.0156 135.641 31.2344 135.125 31.6719 134.688C32.1406 134.219 32.6719 133.984 33.2656 133.984C33.8906 133.984 34.4531 134.234 34.9531 134.734L36.3125 136.281C36.5 136.5 36.7812 136.734 37.1562 136.984H44V128.828C44 128.609 43.9219 128.422 43.7656 128.266C43.6094 128.078 43.4062 127.984 43.1562 127.984C42.9375 127.984 42.75 128.062 42.5938 128.219L41.3281 129.484C41.3906 129.734 41.4219 129.906 41.4219 130C41.4219 130.344 41.3125 130.703 41.0938 131.078L38.3281 128.312C38.7031 128.094 39.0625 127.984 39.4062 127.984C39.5625 127.984 39.7344 128.016 39.9219 128.078L41.1875 126.812C41.7188 126.281 42.375 126.016 43.1562 126.016C43.9375 126.016 44.6094 126.297 45.1719 126.859C45.7344 127.391 46.0156 128.047 46.0156 128.828V136.984ZM31.5781 132.438C31.2031 132.031 31.0156 131.547 31.0156 130.984C31.0156 130.422 31.2031 129.953 31.5781 129.578C31.9531 129.203 32.4219 129.016 32.9844 129.016C33.5469 129.016 34.0156 129.203 34.3906 129.578C34.7969 129.953 35 130.422 35 130.984C35 131.547 34.7969 132.031 34.3906 132.438C34.0156 132.812 33.5469 133 32.9844 133C32.4219 133 31.9531 132.812 31.5781 132.438Z" fill="#03A9F4"/>
|
||||
<path d="M84 4C84 1.79086 85.7909 0 88 0H100C102.209 0 104 1.79086 104 4V4C104 6.20914 102.209 8 100 8H88C85.7909 8 84 6.20914 84 4V4Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M84 20C84 15.5817 87.5817 12 92 12H152C156.418 12 160 15.5817 160 20V36C160 40.4183 156.418 44 152 44H92C87.5817 44 84 40.4183 84 36V20Z" fill="#1C1C1C"/>
|
||||
<path d="M92 12.5H152C156.142 12.5 159.5 15.8579 159.5 20V36C159.5 40.1421 156.142 43.5 152 43.5H92C87.8579 43.5 84.5 40.1421 84.5 36V20C84.5 15.8579 87.8579 12.5 92 12.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M131.984 30.0156C131.984 30.7656 131.797 31.4531 131.422 32.0781C131.047 32.6719 130.562 33.1406 129.969 33.4844C129.844 34.2031 129.5 34.8125 128.938 35.3125C128.406 35.7812 127.766 36.0156 127.016 36.0156C126.359 36.0156 125.766 35.8281 125.234 35.4531C124.734 35.0781 124.391 34.5938 124.203 34H119.797C119.609 34.5938 119.25 35.0781 118.719 35.4531C118.219 35.8281 117.641 36.0156 116.984 36.0156C116.234 36.0156 115.578 35.7812 115.016 35.3125C114.484 34.8125 114.156 34.2031 114.031 33.4844C113.438 33.1406 112.953 32.6719 112.578 32.0781C112.203 31.4531 112.016 30.7656 112.016 30.0156C112.016 29.1094 112.266 28.3125 112.766 27.625C113.297 26.9375 113.969 26.4688 114.781 26.2188L113 24.3906L112.719 24.7188C112.5 24.9062 112.25 25 111.969 25C111.719 25 111.5 24.9062 111.312 24.7188C111.094 24.5312 110.984 24.2969 110.984 24.0156C110.984 23.7344 111.094 23.5 111.312 23.3125L113.281 21.2969C113.469 21.1094 113.703 21.0156 113.984 21.0156C114.266 21.0156 114.5 21.1094 114.688 21.2969C114.906 21.4844 115.016 21.7188 115.016 22C115.016 22.2812 114.906 22.5156 114.688 22.7031L114.406 22.9844L115.812 24.3906L116.609 22.0469C116.797 21.4219 117.156 20.9219 117.688 20.5469C118.219 20.1719 118.797 19.9844 119.422 19.9844H124.578C125.203 19.9844 125.781 20.1719 126.312 20.5469C126.844 20.9219 127.203 21.4219 127.391 22.0469L128.75 26.0781C129.375 26.2031 129.922 26.4531 130.391 26.8281C130.891 27.2031 131.281 27.6719 131.562 28.2344C131.844 28.7656 131.984 29.3594 131.984 30.0156ZM116.984 34C117.266 34 117.5 33.9062 117.688 33.7188C117.906 33.5 118.016 33.2656 118.016 33.0156C118.016 32.7344 117.906 32.5 117.688 32.3125C117.5 32.0938 117.266 31.9844 116.984 31.9844C116.734 31.9844 116.5 32.0938 116.281 32.3125C116.094 32.5 116 32.7344 116 33.0156C116 33.2656 116.094 33.5 116.281 33.7188C116.5 33.9062 116.734 34 116.984 34ZM121.016 25.9844V22H119.422C118.953 22 118.641 22.2344 118.484 22.7031L117.406 25.9844H121.016ZM122.984 22V25.9844H126.594L125.516 22.7031C125.359 22.2344 125.047 22 124.578 22H122.984ZM127.016 34C127.266 34 127.484 33.9062 127.672 33.7188C127.891 33.5 128 33.2656 128 33.0156C128 32.7344 127.891 32.5 127.672 32.3125C127.484 32.0938 127.266 31.9844 127.016 31.9844C126.734 31.9844 126.484 32.0938 126.266 32.3125C126.078 32.5 125.984 32.7344 125.984 33.0156C125.984 33.2656 126.078 33.5 126.266 33.7188C126.484 33.9062 126.734 34 127.016 34Z" fill="#03A9F4"/>
|
||||
<path d="M84 56C84 51.5817 87.5817 48 92 48H152C156.418 48 160 51.5817 160 56V72C160 76.4183 156.418 80 152 80H92C87.5817 80 84 76.4183 84 72V56Z" fill="#1C1C1C"/>
|
||||
<path d="M92 48.5H152C156.142 48.5 159.5 51.8579 159.5 56V72C159.5 76.1421 156.142 79.5 152 79.5H92C87.8579 79.5 84.5 76.1421 84.5 72V56C84.5 51.8579 87.8579 48.5 92 48.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M128.984 58.9844C130.078 58.9844 131.016 59.3906 131.797 60.2031C132.609 60.9844 133.016 61.9219 133.016 63.0156V72.0156H131V69.0156H113V72.0156H110.984V57.0156H113V66.0156H121.016V58.9844H128.984ZM119.094 64.0938C118.5 64.6875 117.797 64.9844 116.984 64.9844C116.172 64.9844 115.469 64.6875 114.875 64.0938C114.281 63.5 113.984 62.7969 113.984 61.9844C113.984 61.1719 114.281 60.4688 114.875 59.875C115.469 59.2812 116.172 58.9844 116.984 58.9844C117.797 58.9844 118.5 59.2812 119.094 59.875C119.688 60.4688 119.984 61.1719 119.984 61.9844C119.984 62.7969 119.688 63.5 119.094 64.0938Z" fill="#03A9F4"/>
|
||||
<path d="M84 92C84 87.5817 87.5817 84 92 84H152C156.418 84 160 87.5817 160 92V108C160 112.418 156.418 116 152 116H92C87.5817 116 84 112.418 84 108V92Z" fill="#1C1C1C"/>
|
||||
<path d="M92 84.5H152C156.142 84.5 159.5 87.8579 159.5 92V108C159.5 112.142 156.142 115.5 152 115.5H92C87.8579 115.5 84.5 112.142 84.5 108V92C84.5 87.8579 87.8579 84.5 92 84.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M112.016 94H131.984V106H130.016V103.984H125.984V106H124.016V96.0156H113.984V106H112.016V94ZM130.016 96.0156H125.984V97.9844H130.016V96.0156ZM125.984 102.016H130.016V100H125.984V102.016Z" fill="#03A9F4"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1738_5532">
|
||||
<clipPath id="clip0_3969_57097">
|
||||
<rect width="160" height="160" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 11 KiB |
@ -1,35 +1,31 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1738_5531)">
|
||||
<g clip-path="url(#clip0_3969_50764)">
|
||||
<path d="M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V4C20 6.20914 18.2091 8 16 8H4C1.79086 8 0 6.20914 0 4V4Z" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="40" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 20C0 15.5817 3.58172 12 8 12H40C44.4183 12 48 15.5817 48 20V64C48 68.4183 44.4183 72 40 72H8C3.58172 72 0 68.4183 0 64V20Z" fill="white"/>
|
||||
<path d="M8 12.5H40C44.1421 12.5 47.5 15.8579 47.5 20V64C47.5 68.1421 44.1421 71.5 40 71.5H8C3.85787 71.5 0.5 68.1421 0.5 64V20L0.509766 19.6143C0.704063 15.7792 3.77915 12.7041 7.61426 12.5098L8 12.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M18.9844 41.0156C18.9844 40.0781 18.7031 39.2656 18.1406 38.5781C17.5781 37.8594 16.8594 37.375 15.9844 37.125V36C15.9844 35.4375 16.125 34.9375 16.4062 34.5C16.6875 34.0312 17.0469 33.6719 17.4844 33.4219C17.9531 33.1406 18.4531 33 18.9844 33H29.0156C29.5469 33 30.0312 33.1406 30.4688 33.4219C30.9375 33.6719 31.3125 34.0312 31.5938 34.5C31.875 34.9375 32.0156 35.4375 32.0156 36V37.125C31.1406 37.375 30.4219 37.8594 29.8594 38.5781C29.2969 39.2656 29.0156 40.0781 29.0156 41.0156V42.9844H18.9844V41.0156ZM33 39C33.5625 39 34.0312 39.2031 34.4062 39.6094C34.8125 39.9844 35.0156 40.4531 35.0156 41.0156V45.9844C35.0156 46.5469 34.875 47.0625 34.5938 47.5312C34.3125 47.9688 33.9375 48.3281 33.4688 48.6094C33.0312 48.8594 32.5469 48.9844 32.0156 48.9844V50.0156C32.0156 50.2656 31.9062 50.5 31.6875 50.7188C31.5 50.9062 31.2656 51 30.9844 51C30.7344 51 30.5 50.9062 30.2812 50.7188C30.0938 50.5 30 50.2656 30 50.0156V48.9844H18V50.0156C18 50.2656 17.8906 50.5 17.6719 50.7188C17.4844 50.9062 17.2656 51 17.0156 51C16.7344 51 16.4844 50.9062 16.2656 50.7188C16.0781 50.5 15.9844 50.2656 15.9844 50.0156V48.9844C15.4531 48.9844 14.9531 48.8594 14.4844 48.6094C14.0469 48.3281 13.6875 47.9688 13.4062 47.5312C13.125 47.0625 12.9844 46.5469 12.9844 45.9844V41.0156C12.9844 40.4531 13.1719 39.9844 13.5469 39.6094C13.9531 39.2031 14.4375 39 15 39C15.5625 39 16.0312 39.2031 16.4062 39.6094C16.8125 39.9844 17.0156 40.4531 17.0156 41.0156V45H30.9844V41.0156C30.9844 40.4531 31.1719 39.9844 31.5469 39.6094C31.9531 39.2031 32.4375 39 33 39Z" fill="#03A9F4"/>
|
||||
<path d="M56 4C56 1.79086 57.7909 0 60 0H72C74.2091 0 76 1.79086 76 4V4C76 6.20914 74.2091 8 72 8H60C57.7909 8 56 6.20914 56 4V4Z" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="96" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M56 20C56 15.5817 59.5817 12 64 12H96C100.418 12 104 15.5817 104 20V64C104 68.4183 100.418 72 96 72H64C59.5817 72 56 68.4183 56 64V20Z" fill="white"/>
|
||||
<path d="M64 12.5H96C100.142 12.5 103.5 15.8579 103.5 20V64C103.5 68.1421 100.142 71.5 96 71.5H64C59.8579 71.5 56.5 68.1421 56.5 64V20L56.5098 19.6143C56.7041 15.7792 59.7792 12.7041 63.6143 12.5098L64 12.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M86 39.9844H89.9844V42H88.0156V50.0156H71.9844V42H70.0156V39.9844H74C73.4375 39.9844 72.9531 39.7969 72.5469 39.4219C72.1719 39.0156 71.9844 38.5469 71.9844 38.0156V33.9844H77.9844V38.0156C77.9844 38.5469 77.7812 39.0156 77.375 39.4219C77 39.7969 76.5469 39.9844 76.0156 39.9844H83.9844V36.9844C83.9844 36.7344 83.8906 36.5156 83.7031 36.3281C83.5156 36.1094 83.2812 36 83 36C82.7188 36 82.4844 36.1094 82.2969 36.3281C82.1094 36.5156 82.0156 36.7344 82.0156 36.9844H80C80 36.4531 80.125 35.9688 80.375 35.5312C80.6562 35.0625 81.0156 34.6875 81.4531 34.4062C81.9219 34.125 82.4375 33.9844 83 33.9844C83.5625 33.9844 84.0625 34.125 84.5 34.4062C84.9688 34.6875 85.3281 35.0625 85.5781 35.5312C85.8594 35.9688 86 36.4531 86 36.9844V39.9844ZM80.9844 48V42H79.0156V48H80.9844Z" fill="#03A9F4"/>
|
||||
<path d="M112 4C112 1.79086 113.791 0 116 0H128C130.209 0 132 1.79086 132 4V4C132 6.20914 130.209 8 128 8H116C113.791 8 112 6.20914 112 4V4Z" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="152" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M112 20C112 15.5817 115.582 12 120 12H152C156.418 12 160 15.5817 160 20V64C160 68.4183 156.418 72 152 72H120C115.582 72 112 68.4183 112 64V20Z" fill="white"/>
|
||||
<path d="M120 12.5H152C156.142 12.5 159.5 15.8579 159.5 20V64C159.5 68.1421 156.142 71.5 152 71.5H120C115.858 71.5 112.5 68.1421 112.5 64V20L112.51 19.6143C112.704 15.7792 115.779 12.7041 119.614 12.5098L120 12.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M142.984 36.9844C144.078 36.9844 145.016 37.3906 145.797 38.2031C146.609 38.9844 147.016 39.9219 147.016 41.0156V50.0156H145V47.0156H127V50.0156H124.984V35.0156H127V44.0156H135.016V36.9844H142.984ZM133.094 42.0938C132.5 42.6875 131.797 42.9844 130.984 42.9844C130.172 42.9844 129.469 42.6875 128.875 42.0938C128.281 41.5 127.984 40.7969 127.984 39.9844C127.984 39.1719 128.281 38.4688 128.875 37.875C129.469 37.2812 130.172 36.9844 130.984 36.9844C131.797 36.9844 132.5 37.2812 133.094 37.875C133.688 38.4688 133.984 39.1719 133.984 39.9844C133.984 40.7969 133.688 41.5 133.094 42.0938Z" fill="#03A9F4"/>
|
||||
<path d="M0 84C0 81.7909 1.79086 80 4 80H16C18.2091 80 20 81.7909 20 84V84C20 86.2091 18.2091 88 16 88H4C1.79086 88 0 86.2091 0 84V84Z" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="28" y="80" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="40" y="80" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 100C0 95.5817 3.58172 92 8 92H40C44.4183 92 48 95.5817 48 100V144C48 148.418 44.4183 152 40 152H8C3.58172 152 0 148.418 0 144V100Z" fill="white"/>
|
||||
<path d="M8 92.5H40C44.1421 92.5 47.5 95.8579 47.5 100V144C47.5 148.142 44.1421 151.5 40 151.5H8C3.85787 151.5 0.5 148.142 0.5 144V100L0.509766 99.6143C0.704063 95.7792 3.77915 92.7041 7.61426 92.5098L8 92.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M14.0156 116H33.9844V128H32.0156V125.984H27.9844V128H26.0156V118.016H15.9844V128H14.0156V116ZM32.0156 118.016H27.9844V119.984H32.0156V118.016ZM27.9844 124.016H32.0156V122H27.9844V124.016Z" fill="#03A9F4"/>
|
||||
<path d="M56 84C56 81.7909 57.7909 80 60 80H72C74.2091 80 76 81.7909 76 84V84C76 86.2091 74.2091 88 72 88H60C57.7909 88 56 86.2091 56 84V84Z" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="84" y="80" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="96" y="80" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M56 100C56 95.5817 59.5817 92 64 92H96C100.418 92 104 95.5817 104 100V144C104 148.418 100.418 152 96 152H64C59.5817 152 56 148.418 56 144V100Z" fill="white"/>
|
||||
<path d="M64 92.5H96C100.142 92.5 103.5 95.8579 103.5 100V144C103.5 148.142 100.142 151.5 96 151.5H64C59.8579 151.5 56.5 148.142 56.5 144V100L56.5098 99.6143C56.7041 95.7792 59.7792 92.7041 63.6143 92.5098L64 92.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M77.9844 121.016V122.984H80V121.016H77.9844ZM82.0156 116V131H71V128.984H73.0156V113H82.0156V113.984H86.9844V128.984H89V131H85.0156V116H82.0156Z" fill="#03A9F4"/>
|
||||
<path d="M0 20C0 15.5817 3.58172 12 8 12H68C72.4183 12 76 15.5817 76 20V36C76 40.4183 72.4183 44 68 44H8C3.58172 44 0 40.4183 0 36V20Z" fill="white"/>
|
||||
<path d="M8 12.5H68C72.1421 12.5 75.5 15.8579 75.5 20V36C75.5 40.1421 72.1421 43.5 68 43.5H8C3.85786 43.5 0.5 40.1421 0.5 36V20C0.5 15.8579 3.85786 12.5 8 12.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M32.9844 27.0156C32.9844 26.0781 32.7031 25.2656 32.1406 24.5781C31.5781 23.8594 30.8594 23.375 29.9844 23.125V22C29.9844 21.4375 30.125 20.9375 30.4062 20.5C30.6875 20.0312 31.0469 19.6719 31.4844 19.4219C31.9531 19.1406 32.4531 19 32.9844 19H43.0156C43.5469 19 44.0312 19.1406 44.4688 19.4219C44.9375 19.6719 45.3125 20.0312 45.5938 20.5C45.875 20.9375 46.0156 21.4375 46.0156 22V23.125C45.1406 23.375 44.4219 23.8594 43.8594 24.5781C43.2969 25.2656 43.0156 26.0781 43.0156 27.0156V28.9844H32.9844V27.0156ZM47 25C47.5625 25 48.0312 25.2031 48.4062 25.6094C48.8125 25.9844 49.0156 26.4531 49.0156 27.0156V31.9844C49.0156 32.5469 48.875 33.0625 48.5938 33.5312C48.3125 33.9688 47.9375 34.3281 47.4688 34.6094C47.0312 34.8594 46.5469 34.9844 46.0156 34.9844V36.0156C46.0156 36.2656 45.9062 36.5 45.6875 36.7188C45.5 36.9062 45.2656 37 44.9844 37C44.7344 37 44.5 36.9062 44.2812 36.7188C44.0938 36.5 44 36.2656 44 36.0156V34.9844H32V36.0156C32 36.2656 31.8906 36.5 31.6719 36.7188C31.4844 36.9062 31.2656 37 31.0156 37C30.7344 37 30.4844 36.9062 30.2656 36.7188C30.0781 36.5 29.9844 36.2656 29.9844 36.0156V34.9844C29.4531 34.9844 28.9531 34.8594 28.4844 34.6094C28.0469 34.3281 27.6875 33.9688 27.4062 33.5312C27.125 33.0625 26.9844 32.5469 26.9844 31.9844V27.0156C26.9844 26.4531 27.1719 25.9844 27.5469 25.6094C27.9531 25.2031 28.4375 25 29 25C29.5625 25 30.0312 25.2031 30.4062 25.6094C30.8125 25.9844 31.0156 26.4531 31.0156 27.0156V31H44.9844V27.0156C44.9844 26.4531 45.1719 25.9844 45.5469 25.6094C45.9531 25.2031 46.4375 25 47 25Z" fill="#03A9F4"/>
|
||||
<path d="M0 56C0 51.5817 3.58172 48 8 48H68C72.4183 48 76 51.5817 76 56V72C76 76.4183 72.4183 80 68 80H8C3.58172 80 0 76.4183 0 72V56Z" fill="white"/>
|
||||
<path d="M8 48.5H68C72.1421 48.5 75.5 51.8579 75.5 56V72C75.5 76.1421 72.1421 79.5 68 79.5H8C3.85786 79.5 0.5 76.1421 0.5 72V56C0.5 51.8579 3.85786 48.5 8 48.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M44 61.9844H47.9844V64H46.0156V72.0156H29.9844V64H28.0156V61.9844H32C31.4375 61.9844 30.9531 61.7969 30.5469 61.4219C30.1719 61.0156 29.9844 60.5469 29.9844 60.0156V55.9844H35.9844V60.0156C35.9844 60.5469 35.7812 61.0156 35.375 61.4219C35 61.7969 34.5469 61.9844 34.0156 61.9844H41.9844V58.9844C41.9844 58.7344 41.8906 58.5156 41.7031 58.3281C41.5156 58.1094 41.2812 58 41 58C40.7188 58 40.4844 58.1094 40.2969 58.3281C40.1094 58.5156 40.0156 58.7344 40.0156 58.9844H38C38 58.4531 38.125 57.9688 38.375 57.5312C38.6562 57.0625 39.0156 56.6875 39.4531 56.4062C39.9219 56.125 40.4375 55.9844 41 55.9844C41.5625 55.9844 42.0625 56.125 42.5 56.4062C42.9688 56.6875 43.3281 57.0625 43.5781 57.5312C43.8594 57.9688 44 58.4531 44 58.9844V61.9844ZM38.9844 70V64H37.0156V70H38.9844Z" fill="#03A9F4"/>
|
||||
<path d="M0 92C0 87.5817 3.58172 84 8 84H68C72.4183 84 76 87.5817 76 92V108C76 112.418 72.4183 116 68 116H8C3.58172 116 0 112.418 0 108V92Z" fill="white"/>
|
||||
<path d="M8 84.5H68C72.1421 84.5 75.5 87.8579 75.5 92V108C75.5 112.142 72.1421 115.5 68 115.5H8C3.85786 115.5 0.5 112.142 0.5 108V92C0.5 87.8579 3.85786 84.5 8 84.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M44.9844 94.9844C46.0781 94.9844 47.0156 95.3906 47.7969 96.2031C48.6094 96.9844 49.0156 97.9219 49.0156 99.0156V108.016H47V105.016H29V108.016H26.9844V93.0156H29V102.016H37.0156V94.9844H44.9844ZM35.0938 100.094C34.5 100.688 33.7969 100.984 32.9844 100.984C32.1719 100.984 31.4688 100.688 30.875 100.094C30.2812 99.5 29.9844 98.7969 29.9844 97.9844C29.9844 97.1719 30.2812 96.4688 30.875 95.875C31.4688 95.2812 32.1719 94.9844 32.9844 94.9844C33.7969 94.9844 34.5 95.2812 35.0938 95.875C35.6875 96.4688 35.9844 97.1719 35.9844 97.9844C35.9844 98.7969 35.6875 99.5 35.0938 100.094Z" fill="#03A9F4"/>
|
||||
<path d="M0 128C0 123.582 3.58172 120 8 120H68C72.4183 120 76 123.582 76 128V144C76 148.418 72.4183 152 68 152H8C3.58172 152 0 148.418 0 144V128Z" fill="white"/>
|
||||
<path d="M8 120.5H68C72.1421 120.5 75.5 123.858 75.5 128V144C75.5 148.142 72.1421 151.5 68 151.5H8C3.85786 151.5 0.5 148.142 0.5 144V128C0.5 123.858 3.85786 120.5 8 120.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M46.0156 136.984H47.9844V142.984C47.9844 143.516 47.7812 143.984 47.375 144.391C47 144.797 46.5469 145 46.0156 145C46.0156 145.281 45.9062 145.516 45.6875 145.703C45.5 145.891 45.2656 145.984 44.9844 145.984H31.0156C30.7344 145.984 30.4844 145.891 30.2656 145.703C30.0781 145.516 29.9844 145.281 29.9844 145C29.4531 145 28.9844 144.797 28.5781 144.391C28.2031 143.984 28.0156 143.516 28.0156 142.984V136.984H31.0156V136.234C31.0156 135.641 31.2344 135.125 31.6719 134.688C32.1406 134.219 32.6719 133.984 33.2656 133.984C33.8906 133.984 34.4531 134.234 34.9531 134.734L36.3125 136.281C36.5 136.5 36.7812 136.734 37.1562 136.984H44V128.828C44 128.609 43.9219 128.422 43.7656 128.266C43.6094 128.078 43.4062 127.984 43.1562 127.984C42.9375 127.984 42.75 128.062 42.5938 128.219L41.3281 129.484C41.3906 129.734 41.4219 129.906 41.4219 130C41.4219 130.344 41.3125 130.703 41.0938 131.078L38.3281 128.312C38.7031 128.094 39.0625 127.984 39.4062 127.984C39.5625 127.984 39.7344 128.016 39.9219 128.078L41.1875 126.812C41.7188 126.281 42.375 126.016 43.1562 126.016C43.9375 126.016 44.6094 126.297 45.1719 126.859C45.7344 127.391 46.0156 128.047 46.0156 128.828V136.984ZM31.5781 132.438C31.2031 132.031 31.0156 131.547 31.0156 130.984C31.0156 130.422 31.2031 129.953 31.5781 129.578C31.9531 129.203 32.4219 129.016 32.9844 129.016C33.5469 129.016 34.0156 129.203 34.3906 129.578C34.7969 129.953 35 130.422 35 130.984C35 131.547 34.7969 132.031 34.3906 132.438C34.0156 132.812 33.5469 133 32.9844 133C32.4219 133 31.9531 132.812 31.5781 132.438Z" fill="#03A9F4"/>
|
||||
<path d="M84 4C84 1.79086 85.7909 0 88 0H100C102.209 0 104 1.79086 104 4V4C104 6.20914 102.209 8 100 8H88C85.7909 8 84 6.20914 84 4V4Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M84 20C84 15.5817 87.5817 12 92 12H152C156.418 12 160 15.5817 160 20V36C160 40.4183 156.418 44 152 44H92C87.5817 44 84 40.4183 84 36V20Z" fill="white"/>
|
||||
<path d="M92 12.5H152C156.142 12.5 159.5 15.8579 159.5 20V36C159.5 40.1421 156.142 43.5 152 43.5H92C87.8579 43.5 84.5 40.1421 84.5 36V20C84.5 15.8579 87.8579 12.5 92 12.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M131.984 30.0156C131.984 30.7656 131.797 31.4531 131.422 32.0781C131.047 32.6719 130.562 33.1406 129.969 33.4844C129.844 34.2031 129.5 34.8125 128.938 35.3125C128.406 35.7812 127.766 36.0156 127.016 36.0156C126.359 36.0156 125.766 35.8281 125.234 35.4531C124.734 35.0781 124.391 34.5938 124.203 34H119.797C119.609 34.5938 119.25 35.0781 118.719 35.4531C118.219 35.8281 117.641 36.0156 116.984 36.0156C116.234 36.0156 115.578 35.7812 115.016 35.3125C114.484 34.8125 114.156 34.2031 114.031 33.4844C113.438 33.1406 112.953 32.6719 112.578 32.0781C112.203 31.4531 112.016 30.7656 112.016 30.0156C112.016 29.1094 112.266 28.3125 112.766 27.625C113.297 26.9375 113.969 26.4688 114.781 26.2188L113 24.3906L112.719 24.7188C112.5 24.9062 112.25 25 111.969 25C111.719 25 111.5 24.9062 111.312 24.7188C111.094 24.5312 110.984 24.2969 110.984 24.0156C110.984 23.7344 111.094 23.5 111.312 23.3125L113.281 21.2969C113.469 21.1094 113.703 21.0156 113.984 21.0156C114.266 21.0156 114.5 21.1094 114.688 21.2969C114.906 21.4844 115.016 21.7188 115.016 22C115.016 22.2812 114.906 22.5156 114.688 22.7031L114.406 22.9844L115.812 24.3906L116.609 22.0469C116.797 21.4219 117.156 20.9219 117.688 20.5469C118.219 20.1719 118.797 19.9844 119.422 19.9844H124.578C125.203 19.9844 125.781 20.1719 126.312 20.5469C126.844 20.9219 127.203 21.4219 127.391 22.0469L128.75 26.0781C129.375 26.2031 129.922 26.4531 130.391 26.8281C130.891 27.2031 131.281 27.6719 131.562 28.2344C131.844 28.7656 131.984 29.3594 131.984 30.0156ZM116.984 34C117.266 34 117.5 33.9062 117.688 33.7188C117.906 33.5 118.016 33.2656 118.016 33.0156C118.016 32.7344 117.906 32.5 117.688 32.3125C117.5 32.0938 117.266 31.9844 116.984 31.9844C116.734 31.9844 116.5 32.0938 116.281 32.3125C116.094 32.5 116 32.7344 116 33.0156C116 33.2656 116.094 33.5 116.281 33.7188C116.5 33.9062 116.734 34 116.984 34ZM121.016 25.9844V22H119.422C118.953 22 118.641 22.2344 118.484 22.7031L117.406 25.9844H121.016ZM122.984 22V25.9844H126.594L125.516 22.7031C125.359 22.2344 125.047 22 124.578 22H122.984ZM127.016 34C127.266 34 127.484 33.9062 127.672 33.7188C127.891 33.5 128 33.2656 128 33.0156C128 32.7344 127.891 32.5 127.672 32.3125C127.484 32.0938 127.266 31.9844 127.016 31.9844C126.734 31.9844 126.484 32.0938 126.266 32.3125C126.078 32.5 125.984 32.7344 125.984 33.0156C125.984 33.2656 126.078 33.5 126.266 33.7188C126.484 33.9062 126.734 34 127.016 34Z" fill="#03A9F4"/>
|
||||
<path d="M84 56C84 51.5817 87.5817 48 92 48H152C156.418 48 160 51.5817 160 56V72C160 76.4183 156.418 80 152 80H92C87.5817 80 84 76.4183 84 72V56Z" fill="white"/>
|
||||
<path d="M92 48.5H152C156.142 48.5 159.5 51.8579 159.5 56V72C159.5 76.1421 156.142 79.5 152 79.5H92C87.8579 79.5 84.5 76.1421 84.5 72V56C84.5 51.8579 87.8579 48.5 92 48.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M128.984 58.9844C130.078 58.9844 131.016 59.3906 131.797 60.2031C132.609 60.9844 133.016 61.9219 133.016 63.0156V72.0156H131V69.0156H113V72.0156H110.984V57.0156H113V66.0156H121.016V58.9844H128.984ZM119.094 64.0938C118.5 64.6875 117.797 64.9844 116.984 64.9844C116.172 64.9844 115.469 64.6875 114.875 64.0938C114.281 63.5 113.984 62.7969 113.984 61.9844C113.984 61.1719 114.281 60.4688 114.875 59.875C115.469 59.2812 116.172 58.9844 116.984 58.9844C117.797 58.9844 118.5 59.2812 119.094 59.875C119.688 60.4688 119.984 61.1719 119.984 61.9844C119.984 62.7969 119.688 63.5 119.094 64.0938Z" fill="#03A9F4"/>
|
||||
<path d="M84 92C84 87.5817 87.5817 84 92 84H152C156.418 84 160 87.5817 160 92V108C160 112.418 156.418 116 152 116H92C87.5817 116 84 112.418 84 108V92Z" fill="white"/>
|
||||
<path d="M92 84.5H152C156.142 84.5 159.5 87.8579 159.5 92V108C159.5 112.142 156.142 115.5 152 115.5H92C87.8579 115.5 84.5 112.142 84.5 108V92C84.5 87.8579 87.8579 84.5 92 84.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M112.016 94H131.984V106H130.016V103.984H125.984V106H124.016V96.0156H113.984V106H112.016V94ZM130.016 96.0156H125.984V97.9844H130.016V96.0156ZM125.984 102.016H130.016V100H125.984V102.016Z" fill="#03A9F4"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1738_5531">
|
||||
<clipPath id="clip0_3969_50764">
|
||||
<rect width="160" height="160" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 11 KiB |
@ -33,7 +33,7 @@ fi
|
||||
|
||||
docker run \
|
||||
-v ${LOCAL_FILE}:/opt/src/${LOCAL_FILE} \
|
||||
lokalise/lokalise-cli-2:v2.6.10 lokalise2 \
|
||||
lokalise/lokalise-cli-2:v3.1.4 lokalise2 \
|
||||
--token ${LOKALISE_TOKEN} \
|
||||
--project-id ${PROJECT_ID} \
|
||||
file upload \
|
||||
|
@ -202,7 +202,6 @@ export function storage(options: {
|
||||
// Don't set the initial value if we have a value in localStorage
|
||||
if (this.__initialized || getValue() === undefined) {
|
||||
setValue(this, value);
|
||||
this.requestUpdate(propertyKey, undefined);
|
||||
}
|
||||
},
|
||||
configurable: true,
|
||||
@ -212,11 +211,13 @@ export function storage(options: {
|
||||
const oldSetter = descriptor.set;
|
||||
newDescriptor = {
|
||||
...descriptor,
|
||||
get(this: ReactiveStorageElement) {
|
||||
return getValue();
|
||||
},
|
||||
set(this: ReactiveStorageElement, value) {
|
||||
// Don't set the initial value if we have a value in localStorage
|
||||
if (this.__initialized || getValue() === undefined) {
|
||||
setValue(this, value);
|
||||
this.requestUpdate(propertyKey, undefined);
|
||||
}
|
||||
oldSetter?.call(this, value);
|
||||
},
|
||||
|
@ -165,6 +165,7 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
// state is a timestamp
|
||||
if (
|
||||
[
|
||||
"ai_task",
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { callService, type HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@ -62,7 +62,7 @@ export const toggleGroupEntities = (
|
||||
|
||||
const entitiesIds = states.map((stateObj) => stateObj.entity_id);
|
||||
|
||||
callService(hass.connection, domain, service, {
|
||||
hass.callService(domain, service, {
|
||||
entity_id: entitiesIds,
|
||||
});
|
||||
};
|
||||
|
@ -2,12 +2,13 @@ import memoizeOne from "memoize-one";
|
||||
import { isIPAddress } from "./is_ip_address";
|
||||
|
||||
const collator = memoizeOne(
|
||||
(language: string | undefined) => new Intl.Collator(language)
|
||||
(language: string | undefined) =>
|
||||
new Intl.Collator(language, { numeric: true })
|
||||
);
|
||||
|
||||
const caseInsensitiveCollator = memoizeOne(
|
||||
(language: string | undefined) =>
|
||||
new Intl.Collator(language, { sensitivity: "accent" })
|
||||
new Intl.Collator(language, { sensitivity: "accent", numeric: true })
|
||||
);
|
||||
|
||||
const fallbackStringCompare = (a: string, b: string) => {
|
||||
|
@ -29,12 +29,22 @@ import { formatTimeLabel } from "./axis-label";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import "../chips/ha-assist-chip";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
import { colorVariables } from "../../resources/theme/color.globals";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
const LEGEND_OVERFLOW_LIMIT = 10;
|
||||
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
||||
const DOUBLE_TAP_TIME = 300;
|
||||
|
||||
export type CustomLegendOption = ECOption["legend"] & {
|
||||
type: "custom";
|
||||
data?: {
|
||||
id?: string;
|
||||
name: string;
|
||||
itemStyle?: Record<string, any>;
|
||||
}[];
|
||||
};
|
||||
|
||||
@customElement("ha-chart-base")
|
||||
export class HaChartBase extends LitElement {
|
||||
public chart?: EChartsType;
|
||||
@ -219,16 +229,18 @@ export class HaChartBase extends LitElement {
|
||||
if (!this.options?.legend || !this.data) {
|
||||
return nothing;
|
||||
}
|
||||
const legend = ensureArray(this.options.legend)[0] as LegendComponentOption;
|
||||
if (!legend.show || legend.type !== "custom") {
|
||||
const legend = ensureArray(this.options.legend).find(
|
||||
(l) => l.show && l.type === "custom"
|
||||
) as CustomLegendOption | undefined;
|
||||
if (!legend) {
|
||||
return nothing;
|
||||
}
|
||||
const datasets = ensureArray(this.data);
|
||||
const items: LegendComponentOption["data"] =
|
||||
const items =
|
||||
legend.data ||
|
||||
((datasets
|
||||
datasets
|
||||
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
|
||||
.map((d) => d.name ?? d.id) || []) as string[]);
|
||||
.map((d) => ({ id: d.id, name: d.name }));
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
@ -249,25 +261,29 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
let itemStyle: Record<string, any> = {};
|
||||
let name = "";
|
||||
let id = "";
|
||||
if (typeof item === "string") {
|
||||
name = item;
|
||||
const dataset = datasets.find(
|
||||
(d) => d.id === item || d.name === item
|
||||
);
|
||||
itemStyle = {
|
||||
color: dataset?.color as string,
|
||||
...(dataset?.itemStyle as { borderColor?: string }),
|
||||
};
|
||||
id = item;
|
||||
} else {
|
||||
name = item.name ?? "";
|
||||
id = item.id ?? name;
|
||||
itemStyle = item.itemStyle ?? {};
|
||||
}
|
||||
const dataset =
|
||||
datasets.find((d) => d.id === id) ??
|
||||
datasets.find((d) => d.name === id);
|
||||
itemStyle = {
|
||||
color: dataset?.color as string,
|
||||
...(dataset?.itemStyle as { borderColor?: string }),
|
||||
itemStyle,
|
||||
};
|
||||
const color = itemStyle?.color as string;
|
||||
const borderColor = itemStyle?.borderColor as string;
|
||||
return html`<li
|
||||
.name=${name}
|
||||
.id=${id}
|
||||
@click=${this._legendClick}
|
||||
class=${classMap({ hidden: this._hiddenDatasets.has(name) })}
|
||||
class=${classMap({ hidden: this._hiddenDatasets.has(id) })}
|
||||
.title=${name}
|
||||
>
|
||||
<div
|
||||
@ -333,6 +349,13 @@ export class HaChartBase extends LitElement {
|
||||
const { start, end } = e.batch?.[0] ?? e;
|
||||
this._isZoomed = start !== 0 || end !== 100;
|
||||
this._zoomRatio = (end - start) / 100;
|
||||
if (this._isTouchDevice) {
|
||||
// zooming changes the axis pointer so we need to hide it
|
||||
this.chart?.dispatchAction({
|
||||
type: "hideTip",
|
||||
from: "datazoom",
|
||||
});
|
||||
}
|
||||
});
|
||||
this.chart.on("click", (e: ECElementEvent) => {
|
||||
fireEvent(this, "chart-click", e);
|
||||
@ -354,6 +377,74 @@ export class HaChartBase extends LitElement {
|
||||
this._lastTapTime = Date.now();
|
||||
}
|
||||
});
|
||||
// show axis pointer handle on touch devices
|
||||
let dragJustEnded = false;
|
||||
let lastTipX: number | undefined;
|
||||
let lastTipY: number | undefined;
|
||||
this.chart.on("showTip", (e: any) => {
|
||||
lastTipX = e.x;
|
||||
lastTipY = e.y;
|
||||
this.chart?.setOption({
|
||||
xAxis: ensureArray(this.chart?.getOption().xAxis as any).map(
|
||||
(axis: XAXisOption) =>
|
||||
axis.show
|
||||
? {
|
||||
...axis,
|
||||
axisPointer: {
|
||||
...axis.axisPointer,
|
||||
status: "show",
|
||||
handle: {
|
||||
color: colorVariables["primary-color"],
|
||||
margin: 0,
|
||||
size: 20,
|
||||
...axis.axisPointer?.handle,
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
: axis
|
||||
),
|
||||
});
|
||||
});
|
||||
this.chart.on("hideTip", (e: any) => {
|
||||
// the drag end event doesn't have a `from` property
|
||||
if (e.from) {
|
||||
if (dragJustEnded) {
|
||||
// hideTip is fired twice when the drag ends, so we need to ignore the second one
|
||||
dragJustEnded = false;
|
||||
return;
|
||||
}
|
||||
this.chart?.setOption({
|
||||
xAxis: ensureArray(this.chart?.getOption().xAxis as any).map(
|
||||
(axis: XAXisOption) =>
|
||||
axis.show
|
||||
? {
|
||||
...axis,
|
||||
axisPointer: {
|
||||
...axis.axisPointer,
|
||||
handle: {
|
||||
...axis.axisPointer?.handle,
|
||||
show: false,
|
||||
},
|
||||
status: "hide",
|
||||
},
|
||||
}
|
||||
: axis
|
||||
),
|
||||
});
|
||||
this.chart?.dispatchAction({
|
||||
type: "downplay",
|
||||
});
|
||||
} else if (lastTipX != null && lastTipY != null) {
|
||||
// echarts hides the tip as soon as the drag ends, so we need to show it again
|
||||
dragJustEnded = true;
|
||||
this.chart?.dispatchAction({
|
||||
type: "showTip",
|
||||
x: lastTipX,
|
||||
y: lastTipY,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
@ -390,6 +481,7 @@ export class HaChartBase extends LitElement {
|
||||
type: "inside",
|
||||
orient: "horizontal",
|
||||
filterMode: "none",
|
||||
xAxisIndex: 0,
|
||||
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
zoomLock: !this._isTouchDevice && !this._modifierPressed,
|
||||
@ -644,7 +736,7 @@ export class HaChartBase extends LitElement {
|
||||
| YAXisOption
|
||||
| undefined;
|
||||
const series = ensureArray(this.data).map((s) => {
|
||||
const data = this._hiddenDatasets.has(String(s.name ?? s.id))
|
||||
const data = this._hiddenDatasets.has(String(s.id ?? s.name))
|
||||
? undefined
|
||||
: s.data;
|
||||
if (data && s.type === "line") {
|
||||
@ -739,13 +831,13 @@ export class HaChartBase extends LitElement {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
const name = ev.currentTarget?.name;
|
||||
if (this._hiddenDatasets.has(name)) {
|
||||
this._hiddenDatasets.delete(name);
|
||||
fireEvent(this, "dataset-unhidden", { name });
|
||||
const id = ev.currentTarget?.id;
|
||||
if (this._hiddenDatasets.has(id)) {
|
||||
this._hiddenDatasets.delete(id);
|
||||
fireEvent(this, "dataset-unhidden", { id });
|
||||
} else {
|
||||
this._hiddenDatasets.add(name);
|
||||
fireEvent(this, "dataset-hidden", { name });
|
||||
this._hiddenDatasets.add(id);
|
||||
fireEvent(this, "dataset-hidden", { id });
|
||||
}
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
}
|
||||
@ -880,8 +972,8 @@ declare global {
|
||||
"ha-chart-base": HaChartBase;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"dataset-hidden": { name: string };
|
||||
"dataset-unhidden": { name: string };
|
||||
"dataset-hidden": { id: string };
|
||||
"dataset-unhidden": { id: string };
|
||||
"chart-click": ECElementEvent;
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,13 @@ import type { ECOption } from "../../resources/echarts";
|
||||
import "./ha-chart-base";
|
||||
import type { HaChartBase } from "./ha-chart-base";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
|
||||
export interface NetworkNode {
|
||||
id: string;
|
||||
name?: string;
|
||||
category?: number;
|
||||
label?: string;
|
||||
value?: number;
|
||||
symbolSize?: number;
|
||||
symbol?: string;
|
||||
@ -60,7 +61,7 @@ export interface NetworkData {
|
||||
let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
||||
|
||||
@customElement("ha-network-graph")
|
||||
export class HaNetworkGraph extends LitElement {
|
||||
export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
public chart?: EChartsType;
|
||||
|
||||
@property({ attribute: false }) public data!: NetworkData;
|
||||
@ -77,8 +78,6 @@ export class HaNetworkGraph extends LitElement {
|
||||
|
||||
@state() private _showLabels = true;
|
||||
|
||||
private _listeners: (() => void)[] = [];
|
||||
|
||||
private _nodePositions: Record<string, { x: number; y: number }> = {};
|
||||
|
||||
@query("ha-chart-base") private _baseChart?: HaChartBase;
|
||||
@ -93,35 +92,31 @@ export class HaNetworkGraph extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._listeners.push(
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
||||
if (this._reducedMotion !== matches) {
|
||||
this._reducedMotion = matches;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!GraphChart) {
|
||||
return nothing;
|
||||
}
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return html`<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._getSeries(
|
||||
this.data,
|
||||
this._physicsEnabled,
|
||||
this._reducedMotion,
|
||||
this._showLabels
|
||||
this._showLabels,
|
||||
isMobile
|
||||
)}
|
||||
.options=${this._createOptions(this.data?.categories)}
|
||||
height="100%"
|
||||
@ -168,7 +163,8 @@ export class HaNetworkGraph extends LitElement {
|
||||
type: "inside",
|
||||
filterMode: "none",
|
||||
},
|
||||
})
|
||||
}),
|
||||
deepEqual
|
||||
);
|
||||
|
||||
private _getSeries = memoizeOne(
|
||||
@ -176,79 +172,85 @@ export class HaNetworkGraph extends LitElement {
|
||||
data: NetworkData,
|
||||
physicsEnabled: boolean,
|
||||
reducedMotion: boolean,
|
||||
showLabels: boolean
|
||||
showLabels: boolean,
|
||||
isMobile: boolean
|
||||
) => {
|
||||
const containerWidth = this.clientWidth;
|
||||
const containerHeight = this.clientHeight;
|
||||
return [
|
||||
{
|
||||
id: "network",
|
||||
type: "graph",
|
||||
layout: physicsEnabled ? "force" : "none",
|
||||
draggable: true,
|
||||
roam: true,
|
||||
selectedMode: "single",
|
||||
label: {
|
||||
show: showLabels,
|
||||
position: "right",
|
||||
},
|
||||
emphasis: {
|
||||
focus: "adjacency",
|
||||
},
|
||||
force: {
|
||||
repulsion: [400, 600],
|
||||
edgeLength: [200, 300],
|
||||
gravity: 0.1,
|
||||
layoutAnimation: !reducedMotion && data.nodes.length < 100,
|
||||
},
|
||||
edgeSymbol: ["none", "arrow"],
|
||||
edgeSymbolSize: 10,
|
||||
data: data.nodes.map((node) => {
|
||||
const echartsNode: NonNullable<GraphSeriesOption["data"]>[number] =
|
||||
{
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
category: node.category,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
symbol: node.symbol || "circle",
|
||||
itemStyle: node.itemStyle || {},
|
||||
fixed: node.fixed,
|
||||
};
|
||||
if (this._nodePositions[node.id]) {
|
||||
echartsNode.x = this._nodePositions[node.id].x;
|
||||
echartsNode.y = this._nodePositions[node.id].y;
|
||||
} else if (typeof node.polarDistance === "number") {
|
||||
// set the position of the node at polarDistance from the center in a random direction
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
echartsNode.x =
|
||||
containerWidth / 2 +
|
||||
((Math.cos(angle) * containerWidth) / 2) * node.polarDistance;
|
||||
echartsNode.y =
|
||||
containerHeight / 2 +
|
||||
((Math.sin(angle) * containerHeight) / 2) * node.polarDistance;
|
||||
this._nodePositions[node.id] = {
|
||||
x: echartsNode.x,
|
||||
y: echartsNode.y,
|
||||
};
|
||||
}
|
||||
return echartsNode;
|
||||
}),
|
||||
links: data.links.map((link) => ({
|
||||
...link,
|
||||
value: link.reverseValue
|
||||
? Math.max(link.value ?? 0, link.reverseValue)
|
||||
: link.value,
|
||||
// remove arrow for bidirectional links
|
||||
symbolSize: link.reverseValue ? 1 : link.symbolSize, // 0 doesn't work
|
||||
})),
|
||||
categories: data.categories || [],
|
||||
return {
|
||||
id: "network",
|
||||
type: "graph",
|
||||
layout: physicsEnabled ? "force" : "none",
|
||||
draggable: true,
|
||||
roam: true,
|
||||
selectedMode: "single",
|
||||
label: {
|
||||
show: showLabels,
|
||||
position: "right",
|
||||
},
|
||||
] as any;
|
||||
}
|
||||
emphasis: {
|
||||
focus: isMobile ? "none" : "adjacency",
|
||||
},
|
||||
force: {
|
||||
repulsion: [400, 600],
|
||||
edgeLength: [200, 300],
|
||||
gravity: 0.1,
|
||||
layoutAnimation: !reducedMotion && data.nodes.length < 100,
|
||||
},
|
||||
edgeSymbol: ["none", "arrow"],
|
||||
edgeSymbolSize: 10,
|
||||
data: data.nodes.map((node) => {
|
||||
const echartsNode: NonNullable<GraphSeriesOption["data"]>[number] = {
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
category: node.category,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
symbol: node.symbol || "circle",
|
||||
itemStyle: node.itemStyle || {},
|
||||
fixed: node.fixed,
|
||||
};
|
||||
if (this._nodePositions[node.id]) {
|
||||
echartsNode.x = this._nodePositions[node.id].x;
|
||||
echartsNode.y = this._nodePositions[node.id].y;
|
||||
} else if (typeof node.polarDistance === "number") {
|
||||
// set the position of the node at polarDistance from the center in a random direction
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
echartsNode.x =
|
||||
((Math.cos(angle) * containerWidth) / 2) * node.polarDistance;
|
||||
echartsNode.y =
|
||||
((Math.sin(angle) * containerHeight) / 2) * node.polarDistance;
|
||||
this._nodePositions[node.id] = {
|
||||
x: echartsNode.x,
|
||||
y: echartsNode.y,
|
||||
};
|
||||
}
|
||||
return echartsNode;
|
||||
}),
|
||||
links: data.links.map((link) => ({
|
||||
...link,
|
||||
value: link.reverseValue
|
||||
? Math.max(link.value ?? 0, link.reverseValue)
|
||||
: link.value,
|
||||
// remove arrow for bidirectional links
|
||||
symbolSize: link.reverseValue ? 1 : link.symbolSize, // 0 doesn't work
|
||||
})),
|
||||
categories: data.categories || [],
|
||||
};
|
||||
},
|
||||
deepEqual
|
||||
);
|
||||
|
||||
private _togglePhysics() {
|
||||
this._saveNodePositions();
|
||||
this._physicsEnabled = !this._physicsEnabled;
|
||||
}
|
||||
|
||||
private _toggleLabels() {
|
||||
this._showLabels = !this._showLabels;
|
||||
}
|
||||
|
||||
private _saveNodePositions() {
|
||||
if (this._baseChart?.chart) {
|
||||
this._baseChart.chart
|
||||
// @ts-ignore private method but no other way to get the graph positions
|
||||
@ -265,11 +267,6 @@ export class HaNetworkGraph extends LitElement {
|
||||
}
|
||||
});
|
||||
}
|
||||
this._physicsEnabled = !this._physicsEnabled;
|
||||
}
|
||||
|
||||
private _toggleLabels() {
|
||||
this._showLabels = !this._showLabels;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
@ -111,7 +111,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
this._chartData.forEach((dataset, index) => {
|
||||
if (
|
||||
dataset.tooltip?.show === false ||
|
||||
this._hiddenStats.has(dataset.name as string)
|
||||
this._hiddenStats.has(dataset.id as string)
|
||||
)
|
||||
return;
|
||||
const param = params.find(
|
||||
@ -185,11 +185,11 @@ export class StateHistoryChartLine extends LitElement {
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
this._hiddenStats.add(ev.detail.name);
|
||||
this._hiddenStats.add(ev.detail.id);
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev: CustomEvent) {
|
||||
this._hiddenStats.delete(ev.detail.name);
|
||||
this._hiddenStats.delete(ev.detail.id);
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
} from "../../data/recorder";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
|
||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
@ -96,7 +97,7 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
|
||||
@state() private _legendData: string[] = [];
|
||||
@state() private _legendData: CustomLegendOption["data"];
|
||||
|
||||
@state() private _statisticIds: string[] = [];
|
||||
|
||||
@ -106,6 +107,8 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
private _computedStyle?: CSSStyleDeclaration;
|
||||
|
||||
private _previousYAxisLabelValue = 0;
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return changedProps.size > 1 || !changedProps.has("hass");
|
||||
}
|
||||
@ -181,12 +184,18 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
this._hiddenStats.add(ev.detail.name);
|
||||
if (!this._legendData) {
|
||||
return;
|
||||
}
|
||||
this._hiddenStats.add(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev: CustomEvent) {
|
||||
this._hiddenStats.delete(ev.detail.name);
|
||||
if (!this._legendData) {
|
||||
return;
|
||||
}
|
||||
this._hiddenStats.delete(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
@ -197,8 +206,8 @@ export class StatisticsChart extends LitElement {
|
||||
: "";
|
||||
return params
|
||||
.map((param, index: number) => {
|
||||
if (rendered[param.seriesName]) return "";
|
||||
rendered[param.seriesName] = true;
|
||||
if (rendered[param.seriesIndex]) return "";
|
||||
rendered[param.seriesIndex] = true;
|
||||
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
@ -314,6 +323,9 @@ export class StatisticsChart extends LitElement {
|
||||
splitLine: {
|
||||
show: true,
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: this._formatYAxisLabel,
|
||||
} as any,
|
||||
},
|
||||
legend: {
|
||||
type: "custom",
|
||||
@ -362,6 +374,7 @@ export class StatisticsChart extends LitElement {
|
||||
const statisticsData = Object.entries(this.statisticsData);
|
||||
const totalDataSets: typeof this._chartData = [];
|
||||
const legendData: {
|
||||
id: string;
|
||||
name: string;
|
||||
color?: ZRColor;
|
||||
borderColor?: ZRColor;
|
||||
@ -465,6 +478,8 @@ export class StatisticsChart extends LitElement {
|
||||
this.statTypes.includes("min") && statisticsHaveType(stats, "min");
|
||||
const drawBands = [hasMean, hasMax, hasMin].filter(Boolean).length > 1;
|
||||
|
||||
const hasState = this.statTypes.includes("state");
|
||||
|
||||
const bandTop = hasMax ? "max" : "mean";
|
||||
const bandBottom = hasMin ? "min" : "mean";
|
||||
|
||||
@ -486,7 +501,8 @@ export class StatisticsChart extends LitElement {
|
||||
const band = drawBands && (type === bandTop || type === bandBottom);
|
||||
statTypes.push(type);
|
||||
const borderColor =
|
||||
band && hasMin && hasMax && hasMean
|
||||
(band && hasMin && hasMax && hasMean) ||
|
||||
(hasState && ["change", "sum"].includes(type))
|
||||
? color + (this.hideLegend ? "00" : "7F")
|
||||
: color;
|
||||
const backgroundColor = band ? color + "3F" : color + "7F";
|
||||
@ -535,6 +551,7 @@ export class StatisticsChart extends LitElement {
|
||||
: displayedLegend === false;
|
||||
if (showLegend) {
|
||||
statLegendData.push({
|
||||
id: statistic_id,
|
||||
name,
|
||||
color: series.color as ZRColor,
|
||||
borderColor: series.itemStyle?.borderColor,
|
||||
@ -579,7 +596,7 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
dataValues.push(val);
|
||||
});
|
||||
if (!this._hiddenStats.has(name)) {
|
||||
if (!this._hiddenStats.has(statistic_id)) {
|
||||
pushData(startDate, new Date(stat.end), dataValues);
|
||||
}
|
||||
});
|
||||
@ -593,10 +610,10 @@ export class StatisticsChart extends LitElement {
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
legendData.forEach(({ name, color, borderColor }) => {
|
||||
legendData.forEach(({ id, name, color, borderColor }) => {
|
||||
// Add an empty series for the legend
|
||||
totalDataSets.push({
|
||||
id: name + "-legend",
|
||||
id: id,
|
||||
name: name,
|
||||
color,
|
||||
itemStyle: {
|
||||
@ -609,9 +626,13 @@ export class StatisticsChart extends LitElement {
|
||||
});
|
||||
|
||||
this._chartData = totalDataSets;
|
||||
if (legendData.length !== this._legendData.length) {
|
||||
if (legendData.length !== this._legendData?.length) {
|
||||
// only update the legend if it has changed or it will trigger options update
|
||||
this._legendData = legendData.map(({ name }) => name);
|
||||
this._legendData =
|
||||
legendData.length > 1
|
||||
? legendData.map(({ id, name }) => ({ id, name }))
|
||||
: // if there is only one entity, let the base chart handle the legend
|
||||
undefined;
|
||||
}
|
||||
this._statisticIds = statisticIds;
|
||||
}
|
||||
@ -640,6 +661,22 @@ export class StatisticsChart extends LitElement {
|
||||
return Math.abs(value) < 1 ? value : roundingFn(value);
|
||||
}
|
||||
|
||||
private _formatYAxisLabel = (value: number) => {
|
||||
// show the first significant digit for tiny values
|
||||
const maximumFractionDigits = Math.max(
|
||||
1,
|
||||
// use the difference to the previous value to determine the number of significant digits #25526
|
||||
-Math.floor(
|
||||
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
|
||||
)
|
||||
);
|
||||
const label = formatNumber(value, this.hass.locale, {
|
||||
maximumFractionDigits,
|
||||
});
|
||||
this._previousYAxisLabelValue = value;
|
||||
return label;
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
@ -507,7 +507,9 @@ export class HaDataTable extends LitElement {
|
||||
this.hasFab,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
this._collapsedGroups
|
||||
this._collapsedGroups,
|
||||
this.sortColumn,
|
||||
this.sortDirection
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${renderRow}
|
||||
@ -702,22 +704,37 @@ export class HaDataTable extends LitElement {
|
||||
hasFab: boolean,
|
||||
groupColumn: string | undefined,
|
||||
groupOrder: string[] | undefined,
|
||||
collapsedGroups: string[]
|
||||
collapsedGroups: string[],
|
||||
sortColumn: string | undefined,
|
||||
sortDirection: SortingDirection
|
||||
) => {
|
||||
if (appendRow || hasFab || groupColumn) {
|
||||
let items = [...data];
|
||||
|
||||
if (groupColumn) {
|
||||
const isGroupSortColumn = sortColumn === groupColumn;
|
||||
const grouped = groupBy(items, (item) => item[groupColumn]);
|
||||
if (grouped.undefined) {
|
||||
// make sure ungrouped items are at the bottom
|
||||
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
||||
delete grouped.undefined;
|
||||
}
|
||||
const sorted: Record<string, DataTableRowData[]> = Object.keys(
|
||||
const sortedEntries: [string, DataTableRowData[]][] = Object.keys(
|
||||
grouped
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (!groupOrder && isGroupSortColumn) {
|
||||
const comparison = stringCompare(
|
||||
a,
|
||||
b,
|
||||
this.hass.locale.language
|
||||
);
|
||||
if (sortDirection === "asc") {
|
||||
return comparison;
|
||||
}
|
||||
return comparison * -1;
|
||||
}
|
||||
|
||||
const orderA = groupOrder?.indexOf(a) ?? -1;
|
||||
const orderB = groupOrder?.indexOf(b) ?? -1;
|
||||
if (orderA !== orderB) {
|
||||
@ -735,12 +752,18 @@ export class HaDataTable extends LitElement {
|
||||
this.hass.locale.language
|
||||
);
|
||||
})
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = grouped[key];
|
||||
return obj;
|
||||
}, {});
|
||||
.reduce(
|
||||
(entries, key) => {
|
||||
const entry: [string, DataTableRowData[]] = [key, grouped[key]];
|
||||
|
||||
entries.push(entry);
|
||||
return entries;
|
||||
},
|
||||
[] as [string, DataTableRowData[]][]
|
||||
);
|
||||
|
||||
const groupedItems: DataTableRowData[] = [];
|
||||
Object.entries(sorted).forEach(([groupName, rows]) => {
|
||||
sortedEntries.forEach(([groupName, rows]) => {
|
||||
const collapsed = collapsedGroups.includes(groupName);
|
||||
groupedItems.push({
|
||||
append: true,
|
||||
@ -836,7 +859,9 @@ export class HaDataTable extends LitElement {
|
||||
this.hasFab,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
this._collapsedGroups
|
||||
this._collapsedGroups,
|
||||
this.sortColumn,
|
||||
this.sortDirection
|
||||
);
|
||||
|
||||
if (
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { mdiDrag } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-sortable";
|
||||
import "./ha-entity-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
|
||||
|
||||
@ -76,6 +78,9 @@ class HaEntitiesPicker extends LitElement {
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
|
||||
@property({ type: Boolean })
|
||||
public reorder = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
@ -84,28 +89,44 @@ class HaEntitiesPicker extends LitElement {
|
||||
const currentEntities = this._currentEntities;
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${currentEntities.map(
|
||||
(entityId) => html`
|
||||
<div>
|
||||
<ha-entity-picker
|
||||
allow-custom-entity
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.value=${entityId}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this.createDomains}
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<ha-sortable
|
||||
.disabled=${!this.reorder || this.disabled}
|
||||
handle-selector=".entity-handle"
|
||||
@item-moved=${this._entityMoved}
|
||||
>
|
||||
<div class="list">
|
||||
${currentEntities.map(
|
||||
(entityId) => html`
|
||||
<div class="entity">
|
||||
<ha-entity-picker
|
||||
allow-custom-entity
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.value=${entityId}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this.createDomains}
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
${this.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="entity-handle"
|
||||
.path=${mdiDrag}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
<div>
|
||||
<ha-entity-picker
|
||||
allow-custom-entity
|
||||
@ -131,6 +152,17 @@ class HaEntitiesPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _entityMoved(e: CustomEvent) {
|
||||
e.stopPropagation();
|
||||
const { oldIndex, newIndex } = e.detail;
|
||||
const currentEntities = this._currentEntities;
|
||||
const movedEntity = currentEntities[oldIndex];
|
||||
const newEntities = [...currentEntities];
|
||||
newEntities.splice(oldIndex, 1);
|
||||
newEntities.splice(newIndex, 0, movedEntity);
|
||||
this._updateEntities(newEntities);
|
||||
}
|
||||
|
||||
private _excludeEntities = memoizeOne(
|
||||
(
|
||||
value: string[] | undefined,
|
||||
@ -201,6 +233,19 @@ class HaEntitiesPicker extends LitElement {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.entity {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.entity ha-entity-picker {
|
||||
flex: 1;
|
||||
}
|
||||
.entity-handle {
|
||||
padding: 8px;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -2,18 +2,24 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
|
||||
interface AttributeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@customElement("ha-entity-attribute-picker")
|
||||
class HaEntityAttributePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
@property({ attribute: false }) public entityId?: string | string[];
|
||||
|
||||
/**
|
||||
* List of attributes to be hidden.
|
||||
@ -48,23 +54,40 @@ class HaEntityAttributePicker extends LitElement {
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("_opened") && this._opened) {
|
||||
const entityState = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
(this._comboBox as any).items = entityState
|
||||
? Object.keys(entityState.attributes)
|
||||
.filter((key) => !this.hideAttributes?.includes(key))
|
||||
.map((key) => ({
|
||||
value: key,
|
||||
label: computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
entityState,
|
||||
this.hass.entities,
|
||||
key
|
||||
),
|
||||
}))
|
||||
: [];
|
||||
if (
|
||||
(changedProps.has("_opened") && this._opened) ||
|
||||
changedProps.has("entityId") ||
|
||||
changedProps.has("attribute")
|
||||
) {
|
||||
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
||||
const entitiesOptions = entityIds.map<AttributeOption[]>((entityId) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const attributes = Object.keys(stateObj.attributes).filter(
|
||||
(a) => !this.hideAttributes?.includes(a)
|
||||
);
|
||||
|
||||
return attributes.map((a) => ({
|
||||
value: a,
|
||||
label: this.hass.formatEntityAttributeName(stateObj, a),
|
||||
}));
|
||||
});
|
||||
|
||||
const options: AttributeOption[] = [];
|
||||
const optionsSet = new Set<string>();
|
||||
for (const entityOptions of entitiesOptions) {
|
||||
for (const option of entityOptions) {
|
||||
if (!optionsSet.has(option.value)) {
|
||||
optionsSet.add(option.value);
|
||||
options.push(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(this._comboBox as any).filteredItems = options;
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,21 +96,10 @@ class HaEntityAttributePicker extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this.entityId!] as HassEntity | undefined;
|
||||
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.value=${this.value
|
||||
? stateObj
|
||||
? computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.value
|
||||
)
|
||||
: this.value
|
||||
: ""}
|
||||
.value=${this.value}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label ??
|
||||
this.hass.localize(
|
||||
@ -97,6 +109,7 @@ class HaEntityAttributePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
item-id-path="value"
|
||||
item-value-path="value"
|
||||
item-label-path="label"
|
||||
@opened-changed=${this._openedChanged}
|
||||
@ -106,12 +119,28 @@ class HaEntityAttributePicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
this.value = ev.detail.value;
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { getStates } from "../../common/entity/get_states";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
@ -10,11 +11,16 @@ import type { HaComboBox } from "../ha-combo-box";
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
|
||||
interface StateOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@customElement("ha-entity-state-picker")
|
||||
class HaEntityStatePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
@property({ attribute: false }) public entityId?: string | string[];
|
||||
|
||||
@property() public attribute?: string;
|
||||
|
||||
@ -30,6 +36,9 @@ class HaEntityStatePicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-custom-value" })
|
||||
public allowCustomValue;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hideStates?: string[];
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
@ -51,24 +60,42 @@ class HaEntityStatePicker extends LitElement {
|
||||
changedProps.has("attribute") ||
|
||||
changedProps.has("extraOptions")
|
||||
) {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
(this._comboBox as any).items = [
|
||||
...(this.extraOptions ?? []),
|
||||
...(this.entityId && stateObj
|
||||
? getStates(this.hass, stateObj, this.attribute).map((key) => ({
|
||||
value: key,
|
||||
label: !this.attribute
|
||||
? this.hass.formatEntityState(stateObj, key)
|
||||
: this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
this.attribute,
|
||||
key
|
||||
),
|
||||
}))
|
||||
: []),
|
||||
];
|
||||
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
||||
|
||||
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const states = getStates(this.hass, stateObj, this.attribute).filter(
|
||||
(s) => !this.hideStates?.includes(s)
|
||||
);
|
||||
|
||||
return states.map((s) => ({
|
||||
value: s,
|
||||
label: this.attribute
|
||||
? this.hass.formatEntityAttributeValue(stateObj, this.attribute, s)
|
||||
: this.hass.formatEntityState(stateObj, s),
|
||||
}));
|
||||
});
|
||||
|
||||
const options: StateOption[] = [];
|
||||
const optionsSet = new Set<string>();
|
||||
for (const entityOptions of entitiesOptions) {
|
||||
for (const option of entityOptions) {
|
||||
if (!optionsSet.has(option.value)) {
|
||||
optionsSet.add(option.value);
|
||||
options.push(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.extraOptions) {
|
||||
options.unshift(...this.extraOptions);
|
||||
}
|
||||
|
||||
(this._comboBox as any).filteredItems = options;
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +115,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
item-id-path="value"
|
||||
item-value-path="value"
|
||||
item-label-path="label"
|
||||
@opened-changed=${this._openedChanged}
|
||||
|
@ -366,6 +366,7 @@ export class HaAreaPicker extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.area-picker.no_match"
|
||||
)}
|
||||
@ -410,6 +411,7 @@ export class HaAreaPicker extends LitElement {
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._setValue(value);
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { mdiDrag, mdiTextureBox } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { areaCompare } from "../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
@ -17,9 +18,14 @@ import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
|
||||
export interface AreasDisplayValue {
|
||||
hidden?: string[];
|
||||
order?: string[];
|
||||
export interface AreasFloorsDisplayValue {
|
||||
areas_display?: {
|
||||
hidden?: string[];
|
||||
order?: string[];
|
||||
};
|
||||
floors_display?: {
|
||||
order?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
||||
@ -30,65 +36,96 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: AreasDisplayValue;
|
||||
@property({ attribute: false }) public value?: AreasFloorsDisplayValue;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public expanded = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: false }) public actionsRenderer?: () =>
|
||||
| TemplateResult<1>
|
||||
| typeof nothing;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-navigation-button" })
|
||||
public showNavigationButton = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const groupedItems = this._groupedItems(this.hass.areas, this.hass.floors);
|
||||
const groupedAreasItems = this._groupedAreasItems(
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const filteredFloors = this._sortedFloors(this.hass.floors).filter(
|
||||
const filteredFloors = this._sortedFloors(
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
).filter(
|
||||
(floor) =>
|
||||
// Only include floors that have areas assigned to them
|
||||
groupedItems[floor.floor_id]?.length > 0
|
||||
groupedAreasItems[floor.floor_id]?.length > 0
|
||||
);
|
||||
|
||||
const value: DisplayValue = {
|
||||
order: this.value?.order ?? [],
|
||||
hidden: this.value?.hidden ?? [],
|
||||
order: this.value?.areas_display?.order ?? [],
|
||||
hidden: this.value?.areas_display?.hidden ?? [],
|
||||
};
|
||||
|
||||
const canReorderFloors =
|
||||
filteredFloors.filter((floor) => floor.floor_id !== UNASSIGNED_FLOOR)
|
||||
.length > 1;
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.label}
|
||||
.expanded=${this.expanded}
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-sortable
|
||||
draggable-selector=".draggable"
|
||||
handle-selector=".handle"
|
||||
@item-moved=${this._floorMoved}
|
||||
.disabled=${this.disabled || !canReorderFloors}
|
||||
invert-swap
|
||||
>
|
||||
<ha-svg-icon slot="leading-icon" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
${filteredFloors.map(
|
||||
(floor) => html`
|
||||
<div class="floor">
|
||||
<div class="header">
|
||||
<ha-floor-icon .floor=${floor}></ha-floor-icon>
|
||||
<p>${computeFloorName(floor)}</p>
|
||||
</div>
|
||||
<div class="areas">
|
||||
<div>
|
||||
${repeat(
|
||||
filteredFloors,
|
||||
(floor) => floor.floor_id,
|
||||
(floor: FloorRegistryEntry) => html`
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${computeFloorName(floor)}
|
||||
left-chevron
|
||||
class=${floor.floor_id === UNASSIGNED_FLOOR ? "" : "draggable"}
|
||||
>
|
||||
<ha-floor-icon
|
||||
slot="leading-icon"
|
||||
.floor=${floor}
|
||||
></ha-floor-icon>
|
||||
${floor.floor_id === UNASSIGNED_FLOOR || !canReorderFloors
|
||||
? nothing
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
class="handle"
|
||||
slot="icons"
|
||||
.path=${mdiDrag}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${groupedItems[floor.floor_id] || []}
|
||||
.items=${groupedAreasItems[floor.floor_id]}
|
||||
.value=${value}
|
||||
.floorId=${floor.floor_id}
|
||||
.actionsRenderer=${this.actionsRenderer}
|
||||
@value-changed=${this._areaDisplayChanged}
|
||||
.showNavigationButton=${this.showNavigationButton}
|
||||
></ha-items-display-editor>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-expansion-panel>
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
`;
|
||||
}
|
||||
|
||||
private _groupedItems = memoizeOne(
|
||||
private _groupedAreasItems = memoizeOne(
|
||||
(
|
||||
hassAreas: HomeAssistant["areas"],
|
||||
// update items if floors change
|
||||
@ -112,7 +149,6 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
label: area.name,
|
||||
icon: area.icon ?? undefined,
|
||||
iconPath: mdiTextureBox,
|
||||
description: floor?.name,
|
||||
});
|
||||
|
||||
return acc;
|
||||
@ -124,20 +160,19 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
);
|
||||
|
||||
private _sortedFloors = memoizeOne(
|
||||
(hassFloors: HomeAssistant["floors"]): FloorRegistryEntry[] => {
|
||||
const floors = Object.values(hassFloors).sort((floorA, floorB) => {
|
||||
if (floorA.level !== floorB.level) {
|
||||
return (floorA.level ?? 0) - (floorB.level ?? 0);
|
||||
}
|
||||
return stringCompare(floorA.name, floorB.name);
|
||||
});
|
||||
(
|
||||
hassFloors: HomeAssistant["floors"],
|
||||
order: string[] | undefined
|
||||
): FloorRegistryEntry[] => {
|
||||
const floors = getFloors(hassFloors, order);
|
||||
const noFloors = floors.length === 0;
|
||||
floors.push({
|
||||
floor_id: UNASSIGNED_FLOOR,
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.strategy.areas.unassigned_areas"
|
||||
),
|
||||
name: noFloors
|
||||
? this.hass.localize("ui.panel.lovelace.strategy.areas.areas")
|
||||
: this.hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
||||
icon: null,
|
||||
level: 999999,
|
||||
level: null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
@ -146,68 +181,101 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private async _areaDisplayChanged(ev) {
|
||||
private _floorMoved(ev: CustomEvent<HASSDomEvents["item-moved"]>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as DisplayValue;
|
||||
const currentFloorId = ev.currentTarget.floorId;
|
||||
const newIndex = ev.detail.newIndex;
|
||||
const oldIndex = ev.detail.oldIndex;
|
||||
const floorIds = this._sortedFloors(
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
).map((floor) => floor.floor_id);
|
||||
const newOrder = [...floorIds];
|
||||
const movedFloorId = newOrder.splice(oldIndex, 1)[0];
|
||||
newOrder.splice(newIndex, 0, movedFloorId);
|
||||
const newValue: AreasFloorsDisplayValue = {
|
||||
areas_display: this.value?.areas_display,
|
||||
floors_display: {
|
||||
order: newOrder,
|
||||
},
|
||||
};
|
||||
if (newValue.floors_display?.order?.length === 0) {
|
||||
delete newValue.floors_display.order;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
const floorIds = this._sortedFloors(this.hass.floors).map(
|
||||
(floor) => floor.floor_id
|
||||
);
|
||||
private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const currentFloorId = (ev.currentTarget as any).floorId;
|
||||
|
||||
const floorIds = this._sortedFloors(
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
).map((floor) => floor.floor_id);
|
||||
|
||||
const oldAreaDisplay = this.value?.areas_display ?? {};
|
||||
|
||||
const oldHidden = oldAreaDisplay?.hidden ?? [];
|
||||
const oldOrder = oldAreaDisplay?.order ?? [];
|
||||
|
||||
const newHidden: string[] = [];
|
||||
const newOrder: string[] = [];
|
||||
|
||||
for (const floorId of floorIds) {
|
||||
if (currentFloorId === floorId) {
|
||||
if ((currentFloorId ?? UNASSIGNED_FLOOR) === floorId) {
|
||||
newHidden.push(...(value.hidden ?? []));
|
||||
newOrder.push(...(value.order ?? []));
|
||||
continue;
|
||||
}
|
||||
const hidden = this.value?.hidden?.filter(
|
||||
(areaId) => this.hass.areas[areaId]?.floor_id === floorId
|
||||
);
|
||||
if (hidden) {
|
||||
const hidden = oldHidden.filter((areaId) => {
|
||||
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
if (hidden?.length) {
|
||||
newHidden.push(...hidden);
|
||||
}
|
||||
const order = this.value?.order?.filter(
|
||||
(areaId) => this.hass.areas[areaId]?.floor_id === floorId
|
||||
);
|
||||
if (order) {
|
||||
const order = oldOrder.filter((areaId) => {
|
||||
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
if (order?.length) {
|
||||
newOrder.push(...order);
|
||||
}
|
||||
}
|
||||
|
||||
const newValue: AreasDisplayValue = {
|
||||
hidden: newHidden,
|
||||
order: newOrder,
|
||||
const newValue: AreasFloorsDisplayValue = {
|
||||
areas_display: {
|
||||
hidden: newHidden,
|
||||
order: newOrder,
|
||||
},
|
||||
floors_display: this.value?.floors_display,
|
||||
};
|
||||
if (newValue.hidden?.length === 0) {
|
||||
delete newValue.hidden;
|
||||
if (newValue.areas_display?.hidden?.length === 0) {
|
||||
delete newValue.areas_display.hidden;
|
||||
}
|
||||
if (newValue.order?.length === 0) {
|
||||
delete newValue.order;
|
||||
if (newValue.areas_display?.order?.length === 0) {
|
||||
delete newValue.areas_display.order;
|
||||
}
|
||||
if (newValue.floors_display?.order?.length === 0) {
|
||||
delete newValue.floors_display.order;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.floor .header p {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex: 1:
|
||||
ha-expansion-panel {
|
||||
margin-bottom: 8px;
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
}
|
||||
.floor .header {
|
||||
margin: 16px 0 8px 0;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
ha-expansion-panel [slot="leading-icon"] {
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -61,8 +61,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
@property({ type: Boolean }) public error = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "enable-fullscreen" })
|
||||
public enableFullscreen = true;
|
||||
@property({ type: Boolean, attribute: "disable-fullscreen" })
|
||||
public disableFullscreen = false;
|
||||
|
||||
@state() private _value = "";
|
||||
|
||||
@ -179,7 +179,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
if (changedProps.has("_isFullscreen")) {
|
||||
this.classList.toggle("fullscreen", this._isFullscreen);
|
||||
}
|
||||
if (changedProps.has("enableFullscreen")) {
|
||||
if (changedProps.has("disableFullscreen")) {
|
||||
this._updateFullscreenButton();
|
||||
}
|
||||
}
|
||||
@ -263,7 +263,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
private _updateFullscreenButton() {
|
||||
const existingButton = this.renderRoot.querySelector(".fullscreen-button");
|
||||
|
||||
if (!this.enableFullscreen) {
|
||||
if (this.disableFullscreen) {
|
||||
// Remove button if it exists and fullscreen is disabled
|
||||
if (existingButton) {
|
||||
existingButton.remove();
|
||||
@ -317,7 +317,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
} else if (e.key === "F11" && this.enableFullscreen) {
|
||||
} else if (e.key === "F11" && !this.disableFullscreen) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
@ -557,11 +557,11 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 10;
|
||||
z-index: 1;
|
||||
color: var(--secondary-text-color);
|
||||
background-color: var(--card-background-color);
|
||||
background-color: var(--secondary-background-color);
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s;
|
||||
--mdc-icon-button-size: 32px;
|
||||
--mdc-icon-size: 18px;
|
||||
@ -584,22 +584,25 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
:host(.fullscreen) {
|
||||
position: fixed !important;
|
||||
top: var(--header-height, 56px) !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
top: calc(var(--header-height, 56px) + 8px) !important;
|
||||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
bottom: 8px !important;
|
||||
z-index: 9999 !important;
|
||||
background-color: var(--primary-background-color) !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
|
||||
overflow: hidden !important;
|
||||
background-color: var(
|
||||
--code-editor-background-color,
|
||||
var(--card-background-color)
|
||||
) !important;
|
||||
margin: 0 !important;
|
||||
padding: 16px !important;
|
||||
/* Respect iOS safe areas while accounting for header */
|
||||
padding-top: max(16px, env(safe-area-inset-top)) !important;
|
||||
padding-left: max(16px, env(safe-area-inset-left)) !important;
|
||||
padding-right: max(16px, env(safe-area-inset-right)) !important;
|
||||
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
|
||||
padding-top: var(--safe-area-inset-top) !important;
|
||||
padding-left: var(--safe-area-inset-left) !important;
|
||||
padding-right: var(--safe-area-inset-right) !important;
|
||||
padding-bottom: var(--safe-area-inset-bottom) !important;
|
||||
box-sizing: border-box !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
:host(.fullscreen) .cm-editor {
|
||||
@ -609,12 +612,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
|
||||
:host(.fullscreen) .fullscreen-button {
|
||||
position: fixed;
|
||||
top: calc(
|
||||
var(--header-height, 56px) + max(8px, env(safe-area-inset-top))
|
||||
);
|
||||
right: max(24px, calc(env(safe-area-inset-right) + 8px));
|
||||
z-index: 10000;
|
||||
top: calc(var(--safe-area-inset-top, 0px) + 8px);
|
||||
right: calc(var(--safe-area-inset-right, 0px) + 8px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ export class HaComboBox extends LitElement {
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
aria-label=${ifDefined(this.hass?.localize("ui.common.clear"))}
|
||||
class="clear-button"
|
||||
class=${`clear-button ${this.label ? "" : "no-label"}`}
|
||||
.path=${mdiClose}
|
||||
@click=${this._clearValue}
|
||||
></ha-svg-icon>`
|
||||
@ -215,7 +215,7 @@ export class HaComboBox extends LitElement {
|
||||
tabindex="-1"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
aria-expanded=${this.opened ? "true" : "false"}
|
||||
class="toggle-button"
|
||||
class=${`toggle-button ${this.label ? "" : "no-label"}`}
|
||||
.path=${this.opened ? mdiMenuUp : mdiMenuDown}
|
||||
?disabled=${this.disabled}
|
||||
@click=${this._toggleOpen}
|
||||
@ -369,7 +369,6 @@ export class HaComboBox extends LitElement {
|
||||
}
|
||||
vaadin-combo-box-light {
|
||||
position: relative;
|
||||
--vaadin-combo-box-overlay-max-height: calc(45vh - 56px);
|
||||
}
|
||||
ha-combo-box-textfield {
|
||||
width: 100%;
|
||||
@ -398,6 +397,9 @@ export class HaComboBox extends LitElement {
|
||||
color: var(--disabled-text-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
.toggle-button.no-label {
|
||||
top: -3px;
|
||||
}
|
||||
.clear-button {
|
||||
--mdc-icon-size: 20px;
|
||||
top: -7px;
|
||||
@ -406,6 +408,9 @@ export class HaComboBox extends LitElement {
|
||||
inset-inline-end: 36px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.clear-button.no-label {
|
||||
top: 0;
|
||||
}
|
||||
ha-input-helper-text {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ export class HaControlButtonGroup extends LitElement {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: var(--control-button-group-alignment, start);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@ -17,7 +17,7 @@ export interface ControlSelectOption {
|
||||
|
||||
@customElement("ha-control-select")
|
||||
export class HaControlSelect extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
@property({ attribute: false }) public options?: ControlSelectOption[];
|
||||
|
||||
@ -26,94 +26,70 @@ export class HaControlSelect extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public vertical = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-label" })
|
||||
public hideLabel = false;
|
||||
@property({ type: Boolean, attribute: "hide-option-label" })
|
||||
public hideOptionLabel = false;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@state() private _activeIndex?: number;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.setAttribute("role", "listbox");
|
||||
if (!this.hasAttribute("tabindex")) {
|
||||
this.setAttribute("tabindex", "0");
|
||||
private _handleFocus(ev: FocusEvent) {
|
||||
if (this.disabled || !this.options) return;
|
||||
|
||||
// Only handle focus if coming to the container
|
||||
if (ev.target === ev.currentTarget) {
|
||||
// Focus the selected radio or the first one
|
||||
const selectedIndex =
|
||||
this.value != null
|
||||
? this.options.findIndex((option) => option.value === this.value)
|
||||
: -1;
|
||||
const focusIndex = selectedIndex !== -1 ? selectedIndex : 0;
|
||||
this._focusOption(focusIndex);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("_activeIndex")) {
|
||||
const activeValue =
|
||||
this._activeIndex != null
|
||||
? this.options?.[this._activeIndex]?.value
|
||||
: undefined;
|
||||
const activedescendant =
|
||||
activeValue != null ? `option-${activeValue}` : undefined;
|
||||
this.setAttribute("aria-activedescendant", activedescendant ?? "");
|
||||
private _focusOption(index: number) {
|
||||
this._activeIndex = index;
|
||||
this.requestUpdate();
|
||||
this.updateComplete.then(() => {
|
||||
const option = this.shadowRoot?.querySelector(
|
||||
`#option-${this.options![index].value}`
|
||||
) as HTMLElement;
|
||||
option?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private _handleBlur(ev: FocusEvent) {
|
||||
// Only reset if focus is leaving the entire component
|
||||
if (!this.contains(ev.relatedTarget as Node)) {
|
||||
this._activeIndex = undefined;
|
||||
}
|
||||
if (changedProps.has("vertical")) {
|
||||
const orientation = this.vertical ? "vertical" : "horizontal";
|
||||
this.setAttribute("aria-orientation", orientation);
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._setupListeners();
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._destroyListeners();
|
||||
}
|
||||
|
||||
private _setupListeners() {
|
||||
this.addEventListener("focus", this._handleFocus);
|
||||
this.addEventListener("blur", this._handleBlur);
|
||||
this.addEventListener("keydown", this._handleKeydown);
|
||||
}
|
||||
|
||||
private _destroyListeners() {
|
||||
this.removeEventListener("focus", this._handleFocus);
|
||||
this.removeEventListener("blur", this._handleBlur);
|
||||
this.removeEventListener("keydown", this._handleKeydown);
|
||||
}
|
||||
|
||||
private _handleFocus() {
|
||||
if (this.disabled) return;
|
||||
this._activeIndex =
|
||||
(this.value != null
|
||||
? this.options?.findIndex((option) => option.value === this.value)
|
||||
: undefined) ?? 0;
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
this._activeIndex = undefined;
|
||||
}
|
||||
|
||||
private _handleKeydown(ev: KeyboardEvent) {
|
||||
if (!this.options || this._activeIndex == null || this.disabled) return;
|
||||
const value = this.options[this._activeIndex].value;
|
||||
if (!this.options || this.disabled) return;
|
||||
|
||||
let newIndex = this._activeIndex ?? 0;
|
||||
|
||||
switch (ev.key) {
|
||||
case " ":
|
||||
this.value = value;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
case "Enter":
|
||||
if (this._activeIndex != null) {
|
||||
const value = this.options[this._activeIndex].value;
|
||||
this.value = value;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
break;
|
||||
case "ArrowUp":
|
||||
case "ArrowLeft":
|
||||
this._activeIndex =
|
||||
this._activeIndex <= 0
|
||||
? this.options.length - 1
|
||||
: this._activeIndex - 1;
|
||||
newIndex = newIndex <= 0 ? this.options.length - 1 : newIndex - 1;
|
||||
this._focusOption(newIndex);
|
||||
break;
|
||||
case "ArrowDown":
|
||||
case "ArrowRight":
|
||||
this._activeIndex = (this._activeIndex + 1) % this.options.length;
|
||||
break;
|
||||
case "Home":
|
||||
this._activeIndex = 0;
|
||||
break;
|
||||
case "End":
|
||||
this._activeIndex = this.options.length - 1;
|
||||
newIndex = (newIndex + 1) % this.options.length;
|
||||
this._focusOption(newIndex);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
@ -139,38 +115,56 @@ export class HaControlSelect extends LitElement {
|
||||
|
||||
private _handleOptionMouseUp(ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
this._activeIndex = undefined;
|
||||
}
|
||||
|
||||
private _handleOptionFocus(ev: FocusEvent) {
|
||||
if (this.disabled) return;
|
||||
const value = (ev.target as any).value;
|
||||
this._activeIndex = this.options?.findIndex(
|
||||
(option) => option.value === value
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div
|
||||
class="container"
|
||||
role="radiogroup"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@focus=${this._handleFocus}
|
||||
@blur=${this._handleBlur}
|
||||
@keydown=${this._handleKeydown}
|
||||
?disabled=${this.disabled}
|
||||
>
|
||||
${this.options
|
||||
? repeat(
|
||||
this.options,
|
||||
(option) => option.value,
|
||||
(option, idx) => this._renderOption(option, idx)
|
||||
(option) => this._renderOption(option)
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderOption(option: ControlSelectOption, index: number) {
|
||||
private _renderOption(option: ControlSelectOption) {
|
||||
const isSelected = this.value === option.value;
|
||||
|
||||
return html`
|
||||
<div
|
||||
id=${`option-${option.value}`}
|
||||
class=${classMap({
|
||||
option: true,
|
||||
selected: this.value === option.value,
|
||||
focused: this._activeIndex === index,
|
||||
selected: isSelected,
|
||||
})}
|
||||
role="option"
|
||||
role="radio"
|
||||
tabindex=${isSelected ? "0" : "-1"}
|
||||
.value=${option.value}
|
||||
aria-selected=${this.value === option.value}
|
||||
aria-checked=${isSelected ? "true" : "false"}
|
||||
aria-label=${ifDefined(option.label)}
|
||||
title=${ifDefined(option.label)}
|
||||
@click=${this._handleOptionClick}
|
||||
@focus=${this._handleOptionFocus}
|
||||
@mousedown=${this._handleOptionMouseDown}
|
||||
@mouseup=${this._handleOptionMouseUp}
|
||||
>
|
||||
@ -178,7 +172,7 @@ export class HaControlSelect extends LitElement {
|
||||
${option.path
|
||||
? html`<ha-svg-icon .path=${option.path}></ha-svg-icon>`
|
||||
: option.icon || nothing}
|
||||
${option.label && !this.hideLabel
|
||||
${option.label && !this.hideOptionLabel
|
||||
? html`<span>${option.label}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
@ -203,18 +197,12 @@ export class HaControlSelect extends LitElement {
|
||||
--mdc-icon-size: 20px;
|
||||
height: var(--control-select-thickness);
|
||||
width: 100%;
|
||||
border-radius: var(--control-select-border-radius);
|
||||
outline: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
font-style: normal;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-text-color);
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
:host(:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--control-select-color);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--control-select-thickness);
|
||||
height: 100%;
|
||||
@ -225,11 +213,12 @@ export class HaControlSelect extends LitElement {
|
||||
width: 100%;
|
||||
border-radius: var(--control-select-border-radius);
|
||||
transform: translateZ(0);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: var(--control-select-padding);
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
}
|
||||
.container::before {
|
||||
position: absolute;
|
||||
@ -240,6 +229,7 @@ export class HaControlSelect extends LitElement {
|
||||
width: 100%;
|
||||
background: var(--control-select-background);
|
||||
opacity: var(--control-select-background-opacity);
|
||||
border-radius: var(--control-select-border-radius);
|
||||
}
|
||||
|
||||
.container > *:not(:last-child) {
|
||||
@ -248,6 +238,16 @@ export class HaControlSelect extends LitElement {
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.container[disabled] {
|
||||
--control-select-color: var(--disabled-color);
|
||||
--control-select-focused-opacity: 0;
|
||||
color: var(--disabled-color);
|
||||
}
|
||||
|
||||
.container[disabled] .option {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.option {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
@ -258,9 +258,13 @@ export class HaControlSelect extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--control-select-button-border-radius);
|
||||
overflow: hidden;
|
||||
/* For safari border-radius overflow */
|
||||
z-index: 0;
|
||||
outline: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
}
|
||||
.option:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-select-color);
|
||||
}
|
||||
.content > *:not(:last-child) {
|
||||
margin-bottom: 4px;
|
||||
@ -274,11 +278,11 @@ export class HaControlSelect extends LitElement {
|
||||
width: 100%;
|
||||
background-color: var(--control-select-color);
|
||||
opacity: 0;
|
||||
border-radius: var(--control-select-button-border-radius);
|
||||
transition:
|
||||
background-color ease-in-out 180ms,
|
||||
opacity ease-in-out 80ms;
|
||||
}
|
||||
.option.focused::before,
|
||||
.option:hover::before {
|
||||
opacity: var(--control-select-focused-opacity);
|
||||
}
|
||||
@ -319,14 +323,6 @@ export class HaControlSelect extends LitElement {
|
||||
margin-inline-end: initial;
|
||||
margin-bottom: var(--control-select-padding);
|
||||
}
|
||||
:host([disabled]) {
|
||||
--control-select-color: var(--disabled-color);
|
||||
--control-select-focused-opacity: 0;
|
||||
color: var(--disabled-color);
|
||||
}
|
||||
:host([disabled]) .option {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -3,11 +3,12 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../common/translations/blank_before_unit";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@ -75,6 +76,9 @@ export class HaControlSlider extends LitElement {
|
||||
@property({ type: Number })
|
||||
public max = 100;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@state()
|
||||
public pressed = false;
|
||||
|
||||
@ -107,10 +111,6 @@ export class HaControlSlider extends LitElement {
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.setupListeners();
|
||||
this.setAttribute("role", "slider");
|
||||
if (!this.hasAttribute("tabindex")) {
|
||||
this.setAttribute("tabindex", "0");
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@ -197,9 +197,6 @@ export class HaControlSlider extends LitElement {
|
||||
this.value = this.steppedValue(this.percentageToValue(percentage));
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
});
|
||||
|
||||
this.addEventListener("keydown", this._handleKeyDown);
|
||||
this.addEventListener("keyup", this._handleKeyUp);
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,8 +205,6 @@ export class HaControlSlider extends LitElement {
|
||||
this._mc.destroy();
|
||||
this._mc = undefined;
|
||||
}
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
this.removeEventListener("keyup", this._handleKeyUp);
|
||||
}
|
||||
|
||||
private get _tenPercentStep() {
|
||||
@ -323,6 +318,7 @@ export class HaControlSlider extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valuenow = this.steppedValue(this.value ?? 0);
|
||||
return html`
|
||||
<div
|
||||
class="container${classMap({
|
||||
@ -332,7 +328,24 @@ export class HaControlSlider extends LitElement {
|
||||
"--value": `${this.valueToPercentage(this.value ?? 0)}`,
|
||||
})}
|
||||
>
|
||||
<div id="slider" class="slider">
|
||||
<div
|
||||
id="slider"
|
||||
class="slider"
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
aria-valuenow=${valuenow.toString()}
|
||||
aria-valuetext=${this._formatValue(valuenow)}
|
||||
aria-valuemin=${ifDefined(
|
||||
this.min != null ? this.min.toString() : undefined
|
||||
)}
|
||||
aria-valuemax=${ifDefined(
|
||||
this.max != null ? this.max.toString() : undefined
|
||||
)}
|
||||
aria-orientation=${this.vertical ? "vertical" : "horizontal"}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
>
|
||||
<div class="slider-track-background"></div>
|
||||
<slot name="background"></slot>
|
||||
${this.mode === "cursor"
|
||||
@ -371,12 +384,6 @@ export class HaControlSlider extends LitElement {
|
||||
--control-slider-tooltip-font-size: var(--ha-font-size-m);
|
||||
height: var(--control-slider-thickness);
|
||||
width: 100%;
|
||||
border-radius: var(--control-slider-border-radius);
|
||||
outline: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
}
|
||||
:host(:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--control-slider-color);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--control-slider-thickness);
|
||||
@ -471,9 +478,14 @@ export class HaControlSlider extends LitElement {
|
||||
width: 100%;
|
||||
border-radius: var(--control-slider-border-radius);
|
||||
transform: translateZ(0);
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
.slider:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-slider-color);
|
||||
}
|
||||
.slider * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -9,18 +9,19 @@ import {
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-control-switch")
|
||||
export class HaControlSwitch extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public vertical = false;
|
||||
|
||||
@property({ type: Boolean }) public reversed = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public checked = false;
|
||||
@property({ type: Boolean }) public checked = false;
|
||||
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided on icon slot to pass an <ha-icon slot="icon-on"> in)
|
||||
@property({ attribute: false, type: String }) pathOn?: string;
|
||||
@ -28,6 +29,9 @@ export class HaControlSwitch extends LitElement {
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
|
||||
@property({ attribute: false, type: String }) pathOff?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@property({ attribute: "touch-action" })
|
||||
public touchAction?: string;
|
||||
|
||||
@ -36,17 +40,6 @@ export class HaControlSwitch extends LitElement {
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.setupListeners();
|
||||
this.setAttribute("role", "switch");
|
||||
if (!this.hasAttribute("tabindex")) {
|
||||
this.setAttribute("tabindex", "0");
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("checked")) {
|
||||
this.setAttribute("aria-checked", this.checked ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
private _toggle() {
|
||||
@ -112,8 +105,6 @@ export class HaControlSwitch extends LitElement {
|
||||
if (this.disabled) return;
|
||||
this._toggle();
|
||||
});
|
||||
|
||||
this.addEventListener("keydown", this._keydown);
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,7 +113,6 @@ export class HaControlSwitch extends LitElement {
|
||||
this._mc.destroy();
|
||||
this._mc = undefined;
|
||||
}
|
||||
this.removeEventListener("keydown", this._keydown);
|
||||
}
|
||||
|
||||
private _keydown(ev: any) {
|
||||
@ -135,7 +125,17 @@ export class HaControlSwitch extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div id="switch" class="switch">
|
||||
<div
|
||||
id="switch"
|
||||
class="switch"
|
||||
@keydown=${this._keydown}
|
||||
aria-checked=${this.checked ? "true" : "false"}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
?checked=${this.checked}
|
||||
?disabled=${this.disabled}
|
||||
>
|
||||
<div class="background"></div>
|
||||
<div class="button" aria-hidden="true">
|
||||
${this.checked
|
||||
@ -164,16 +164,13 @@ export class HaControlSwitch extends LitElement {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--control-switch-border-radius);
|
||||
outline: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
:host(:focus-visible) {
|
||||
.switch:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-switch-off-color);
|
||||
}
|
||||
:host([checked]:focus-visible) {
|
||||
.switch[checked]:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-switch-on-color);
|
||||
}
|
||||
.switch {
|
||||
@ -182,9 +179,15 @@ export class HaControlSwitch extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--control-switch-border-radius);
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
padding: var(--control-switch-padding);
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
.switch[disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.switch .background {
|
||||
position: absolute;
|
||||
@ -212,24 +215,24 @@ export class HaControlSwitch extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:host([checked]) .switch .background {
|
||||
.switch[checked] .background {
|
||||
background-color: var(--control-switch-on-color);
|
||||
}
|
||||
:host([checked]) .switch .button {
|
||||
.switch[checked] .button {
|
||||
transform: translateX(100%);
|
||||
background-color: var(--control-switch-on-color);
|
||||
}
|
||||
:host([reversed]) .switch {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
:host([reversed][checked]) .switch .button {
|
||||
:host([reversed]) .switch[checked] .button {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--control-switch-thickness);
|
||||
height: 100%;
|
||||
}
|
||||
:host([vertical][checked]) .switch .button {
|
||||
:host([vertical]) .switch[checked] .button {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
:host([vertical]) .switch .button {
|
||||
@ -239,13 +242,9 @@ export class HaControlSwitch extends LitElement {
|
||||
:host([vertical][reversed]) .switch {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
:host([vertical][reversed][checked]) .switch .button {
|
||||
:host([vertical][reversed]) .switch[checked] .button {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
:host([disabled]) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -86,11 +86,12 @@ export class HaFileUpload extends LitElement {
|
||||
? html`<div class="container">
|
||||
<div class="uploading">
|
||||
<span class="header"
|
||||
>${this.uploadingLabel || this.value
|
||||
>${this.uploadingLabel ||
|
||||
(this.value
|
||||
? localize("ui.components.file-upload.uploading_name", {
|
||||
name: this._name,
|
||||
})
|
||||
: localize("ui.components.file-upload.uploading")}</span
|
||||
: localize("ui.components.file-upload.uploading"))}</span
|
||||
>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
|
@ -14,6 +14,7 @@ import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
|
||||
@customElement("ha-filter-blueprints")
|
||||
export class HaFilterBlueprints extends LitElement {
|
||||
@ -34,10 +35,11 @@ export class HaFilterBlueprints extends LitElement {
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
if (this.value?.length) {
|
||||
this._findRelated();
|
||||
}
|
||||
if (
|
||||
properties.has("value") &&
|
||||
!deepEqual(this.value, properties.get("value"))
|
||||
) {
|
||||
this._findRelated();
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,17 +132,15 @@ export class HaFilterBlueprints extends LitElement {
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
|
||||
this._findRelated();
|
||||
}
|
||||
|
||||
private async _findRelated() {
|
||||
if (!this.value?.length) {
|
||||
this.value = [];
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
@ -37,9 +38,13 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
if (this.value?.length) {
|
||||
this._findRelated();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("value") &&
|
||||
!deepEqual(this.value, properties.get("value"))
|
||||
) {
|
||||
this._findRelated();
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +115,6 @@ export class HaFilterDevices extends LitElement {
|
||||
this.value = [...(this.value || []), value];
|
||||
}
|
||||
listItem.selected = this.value?.includes(value);
|
||||
this._findRelated();
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
@ -160,11 +164,11 @@ export class HaFilterDevices extends LitElement {
|
||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||
|
||||
if (!this.value?.length) {
|
||||
this.value = [];
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
@ -176,7 +180,6 @@ export class HaFilterDevices extends LitElement {
|
||||
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
||||
}
|
||||
}
|
||||
this.value = value;
|
||||
const results = await Promise.all(relatedPromises);
|
||||
const items = new Set<string>();
|
||||
for (const result of results) {
|
||||
|
@ -17,6 +17,7 @@ import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./ha-state-icon";
|
||||
import "./search-input-outlined";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
|
||||
@customElement("ha-filter-entities")
|
||||
export class HaFilterEntities extends LitElement {
|
||||
@ -39,9 +40,13 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
if (this.value?.length) {
|
||||
this._findRelated();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("value") &&
|
||||
!deepEqual(this.value, properties.get("value"))
|
||||
) {
|
||||
this._findRelated();
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,7 +136,6 @@ export class HaFilterEntities extends LitElement {
|
||||
this.value = [...(this.value || []), value];
|
||||
}
|
||||
listItem.selected = this.value?.includes(value);
|
||||
this._findRelated();
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
@ -178,11 +182,11 @@ export class HaFilterEntities extends LitElement {
|
||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||
|
||||
if (!this.value?.length) {
|
||||
this.value = [];
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import "./ha-icon-button";
|
||||
import "./ha-list";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tree-indicator";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
|
||||
@customElement("ha-filter-floor-areas")
|
||||
export class HaFilterFloorAreas extends LitElement {
|
||||
@ -41,10 +42,11 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
if (this.value?.floors?.length || this.value?.areas?.length) {
|
||||
this._findRelated();
|
||||
}
|
||||
if (
|
||||
properties.has("value") &&
|
||||
!deepEqual(this.value, properties.get("value"))
|
||||
) {
|
||||
this._findRelated();
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,8 +176,6 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
|
||||
listItem.selected = this.value[type]?.includes(value);
|
||||
|
||||
this._findRelated();
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
@ -188,10 +188,6 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._findRelated();
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
@ -226,6 +222,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
!this.value ||
|
||||
(!this.value.areas?.length && !this.value.floors?.length)
|
||||
) {
|
||||
this.value = {};
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: {},
|
||||
items: undefined,
|
||||
|
@ -433,6 +433,7 @@ export class HaFloorPicker extends LitElement {
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._setValue(value);
|
||||
|
@ -59,6 +59,15 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
private static streamCount = 0;
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
this._cleanUp();
|
||||
} else {
|
||||
this._resetError();
|
||||
this._startHls();
|
||||
}
|
||||
};
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
HaHLSPlayer.streamCount += 1;
|
||||
@ -66,10 +75,15 @@ class HaHLSPlayer extends LitElement {
|
||||
this._resetError();
|
||||
this._startHls();
|
||||
}
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
HaHLSPlayer.streamCount -= 1;
|
||||
this._cleanUp();
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { repeat } from "lit/directives/repeat";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { orderCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
@ -122,22 +123,6 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
${description
|
||||
? html`<span slot="supporting-text">${description}</span>`
|
||||
: nothing}
|
||||
${isVisible && !disableSorting
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
tabindex=${ifDefined(
|
||||
this.showNavigationButton ? "0" : undefined
|
||||
)}
|
||||
.idx=${idx}
|
||||
@keydown=${this.showNavigationButton
|
||||
? this._dragHandleKeydown
|
||||
: undefined}
|
||||
class="handle"
|
||||
.path=${mdiDrag}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`<ha-svg-icon slot="start"></ha-svg-icon>`}
|
||||
${!showIcon
|
||||
? nothing
|
||||
: icon
|
||||
@ -157,9 +142,17 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${this.showNavigationButton
|
||||
? html`
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
<div slot="end" class="separator"></div>
|
||||
`
|
||||
: nothing}
|
||||
${this.actionsRenderer
|
||||
? html`
|
||||
<span slot="end"> ${this.actionsRenderer(item)} </span>
|
||||
<div slot="end" @click=${stopPropagation}>
|
||||
${this.actionsRenderer(item)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
@ -174,9 +167,22 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
.value=${value}
|
||||
@click=${this._toggle}
|
||||
></ha-icon-button>
|
||||
${this.showNavigationButton
|
||||
? html` <ha-icon-next slot="end"></ha-icon-next> `
|
||||
: nothing}
|
||||
${isVisible && !disableSorting
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
tabindex=${ifDefined(
|
||||
this.showNavigationButton ? "0" : undefined
|
||||
)}
|
||||
.idx=${idx}
|
||||
@keydown=${this.showNavigationButton
|
||||
? this._dragHandleKeydown
|
||||
: undefined}
|
||||
class="handle"
|
||||
.path=${mdiDrag}
|
||||
slot="end"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`<ha-svg-icon slot="end"></ha-svg-icon>`}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
@ -369,6 +375,12 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
padding: 8px;
|
||||
margin: -8px;
|
||||
}
|
||||
.separator {
|
||||
width: 1px;
|
||||
background-color: var(--divider-color);
|
||||
height: 21px;
|
||||
margin: 0 -4px;
|
||||
}
|
||||
ha-md-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { AttributeSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-attribute-picker";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
|
||||
@customElement("ha-selector-attribute")
|
||||
export class HaSelectorAttribute extends LitElement {
|
||||
@ -23,7 +24,7 @@ export class HaSelectorAttribute extends LitElement {
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public context?: {
|
||||
filter_entity?: string;
|
||||
filter_entity?: string | string[];
|
||||
};
|
||||
|
||||
protected render() {
|
||||
@ -69,11 +70,16 @@ export class HaSelectorAttribute extends LitElement {
|
||||
// Validate that that the attribute is still valid for this entity, else unselect.
|
||||
let invalid = false;
|
||||
if (this.context.filter_entity) {
|
||||
const stateObj = this.hass.states[this.context.filter_entity];
|
||||
const entityIds = ensureArray(this.context.filter_entity);
|
||||
|
||||
if (!(stateObj && this.value in stateObj.attributes)) {
|
||||
invalid = true;
|
||||
}
|
||||
invalid = !entityIds.some((entityId) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
return (
|
||||
stateObj &&
|
||||
this.value in stateObj.attributes &&
|
||||
stateObj.attributes[this.value] !== undefined
|
||||
);
|
||||
});
|
||||
} else {
|
||||
invalid = this.value !== undefined;
|
||||
}
|
||||
|
@ -95,6 +95,7 @@ export class HaColorTempSelector extends LitElement {
|
||||
);
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: Number((ev.detail as any).value),
|
||||
});
|
||||
|
@ -83,6 +83,7 @@ export class HaEntitySelector extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.includeEntities=${this.selector.entity.include_entities}
|
||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
||||
.reorder=${this.selector.entity.reorder ?? false}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.createDomains=${this._createDomains}
|
||||
.disabled=${this.disabled}
|
||||
|
@ -24,6 +24,10 @@ const MANUAL_SCHEMA = [
|
||||
{ name: "media_content_type", required: false, selector: { text: {} } },
|
||||
] as const;
|
||||
|
||||
const INCLUDE_DOMAINS = ["media_player"];
|
||||
|
||||
const EMPTY_FORM = {};
|
||||
|
||||
@customElement("ha-selector-media")
|
||||
export class HaMediaSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -84,7 +88,7 @@ export class HaMediaSelector extends LitElement {
|
||||
(stateObj &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
|
||||
|
||||
const hasAccept = this.selector.media?.accept?.length;
|
||||
const hasAccept = this.selector?.media?.accept?.length;
|
||||
|
||||
return html`
|
||||
${hasAccept
|
||||
@ -100,7 +104,7 @@ export class HaMediaSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
include-domains='["media_player"]'
|
||||
.includeDomains=${INCLUDE_DOMAINS}
|
||||
allow-custom-entity
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
@ -114,7 +118,7 @@ export class HaMediaSelector extends LitElement {
|
||||
</ha-alert>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.value}
|
||||
.data=${this.value || EMPTY_FORM}
|
||||
.schema=${MANUAL_SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
></ha-form>
|
||||
@ -122,63 +126,62 @@ export class HaMediaSelector extends LitElement {
|
||||
: html`
|
||||
<ha-card
|
||||
outlined
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label=${!this.value?.media_content_id
|
||||
? this.hass.localize("ui.components.selectors.media.pick_media")
|
||||
: this.value.metadata?.title || this.value.media_content_id}
|
||||
@click=${this._pickMedia}
|
||||
@keydown=${this._handleKeyDown}
|
||||
class=${this.disabled || (!this.value?.entity_id && !hasAccept)
|
||||
? "disabled"
|
||||
: ""}
|
||||
>
|
||||
<div
|
||||
class="thumbnail ${classMap({
|
||||
portrait:
|
||||
!!this.value?.metadata?.media_class &&
|
||||
MediaClassBrowserSettings[
|
||||
this.value.metadata.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
].thumbnail_ratio === "portrait",
|
||||
})}"
|
||||
>
|
||||
${this.value?.metadata?.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="${classMap({
|
||||
"centered-image":
|
||||
!!this.value.metadata.media_class &&
|
||||
["app", "directory"].includes(
|
||||
this.value.metadata.media_class
|
||||
),
|
||||
})}
|
||||
image"
|
||||
style=${this._thumbnailUrl
|
||||
? `background-image: url(${this._thumbnailUrl});`
|
||||
: ""}
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${!this.value?.media_content_id
|
||||
? mdiPlus
|
||||
: this.value?.metadata?.media_class
|
||||
? MediaClassBrowserSettings[
|
||||
this.value.metadata.media_class ===
|
||||
"directory"
|
||||
? this.value.metadata
|
||||
.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
: this.value.metadata.media_class
|
||||
].icon
|
||||
: mdiPlayBox}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="title">
|
||||
${!this.value?.media_content_id
|
||||
? this.hass.localize(
|
||||
"ui.components.selectors.media.pick_media"
|
||||
)
|
||||
: this.value.metadata?.title || this.value.media_content_id}
|
||||
<div class="content-container">
|
||||
<div class="thumbnail">
|
||||
${this.value?.metadata?.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="${classMap({
|
||||
"centered-image":
|
||||
!!this.value.metadata.media_class &&
|
||||
["app", "directory"].includes(
|
||||
this.value.metadata.media_class
|
||||
),
|
||||
})}
|
||||
image"
|
||||
style=${this._thumbnailUrl
|
||||
? `background-image: url(${this._thumbnailUrl});`
|
||||
: ""}
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${!this.value?.media_content_id
|
||||
? mdiPlus
|
||||
: this.value?.metadata?.media_class
|
||||
? MediaClassBrowserSettings[
|
||||
this.value.metadata.media_class ===
|
||||
"directory"
|
||||
? this.value.metadata
|
||||
.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
: this.value.metadata.media_class
|
||||
].icon
|
||||
: mdiPlayBox}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="title">
|
||||
${!this.value?.media_content_id
|
||||
? this.hass.localize(
|
||||
"ui.components.selectors.media.pick_media"
|
||||
)
|
||||
: this.value.metadata?.title || this.value.media_content_id}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`}
|
||||
@ -229,6 +232,13 @@ export class HaMediaSelector extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._pickMedia();
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
@ -243,41 +253,52 @@ export class HaMediaSelector extends LitElement {
|
||||
}
|
||||
ha-card {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
transition: background-color 180ms ease-in-out;
|
||||
min-height: 56px;
|
||||
}
|
||||
ha-card:hover:not(.disabled),
|
||||
ha-card:focus:not(.disabled) {
|
||||
background-color: var(--state-icon-hover-color, rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
ha-card:focus {
|
||||
outline: none;
|
||||
}
|
||||
ha-card.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
.content-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
gap: 12px;
|
||||
}
|
||||
ha-card .thumbnail {
|
||||
width: 100%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
transition: padding-bottom 0.1s ease-out;
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
ha-card .thumbnail.portrait {
|
||||
padding-bottom: 150%;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-card .image {
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.folder {
|
||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.title {
|
||||
font-size: var(--ha-font-size-l);
|
||||
padding-top: 16px;
|
||||
font-size: var(--ha-font-size-m);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 4px;
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: 4px;
|
||||
white-space: nowrap;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.image {
|
||||
position: absolute;
|
||||
@ -290,13 +311,15 @@ export class HaMediaSelector extends LitElement {
|
||||
background-position: center;
|
||||
}
|
||||
.centered-image {
|
||||
margin: 0 8px;
|
||||
margin: 4px;
|
||||
background-size: contain;
|
||||
}
|
||||
.icon-holder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -23,6 +23,9 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public localizeValue?: (key: string) => string;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@ -60,6 +63,14 @@ export class HaNumberSelector extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
const translationKey = this.selector.number?.translation_key;
|
||||
let unit = this.selector.number?.unit_of_measurement;
|
||||
if (isBox && unit && this.localizeValue && translationKey) {
|
||||
unit =
|
||||
this.localizeValue(`${translationKey}.unit_of_measurement.${unit}`) ||
|
||||
unit;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.label && !isBox
|
||||
? html`${this.label}${this.required ? "*" : ""}`
|
||||
@ -97,7 +108,7 @@ export class HaNumberSelector extends LitElement {
|
||||
.helper=${isBox ? this.helper : undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.suffix=${this.selector.number?.unit_of_measurement}
|
||||
.suffix=${unit}
|
||||
type="number"
|
||||
autoValidate
|
||||
?no-spinner=${!isBox}
|
||||
|
@ -15,6 +15,7 @@ import "../ha-md-list-item";
|
||||
import "../ha-sortable";
|
||||
import "../ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../ha-yaml-editor";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
|
||||
@customElement("ha-selector-object")
|
||||
export class HaObjectSelector extends LitElement {
|
||||
@ -122,11 +123,7 @@ export class HaObjectSelector extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.selector.object) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this.selector.object.fields) {
|
||||
if (this.selector.object?.fields) {
|
||||
if (this.selector.object.multiple) {
|
||||
const items = ensureArray(this.value ?? []);
|
||||
return html`
|
||||
@ -275,7 +272,8 @@ export class HaObjectSelector extends LitElement {
|
||||
if (
|
||||
changedProps.has("value") &&
|
||||
!this._valueChangedFromChild &&
|
||||
this._yamlEditor
|
||||
this._yamlEditor &&
|
||||
!deepEqual(this.value, changedProps.get("value"))
|
||||
) {
|
||||
this._yamlEditor.setValue(this.value);
|
||||
}
|
||||
@ -283,6 +281,7 @@ export class HaObjectSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
ev.stopPropagation();
|
||||
this._valueChangedFromChild = true;
|
||||
const value = ev.target.value;
|
||||
if (!ev.target.isValid) {
|
||||
|
@ -23,7 +23,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public context?: {
|
||||
filter_attribute?: string;
|
||||
filter_entity?: string;
|
||||
filter_entity?: string | string[];
|
||||
};
|
||||
|
||||
protected render() {
|
||||
@ -41,6 +41,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-value
|
||||
.hideStates=${this.selector.state?.hide_states}
|
||||
></ha-entity-state-picker>
|
||||
`;
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ export class HaTemplateSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
ev.stopPropagation();
|
||||
let value = ev.target.value;
|
||||
if (this.value === value) {
|
||||
return;
|
||||
|
@ -111,6 +111,7 @@ export class HaTextSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
ev.stopPropagation();
|
||||
let value = ev.detail?.value ?? ev.target.value;
|
||||
if (this.value === value) {
|
||||
return;
|
||||
|
@ -33,6 +33,7 @@ export class HaSelectorUiAction extends LitElement {
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ export class HaSelectorUiColor extends LitElement {
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
}
|
||||
}
|
||||
|
@ -314,7 +314,12 @@ export class HaServiceControl extends LitElement {
|
||||
targetSelector
|
||||
);
|
||||
targetDevices.push(...expanded.devices);
|
||||
targetEntities.push(...expanded.entities);
|
||||
const primaryEntities = expanded.entities.filter(
|
||||
(entityId) =>
|
||||
!this.hass.entities[entityId]?.entity_category &&
|
||||
!this.hass.entities[entityId]?.hidden
|
||||
);
|
||||
targetEntities.push(primaryEntities);
|
||||
targetAreas.push(...expanded.areas);
|
||||
});
|
||||
}
|
||||
@ -338,20 +343,29 @@ export class HaServiceControl extends LitElement {
|
||||
this.hass.entities,
|
||||
targetSelector
|
||||
);
|
||||
targetEntities.push(...expanded.entities);
|
||||
const primaryEntities = expanded.entities.filter(
|
||||
(entityId) =>
|
||||
!this.hass.entities[entityId]?.entity_category &&
|
||||
!this.hass.entities[entityId]?.hidden
|
||||
);
|
||||
targetEntities.push(...primaryEntities);
|
||||
targetDevices.push(...expanded.devices);
|
||||
});
|
||||
}
|
||||
if (targetDevices.length) {
|
||||
targetDevices.forEach((deviceId) => {
|
||||
targetEntities.push(
|
||||
...expandDeviceTarget(
|
||||
this.hass,
|
||||
deviceId,
|
||||
this.hass.entities,
|
||||
targetSelector
|
||||
).entities
|
||||
const expanded = expandDeviceTarget(
|
||||
this.hass,
|
||||
deviceId,
|
||||
this.hass.entities,
|
||||
targetSelector
|
||||
);
|
||||
const primaryEntities = expanded.entities.filter(
|
||||
(entityId) =>
|
||||
!this.hass.entities[entityId]?.entity_category &&
|
||||
!this.hass.entities[entityId]?.hidden
|
||||
);
|
||||
targetEntities.push(...primaryEntities);
|
||||
});
|
||||
}
|
||||
return targetEntities;
|
||||
@ -675,6 +689,7 @@ export class HaServiceControl extends LitElement {
|
||||
) || dataField?.description}</span
|
||||
>
|
||||
<ha-selector
|
||||
.context=${this._selectorContext(targetEntities)}
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
@ -694,6 +709,10 @@ export class HaServiceControl extends LitElement {
|
||||
: "";
|
||||
};
|
||||
|
||||
private _selectorContext = memoizeOne((targetEntities: string[] | null) => ({
|
||||
filter_entity: targetEntities || undefined,
|
||||
}));
|
||||
|
||||
private _localizeValueCallback = (key: string) => {
|
||||
if (!this._value?.action) {
|
||||
return "";
|
||||
|
@ -89,6 +89,7 @@ export class HaSettingsRow extends LitElement {
|
||||
display: var(--settings-row-content-display, flex);
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 16px 0;
|
||||
}
|
||||
.content ::slotted(*) {
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
mdiTooltipAccount,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
@ -205,6 +206,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _recentKeydownActiveUntil = 0;
|
||||
|
||||
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
|
||||
|
||||
@query(".tooltip") private _tooltip!: HTMLDivElement;
|
||||
|
||||
public hassSubscribe() {
|
||||
@ -227,9 +230,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
),
|
||||
subscribeNotifications(this.hass.connection, (notifications) => {
|
||||
this._notifications = notifications;
|
||||
}),
|
||||
...(this.hass.user?.is_admin
|
||||
? [
|
||||
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
|
||||
@ -300,6 +300,23 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._subscribePersistentNotifications();
|
||||
}
|
||||
|
||||
private _subscribePersistentNotifications(): void {
|
||||
if (this._unsubPersistentNotifications) {
|
||||
this._unsubPersistentNotifications();
|
||||
}
|
||||
this._unsubPersistentNotifications = subscribeNotifications(
|
||||
this.hass.connection,
|
||||
(notifications) => {
|
||||
this._notifications = notifications;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("alwaysExpand")) {
|
||||
@ -311,6 +328,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
|
||||
if (
|
||||
this.hass &&
|
||||
oldHass?.connected === false &&
|
||||
this.hass.connected === true
|
||||
) {
|
||||
this._subscribePersistentNotifications();
|
||||
}
|
||||
|
||||
this._calculateCounts();
|
||||
|
||||
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||
|
201
src/components/ha-suggest-with-ai-button.ts
Normal file
201
src/components/ha-suggest-with-ai-button.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, css, LitElement, nothing } from "lit";
|
||||
import { mdiStarFourPoints } from "@mdi/js";
|
||||
|
||||
import { customElement, state, property } from "lit/decorators";
|
||||
import type {
|
||||
AITaskPreferences,
|
||||
GenDataTask,
|
||||
GenDataTaskResult,
|
||||
} from "../data/ai_task";
|
||||
import { fetchAITaskPreferences, generateDataAITask } from "../data/ai_task";
|
||||
import "./chips/ha-assist-chip";
|
||||
import "./ha-svg-icon";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
suggestion: GenDataTaskResult;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SuggestWithAIGenerateTask {
|
||||
type: "data";
|
||||
task: GenDataTask;
|
||||
}
|
||||
|
||||
@customElement("ha-suggest-with-ai-button")
|
||||
export class HaSuggestWithAIButton extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "task-type" })
|
||||
public taskType!: "data";
|
||||
|
||||
@property({ attribute: false })
|
||||
generateTask!: () => SuggestWithAIGenerateTask;
|
||||
|
||||
@state()
|
||||
private _aiPrefs?: AITaskPreferences;
|
||||
|
||||
@state()
|
||||
private _state: {
|
||||
status: "idle" | "suggesting" | "error" | "done";
|
||||
suggestionIndex: 1 | 2 | 3;
|
||||
} = {
|
||||
status: "idle",
|
||||
suggestionIndex: 1,
|
||||
};
|
||||
|
||||
@state()
|
||||
private _minWidth?: string;
|
||||
|
||||
private _intervalId?: number;
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
if (!this.hass || !isComponentLoaded(this.hass, "ai_task")) {
|
||||
return;
|
||||
}
|
||||
fetchAITaskPreferences(this.hass).then((prefs) => {
|
||||
this._aiPrefs = prefs;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this._aiPrefs || !this._aiPrefs.gen_data_entity_id) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let label: string;
|
||||
switch (this._state.status) {
|
||||
case "error":
|
||||
label = this.hass.localize("ui.components.suggest_with_ai.error");
|
||||
break;
|
||||
case "done":
|
||||
label = this.hass.localize("ui.components.suggest_with_ai.done");
|
||||
break;
|
||||
case "suggesting":
|
||||
label = this.hass.localize(
|
||||
`ui.components.suggest_with_ai.suggesting_${this._state.suggestionIndex}`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
label = this.hass.localize("ui.components.suggest_with_ai.label");
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-assist-chip
|
||||
@click=${this._suggest}
|
||||
.label=${label}
|
||||
?active=${this._state.status === "suggesting"}
|
||||
class=${this._state.status === "error"
|
||||
? "error"
|
||||
: this._state.status === "done"
|
||||
? "done"
|
||||
: ""}
|
||||
style=${this._minWidth ? `min-width: ${this._minWidth}` : ""}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiStarFourPoints}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _suggest() {
|
||||
if (!this.generateTask || this._state.status === "suggesting") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture current width before changing state
|
||||
const chip = this.shadowRoot?.querySelector("ha-assist-chip");
|
||||
if (chip) {
|
||||
this._minWidth = `${chip.offsetWidth}px`;
|
||||
}
|
||||
|
||||
// Reset to suggesting state
|
||||
this._state = {
|
||||
status: "suggesting",
|
||||
suggestionIndex: 1,
|
||||
};
|
||||
|
||||
try {
|
||||
// Start cycling through suggestion texts
|
||||
this._intervalId = window.setInterval(() => {
|
||||
this._state = {
|
||||
...this._state,
|
||||
suggestionIndex: ((this._state.suggestionIndex % 3) + 1) as 1 | 2 | 3,
|
||||
};
|
||||
}, 3000);
|
||||
|
||||
const info = await this.generateTask();
|
||||
let result: GenDataTaskResult;
|
||||
if (info.type === "data") {
|
||||
result = await generateDataAITask(this.hass, info.task);
|
||||
} else {
|
||||
throw new Error("Unsupported task type");
|
||||
}
|
||||
|
||||
fireEvent(this, "suggestion", result);
|
||||
|
||||
// Show success state
|
||||
this._state = {
|
||||
...this._state,
|
||||
status: "done",
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error generating AI suggestion:", error);
|
||||
|
||||
this._state = {
|
||||
...this._state,
|
||||
status: "error",
|
||||
};
|
||||
} finally {
|
||||
if (this._intervalId) {
|
||||
clearInterval(this._intervalId);
|
||||
this._intervalId = undefined;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this._state = {
|
||||
...this._state,
|
||||
status: "idle",
|
||||
};
|
||||
this._minWidth = undefined;
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-assist-chip[active] {
|
||||
animation: pulse-glow 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
ha-assist-chip.error {
|
||||
box-shadow: 0 0 12px 4px rgba(var(--rgb-error-color), 0.8);
|
||||
}
|
||||
|
||||
ha-assist-chip.done {
|
||||
box-shadow: 0 0 12px 4px rgba(var(--rgb-primary-color), 0.8);
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--rgb-primary-color), 0);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 8px 2px rgba(var(--rgb-primary-color), 0.6);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--rgb-primary-color), 0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-suggest-with-ai-button": HaSuggestWithAIButton;
|
||||
}
|
||||
}
|
@ -719,7 +719,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _entityRegMeetsFilter(entity: EntityRegistryDisplayEntry): boolean {
|
||||
if (entity.entity_category) {
|
||||
if (entity.hidden || entity.entity_category) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,14 @@ class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
private _candidatesList: RTCIceCandidate[] = [];
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
this._cleanUp();
|
||||
} else {
|
||||
this._startWebRtc();
|
||||
}
|
||||
};
|
||||
|
||||
protected override render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
@ -88,10 +96,15 @@ class HaWebRtcPlayer extends LitElement {
|
||||
if (this.hasUpdated && this.entityid) {
|
||||
this._startWebRtc();
|
||||
}
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
}
|
||||
|
||||
public override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
this._cleanUp();
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,9 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
@property({ attribute: "read-only", type: Boolean }) public readOnly = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "disable-fullscreen" })
|
||||
public disableFullscreen = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: "copy-clipboard", type: Boolean })
|
||||
@ -110,6 +113,7 @@ export class HaYamlEditor extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this._yaml}
|
||||
.readOnly=${this.readOnly}
|
||||
.disableFullscreen=${this.disableFullscreen}
|
||||
mode="yaml"
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
|
@ -36,6 +36,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const PROGRAMMITIC_FIT_DELAY = 250;
|
||||
|
||||
const getEntityId = (entity: string | HaMapEntity): string =>
|
||||
typeof entity === "string" ? entity : entity.entity_id;
|
||||
|
||||
@ -113,14 +115,33 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private _clickCount = 0;
|
||||
|
||||
private _isProgrammaticFit = false;
|
||||
|
||||
private _pauseAutoFit = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
this._pauseAutoFit = false;
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._handleVisibilityChange();
|
||||
super.connectedCallback();
|
||||
this._loadMap();
|
||||
this._attachObserver();
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = async () => {
|
||||
if (!document.hidden) {
|
||||
setTimeout(() => {
|
||||
this._pauseAutoFit = false;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
if (this.leafletMap) {
|
||||
this.leafletMap.remove();
|
||||
this.leafletMap = undefined;
|
||||
@ -145,7 +166,7 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("entities")) {
|
||||
this._drawEntities();
|
||||
autoFitRequired = true;
|
||||
autoFitRequired = !this._pauseAutoFit;
|
||||
} else if (this._loaded && oldHass && this.entities) {
|
||||
// Check if any state has changed
|
||||
for (const entity of this.entities) {
|
||||
@ -154,7 +175,7 @@ export class HaMap extends ReactiveElement {
|
||||
this.hass!.states[getEntityId(entity)]
|
||||
) {
|
||||
this._drawEntities();
|
||||
autoFitRequired = true;
|
||||
autoFitRequired = !this._pauseAutoFit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -178,7 +199,11 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
if (changedProps.has("zoom")) {
|
||||
this._isProgrammaticFit = true;
|
||||
this.leafletMap!.setZoom(this.zoom);
|
||||
setTimeout(() => {
|
||||
this._isProgrammaticFit = false;
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
}
|
||||
|
||||
if (
|
||||
@ -234,13 +259,30 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
this._clickCount++;
|
||||
});
|
||||
this.leafletMap.on("zoomstart", () => {
|
||||
if (!this._isProgrammaticFit) {
|
||||
this._pauseAutoFit = true;
|
||||
}
|
||||
});
|
||||
this.leafletMap.on("movestart", () => {
|
||||
if (!this._isProgrammaticFit) {
|
||||
this._pauseAutoFit = true;
|
||||
}
|
||||
});
|
||||
this._loaded = true;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public fitMap(options?: { zoom?: number; pad?: number }): void {
|
||||
public fitMap(options?: {
|
||||
zoom?: number;
|
||||
pad?: number;
|
||||
unpause_autofit?: boolean;
|
||||
}): void {
|
||||
if (options?.unpause_autofit) {
|
||||
this._pauseAutoFit = false;
|
||||
}
|
||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||
return;
|
||||
}
|
||||
@ -250,6 +292,7 @@ export class HaMap extends ReactiveElement {
|
||||
!this._mapFocusZones.length &&
|
||||
!this.layers?.length
|
||||
) {
|
||||
this._isProgrammaticFit = true;
|
||||
this.leafletMap.setView(
|
||||
new this.Leaflet.LatLng(
|
||||
this.hass.config.latitude,
|
||||
@ -257,6 +300,9 @@ export class HaMap extends ReactiveElement {
|
||||
),
|
||||
options?.zoom || this.zoom
|
||||
);
|
||||
setTimeout(() => {
|
||||
this._isProgrammaticFit = false;
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -277,8 +323,11 @@ export class HaMap extends ReactiveElement {
|
||||
});
|
||||
|
||||
bounds = bounds.pad(options?.pad ?? 0.5);
|
||||
|
||||
this._isProgrammaticFit = true;
|
||||
this.leafletMap.fitBounds(bounds, { maxZoom: options?.zoom || this.zoom });
|
||||
setTimeout(() => {
|
||||
this._isProgrammaticFit = false;
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
}
|
||||
|
||||
public fitBounds(
|
||||
@ -291,7 +340,11 @@ export class HaMap extends ReactiveElement {
|
||||
const bounds = this.Leaflet.latLngBounds(boundingbox).pad(
|
||||
options?.pad ?? 0.5
|
||||
);
|
||||
this._isProgrammaticFit = true;
|
||||
this.leafletMap.fitBounds(bounds, { maxZoom: options?.zoom || this.zoom });
|
||||
setTimeout(() => {
|
||||
this._isProgrammaticFit = false;
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
}
|
||||
|
||||
private _drawLayers(prevLayers: Layer[] | undefined): void {
|
||||
|
@ -1,14 +1,30 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { Selector } from "./selector";
|
||||
|
||||
export interface AITaskPreferences {
|
||||
gen_data_entity_id: string | null;
|
||||
}
|
||||
|
||||
export interface GenDataTaskResult {
|
||||
conversation_id: string;
|
||||
data: string;
|
||||
export interface GenDataTask {
|
||||
task_name: string;
|
||||
entity_id?: string;
|
||||
instructions: string;
|
||||
structure?: AITaskStructure;
|
||||
}
|
||||
|
||||
export interface GenDataTaskResult<T = string> {
|
||||
conversation_id: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface AITaskStructureField {
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
selector: Selector;
|
||||
}
|
||||
|
||||
export type AITaskStructure = Record<string, AITaskStructureField>;
|
||||
|
||||
export const fetchAITaskPreferences = (hass: HomeAssistant) =>
|
||||
hass.callWS<AITaskPreferences>({
|
||||
type: "ai_task/preferences/get",
|
||||
@ -23,15 +39,11 @@ export const saveAITaskPreferences = (
|
||||
...preferences,
|
||||
});
|
||||
|
||||
export const generateDataAITask = async (
|
||||
export const generateDataAITask = async <T = string>(
|
||||
hass: HomeAssistant,
|
||||
task: {
|
||||
task_name: string;
|
||||
entity_id?: string;
|
||||
instructions: string;
|
||||
}
|
||||
): Promise<GenDataTaskResult> => {
|
||||
const result = await hass.callService<GenDataTaskResult>(
|
||||
task: GenDataTask
|
||||
): Promise<GenDataTaskResult<T>> => {
|
||||
const result = await hass.callService<GenDataTaskResult<T>>(
|
||||
"ai_task",
|
||||
"generate_data",
|
||||
task,
|
||||
|
@ -26,7 +26,12 @@ import type {
|
||||
StatisticsMetaData,
|
||||
StatisticsUnitConfiguration,
|
||||
} from "./recorder";
|
||||
import { fetchStatistics, getStatisticMetadata } from "./recorder";
|
||||
import {
|
||||
fetchStatistics,
|
||||
getDisplayUnit,
|
||||
getStatisticMetadata,
|
||||
VOLUME_UNITS,
|
||||
} from "./recorder";
|
||||
import { calcDateRange } from "../common/datetime/calc_date_range";
|
||||
import type { DateRange } from "../common/datetime/calc_date_range";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
@ -275,6 +280,8 @@ export interface EnergyData {
|
||||
co2SignalEntity?: string;
|
||||
fossilEnergyConsumption?: FossilEnergyConsumption;
|
||||
fossilEnergyConsumptionCompare?: FossilEnergyConsumption;
|
||||
waterUnit: string;
|
||||
gasUnit: string;
|
||||
}
|
||||
|
||||
export const getReferencedStatisticIds = (
|
||||
@ -397,13 +404,29 @@ const getEnergyData = async (
|
||||
? "day"
|
||||
: "hour";
|
||||
|
||||
const lengthUnit = hass.config.unit_system.length || "";
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
const statsMetadataArray = allStatIDs.length
|
||||
? await getStatisticMetadata(hass, allStatIDs)
|
||||
: [];
|
||||
|
||||
if (allStatIDs.length) {
|
||||
statsMetadataArray.forEach((x) => {
|
||||
statsMetadata[x.statistic_id] = x;
|
||||
});
|
||||
}
|
||||
|
||||
const gasUnit = getEnergyGasUnit(hass, prefs, statsMetadata);
|
||||
const gasIsVolume = VOLUME_UNITS.includes(gasUnit as any);
|
||||
|
||||
const energyUnits: StatisticsUnitConfiguration = {
|
||||
energy: "kWh",
|
||||
volume: lengthUnit === "km" ? "m³" : "ft³",
|
||||
volume: gasIsVolume
|
||||
? (gasUnit as (typeof VOLUME_UNITS)[number])
|
||||
: undefined,
|
||||
};
|
||||
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
|
||||
const waterUnits: StatisticsUnitConfiguration = {
|
||||
volume: lengthUnit === "km" ? "L" : "gal",
|
||||
volume: waterUnit,
|
||||
};
|
||||
|
||||
const _energyStats: Statistics | Promise<Statistics> = energyStatIds.length
|
||||
@ -511,18 +534,11 @@ const getEnergyData = async (
|
||||
}
|
||||
}
|
||||
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
const _getStatisticMetadata:
|
||||
| Promise<StatisticsMetaData[]>
|
||||
| StatisticsMetaData[] = allStatIDs.length
|
||||
? getStatisticMetadata(hass, allStatIDs)
|
||||
: [];
|
||||
const [
|
||||
energyStats,
|
||||
waterStats,
|
||||
energyStatsCompare,
|
||||
waterStatsCompare,
|
||||
statsMetadataArray,
|
||||
fossilEnergyConsumption,
|
||||
fossilEnergyConsumptionCompare,
|
||||
] = await Promise.all([
|
||||
@ -530,7 +546,6 @@ const getEnergyData = async (
|
||||
_waterStats,
|
||||
_energyStatsCompare,
|
||||
_waterStatsCompare,
|
||||
_getStatisticMetadata,
|
||||
_fossilEnergyConsumption,
|
||||
_fossilEnergyConsumptionCompare,
|
||||
]);
|
||||
@ -538,11 +553,6 @@ const getEnergyData = async (
|
||||
if (compare) {
|
||||
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
||||
}
|
||||
if (allStatIDs.length) {
|
||||
statsMetadataArray.forEach((x) => {
|
||||
statsMetadata[x.statistic_id] = x;
|
||||
});
|
||||
}
|
||||
|
||||
const data: EnergyData = {
|
||||
start,
|
||||
@ -557,6 +567,8 @@ const getEnergyData = async (
|
||||
co2SignalEntity,
|
||||
fossilEnergyConsumption,
|
||||
fossilEnergyConsumptionCompare,
|
||||
waterUnit,
|
||||
gasUnit,
|
||||
};
|
||||
|
||||
return data;
|
||||
@ -765,7 +777,7 @@ export const getEnergyGasUnitClass = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getEnergyGasUnit = (
|
||||
const getEnergyGasUnit = (
|
||||
hass: HomeAssistant,
|
||||
prefs: EnergyPreferences,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData> = {}
|
||||
@ -775,11 +787,54 @@ export const getEnergyGasUnit = (
|
||||
return "kWh";
|
||||
}
|
||||
|
||||
const units = prefs.energy_sources
|
||||
.filter((s) => s.type === "gas")
|
||||
.map((s) =>
|
||||
getDisplayUnit(
|
||||
hass,
|
||||
s.stat_energy_from,
|
||||
statisticsMetaData[s.stat_energy_from]
|
||||
)
|
||||
);
|
||||
if (units.length) {
|
||||
const first = units[0];
|
||||
if (
|
||||
VOLUME_UNITS.includes(first as any) &&
|
||||
units.every((u) => u === first)
|
||||
) {
|
||||
return first as (typeof VOLUME_UNITS)[number];
|
||||
}
|
||||
}
|
||||
|
||||
return hass.config.unit_system.length === "km" ? "m³" : "ft³";
|
||||
};
|
||||
|
||||
export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
|
||||
hass.config.unit_system.length === "km" ? "L" : "gal";
|
||||
const getEnergyWaterUnit = (
|
||||
hass: HomeAssistant,
|
||||
prefs: EnergyPreferences,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData>
|
||||
): (typeof VOLUME_UNITS)[number] => {
|
||||
const units = prefs.energy_sources
|
||||
.filter((s) => s.type === "water")
|
||||
.map((s) =>
|
||||
getDisplayUnit(
|
||||
hass,
|
||||
s.stat_energy_from,
|
||||
statisticsMetaData[s.stat_energy_from]
|
||||
)
|
||||
);
|
||||
if (units.length) {
|
||||
const first = units[0];
|
||||
if (
|
||||
VOLUME_UNITS.includes(first as any) &&
|
||||
units.every((u) => u === first)
|
||||
) {
|
||||
return first as (typeof VOLUME_UNITS)[number];
|
||||
}
|
||||
}
|
||||
|
||||
return hass.config.unit_system.length === "km" ? "L" : "gal";
|
||||
};
|
||||
|
||||
export const energyStatisticHelpUrl =
|
||||
"/docs/energy/faq/#troubleshooting-missing-entities";
|
||||
@ -1109,21 +1164,31 @@ export const computeConsumptionSingle = (data: {
|
||||
export const formatConsumptionShort = (
|
||||
hass: HomeAssistant,
|
||||
consumption: number | null,
|
||||
unit: string
|
||||
unit: string,
|
||||
targetUnit?: string
|
||||
): string => {
|
||||
if (!consumption) {
|
||||
return `0 ${unit}`;
|
||||
}
|
||||
const units = ["Wh", "kWh", "MWh", "GWh", "TWh"];
|
||||
let pickedUnit = unit;
|
||||
let val = consumption;
|
||||
let val = consumption || 0;
|
||||
let targetUnitIndex = -1;
|
||||
if (targetUnit) {
|
||||
targetUnitIndex = units.findIndex((u) => u === targetUnit);
|
||||
}
|
||||
let unitIndex = units.findIndex((u) => u === unit);
|
||||
if (unitIndex >= 0) {
|
||||
while (Math.abs(val) < 1 && unitIndex > 0) {
|
||||
while (
|
||||
targetUnitIndex > -1
|
||||
? targetUnitIndex < unitIndex
|
||||
: Math.abs(val) < 1 && unitIndex > 0
|
||||
) {
|
||||
val *= 1000;
|
||||
unitIndex--;
|
||||
}
|
||||
while (Math.abs(val) >= 1000 && unitIndex < units.length - 1) {
|
||||
while (
|
||||
targetUnitIndex > -1
|
||||
? targetUnitIndex > unitIndex
|
||||
: Math.abs(val) >= 1000 && unitIndex < units.length - 1
|
||||
) {
|
||||
val /= 1000;
|
||||
unitIndex++;
|
||||
}
|
||||
|
@ -578,7 +578,7 @@ export const computeHistory = (
|
||||
const unitStates = Object.keys(lineChartDevices).map((key) => {
|
||||
const splitKey = key.split("_");
|
||||
const unit = splitKey[0];
|
||||
const deviceClass = splitKey[1] || undefined;
|
||||
const deviceClass = splitKey.slice(1).join("_") || undefined;
|
||||
return processLineChartEntities(
|
||||
unit,
|
||||
deviceClass,
|
||||
|
@ -354,7 +354,10 @@ const getIconFromTranslations = (
|
||||
}
|
||||
// Then check for range-based icons if we have a numeric state
|
||||
if (state !== undefined && translations.range && !isNaN(Number(state))) {
|
||||
return getIconFromRange(Number(state), translations.range);
|
||||
return (
|
||||
getIconFromRange(Number(state), translations.range) ??
|
||||
translations.default
|
||||
);
|
||||
}
|
||||
// Fallback to default icon
|
||||
return translations.default;
|
||||
@ -518,7 +521,10 @@ export const domainIcon = async (
|
||||
}
|
||||
// Then check for range-based icons if we have a numeric state
|
||||
if (state !== undefined && translations.range && !isNaN(Number(state))) {
|
||||
return getIconFromRange(Number(state), translations.range);
|
||||
return (
|
||||
getIconFromRange(Number(state), translations.range) ??
|
||||
translations.default
|
||||
);
|
||||
}
|
||||
// Fallback to default icon
|
||||
return translations.default;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { mdiContentSave, mdiMedal, mdiTrophy } from "@mdi/js";
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
|
||||
/**
|
||||
@ -26,11 +25,6 @@ export const QUALITY_SCALE_MAP: Record<
|
||||
translationKey:
|
||||
"ui.panel.config.integrations.config_entry.platinum_quality",
|
||||
},
|
||||
internal: {
|
||||
icon: mdiHomeAssistant,
|
||||
translationKey:
|
||||
"ui.panel.config.integrations.config_entry.internal_integration",
|
||||
},
|
||||
legacy: {
|
||||
icon: mdiContentSave,
|
||||
translationKey:
|
||||
|
@ -114,9 +114,13 @@ const getLogbookDataFromServer = (
|
||||
|
||||
export const subscribeLogbook = (
|
||||
hass: HomeAssistant,
|
||||
callbackFunction: (message: LogbookStreamMessage) => void,
|
||||
callbackFunction: (
|
||||
message: LogbookStreamMessage,
|
||||
subscriptionId: number
|
||||
) => void,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
subscriptionId: number,
|
||||
entityIds?: string[],
|
||||
deviceIds?: string[]
|
||||
): Promise<UnsubscribeFunc> => {
|
||||
@ -140,7 +144,7 @@ export const subscribeLogbook = (
|
||||
params.device_ids = deviceIds;
|
||||
}
|
||||
return hass.connection.subscribeMessage<LogbookStreamMessage>(
|
||||
(message) => callbackFunction(message),
|
||||
(message) => callbackFunction(message, subscriptionId),
|
||||
params
|
||||
);
|
||||
};
|
||||
|
@ -24,6 +24,9 @@ export const browseLocalMediaPlayer = (
|
||||
media_content_id: mediaContentId,
|
||||
});
|
||||
|
||||
export const isMediaSourceContentId = (mediaId: string) =>
|
||||
mediaId.startsWith("media-source://");
|
||||
|
||||
export const isLocalMediaSourceContentId = (mediaId: string) =>
|
||||
mediaId.startsWith("media-source://media_source");
|
||||
|
||||
|
@ -108,6 +108,8 @@ export interface StatisticsValidationResultMeanTypeChanged {
|
||||
};
|
||||
}
|
||||
|
||||
export const VOLUME_UNITS = ["L", "gal", "ft³", "m³", "CCF"] as const;
|
||||
|
||||
export interface StatisticsUnitConfiguration {
|
||||
energy?: "Wh" | "kWh" | "MWh" | "GJ";
|
||||
power?: "W" | "kW";
|
||||
@ -122,7 +124,7 @@ export interface StatisticsUnitConfiguration {
|
||||
| "psi"
|
||||
| "mmHg";
|
||||
temperature?: "°C" | "°F" | "K";
|
||||
volume?: "L" | "gal" | "ft³" | "m³";
|
||||
volume?: (typeof VOLUME_UNITS)[number];
|
||||
}
|
||||
|
||||
const _statisticTypes = [
|
||||
|
@ -98,7 +98,7 @@ export interface AreasDisplaySelector {
|
||||
|
||||
export interface AttributeSelector {
|
||||
attribute: {
|
||||
entity_id?: string;
|
||||
entity_id?: string | string[];
|
||||
hide_attributes?: readonly string[];
|
||||
} | null;
|
||||
}
|
||||
@ -226,6 +226,7 @@ export interface EntitySelector {
|
||||
include_entities?: string[];
|
||||
exclude_entities?: string[];
|
||||
filter?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
reorder?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@ -333,6 +334,7 @@ export interface NumberSelector {
|
||||
mode?: "box" | "slider";
|
||||
unit_of_measurement?: string;
|
||||
slider_ticks?: boolean;
|
||||
translation_key?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@ -392,8 +394,9 @@ export interface SelectorSelector {
|
||||
export interface StateSelector {
|
||||
state: {
|
||||
extra_options?: { label: string; value: any }[];
|
||||
entity_id?: string;
|
||||
entity_id?: string | string[];
|
||||
attribute?: string;
|
||||
hide_states?: string[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
@ -146,7 +146,12 @@ const cloudyStates = new Set<string>([
|
||||
"lightning-rainy",
|
||||
]);
|
||||
|
||||
const rainStates = new Set<string>(["hail", "rainy", "pouring"]);
|
||||
const rainStates = new Set<string>([
|
||||
"hail",
|
||||
"rainy",
|
||||
"pouring",
|
||||
"lightning-rainy",
|
||||
]);
|
||||
|
||||
const windyStates = new Set<string>(["windy", "windy-variant"]);
|
||||
|
||||
|
@ -358,6 +358,8 @@ export enum ProtocolDataRate {
|
||||
export interface ZWaveJSNodeStatisticsUpdatedMessage {
|
||||
event: "statistics updated";
|
||||
source: "node";
|
||||
nodeId?: number;
|
||||
node_id?: number;
|
||||
commands_tx: number;
|
||||
commands_rx: number;
|
||||
commands_dropped_tx: number;
|
||||
@ -439,6 +441,7 @@ export interface ZwaveJSProvisioningEntry {
|
||||
dsk: string;
|
||||
securityClasses: SecurityClass[];
|
||||
nodeId?: number;
|
||||
status: ProvisioningEntryStatus;
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
@ -922,7 +925,8 @@ export const uploadFirmwareAndBeginUpdate = async (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
file: File,
|
||||
target?: number
|
||||
target?: number,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
@ -934,6 +938,7 @@ export const uploadFirmwareAndBeginUpdate = async (
|
||||
{
|
||||
method: "POST",
|
||||
body: fd,
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -286,7 +286,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${dialogTitle}
|
||||
.heading=${dialogTitle || true}
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
<ha-icon-button
|
||||
@ -438,7 +438,10 @@ class DataEntryFlowDialog extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._loading = "loading_step";
|
||||
const delayedLoading = setTimeout(() => {
|
||||
// only show loading for slow steps to avoid flickering
|
||||
this._loading = "loading_step";
|
||||
}, 250);
|
||||
let _step: DataEntryFlowStep;
|
||||
try {
|
||||
_step = await step;
|
||||
@ -452,6 +455,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
clearTimeout(delayedLoading);
|
||||
this._loading = undefined;
|
||||
}
|
||||
|
||||
|
@ -35,10 +35,16 @@ export const showConfigFlowDialog = (
|
||||
return step;
|
||||
},
|
||||
fetchFlow: async (hass, flowId) => {
|
||||
const step = await fetchConfigFlow(hass, flowId);
|
||||
await hass.loadFragmentTranslation("config");
|
||||
await hass.loadBackendTranslation("config", step.handler);
|
||||
await hass.loadBackendTranslation("selector", step.handler);
|
||||
const [step] = await Promise.all([
|
||||
fetchConfigFlow(hass, flowId),
|
||||
hass.loadFragmentTranslation("config"),
|
||||
]);
|
||||
await Promise.all([
|
||||
hass.loadBackendTranslation("config", step.handler),
|
||||
hass.loadBackendTranslation("selector", step.handler),
|
||||
// Used as fallback if no header defined for step
|
||||
hass.loadBackendTranslation("title", step.handler),
|
||||
]);
|
||||
return step;
|
||||
},
|
||||
handleFlowStep: handleConfigFlowStep,
|
||||
|
@ -212,7 +212,10 @@ class StepFlowCreateEntry extends LitElement {
|
||||
entityIds.push(...entities.map((entity) => entity.entity_id));
|
||||
});
|
||||
|
||||
const entityIdsMapping = getAutomaticEntityIds(this.hass, entityIds);
|
||||
const entityIdsMapping = await getAutomaticEntityIds(
|
||||
this.hass,
|
||||
entityIds
|
||||
);
|
||||
|
||||
Object.entries(entityIdsMapping).forEach(([oldEntityId, newEntityId]) => {
|
||||
if (newEntityId) {
|
||||
|
@ -37,8 +37,14 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
@state() private _stepData?: Record<string, any>;
|
||||
|
||||
@state() private _previewErrors?: Record<string, string>;
|
||||
|
||||
@state() private _submitErrors?: Record<string, string>;
|
||||
|
||||
@state() private _errorMsg?: string;
|
||||
|
||||
private _errors?: Record<string, string>;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
@ -72,7 +78,7 @@ class StepFlowForm extends LitElement {
|
||||
.schema=${autocompleteLoginFields(
|
||||
this.handleReadOnlyFields(step.data_schema)
|
||||
)}
|
||||
.error=${step.errors}
|
||||
.error=${this._errors}
|
||||
.computeLabel=${this._labelCallback}
|
||||
.computeHelper=${this._helperCallback}
|
||||
.computeError=${this._errorCallback}
|
||||
@ -119,7 +125,7 @@ class StepFlowForm extends LitElement {
|
||||
}
|
||||
|
||||
private _setError(ev: CustomEvent) {
|
||||
this.step = { ...this.step, errors: ev.detail };
|
||||
this._previewErrors = ev.detail;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
@ -133,6 +139,21 @@ class StepFlowForm extends LitElement {
|
||||
if (changedProps.has("step") && this.step?.preview) {
|
||||
import(`./previews/flow-preview-${previewModule(this.step.preview)}`);
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.has("step") ||
|
||||
changedProps.has("_previewErrors") ||
|
||||
changedProps.has("_submitErrors")
|
||||
) {
|
||||
this._errors =
|
||||
this.step.errors || this._previewErrors || this._submitErrors
|
||||
? {
|
||||
...this.step.errors,
|
||||
...this._previewErrors,
|
||||
...this._submitErrors,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _clickHandler(ev: MouseEvent) {
|
||||
@ -189,6 +210,7 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
this._loading = true;
|
||||
this._errorMsg = undefined;
|
||||
this._submitErrors = undefined;
|
||||
|
||||
const flowId = this.step.flow_id;
|
||||
|
||||
@ -217,6 +239,7 @@ class StepFlowForm extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._previewErrors = undefined;
|
||||
fireEvent(this, "flow-update", {
|
||||
step,
|
||||
});
|
||||
@ -226,7 +249,7 @@ class StepFlowForm extends LitElement {
|
||||
this._errorMsg = err.body.message;
|
||||
}
|
||||
if (err.body.errors) {
|
||||
this.step = { ...this.step, errors: err.body.errors };
|
||||
this._submitErrors = err.body.errors;
|
||||
}
|
||||
if (!err.body.message && !err.body.errors) {
|
||||
this._errorMsg = "Unknown error occurred";
|
||||
|
@ -79,7 +79,7 @@ class LightColorTempPicker extends LitElement {
|
||||
mode="cursor"
|
||||
@value-changed=${this._ctColorChanged}
|
||||
@slider-moved=${this._ctColorCursorMoved}
|
||||
.ariaLabel=${this.hass.localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.color_temp"
|
||||
)}
|
||||
style=${styleMap({
|
||||
|
@ -15,6 +15,7 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { join } from "lit/directives/join";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
@ -96,6 +97,8 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
|
||||
@state() private _parentEntityIds: string[] = [];
|
||||
|
||||
@state() private _entityId?: string | null;
|
||||
|
||||
@state() private _currView: View = DEFAULT_VIEW;
|
||||
@ -207,8 +210,16 @@ export class MoreInfoDialog extends LitElement {
|
||||
private _goBack() {
|
||||
if (this._childView) {
|
||||
this._childView = undefined;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (this._initialView !== this._currView) {
|
||||
this._setView(this._initialView);
|
||||
return;
|
||||
}
|
||||
if (this._parentEntityIds.length > 0) {
|
||||
this._entityId = this._parentEntityIds.pop();
|
||||
this._currView = DEFAULT_VIEW;
|
||||
this._loadEntityRegistryEntry();
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,7 +313,9 @@ export class MoreInfoDialog extends LitElement {
|
||||
const isDefaultView = this._currView === DEFAULT_VIEW && !this._childView;
|
||||
const isSpecificInitialView =
|
||||
this._initialView !== DEFAULT_VIEW && !this._childView;
|
||||
const showCloseIcon = isDefaultView || isSpecificInitialView;
|
||||
const showCloseIcon =
|
||||
(isDefaultView && this._parentEntityIds.length === 0) ||
|
||||
isSpecificInitialView;
|
||||
|
||||
const context = stateObj
|
||||
? getEntityContext(stateObj, this.hass)
|
||||
@ -521,54 +534,58 @@ export class MoreInfoDialog extends LitElement {
|
||||
@show-child-view=${this._showChildView}
|
||||
@entity-entry-updated=${this._entryUpdated}
|
||||
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
|
||||
@hass-more-info=${this._handleMoreInfoEvent}
|
||||
>
|
||||
${cache(
|
||||
this._childView
|
||||
? html`
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: this._currView === "info"
|
||||
${keyed(
|
||||
this._entityId,
|
||||
cache(
|
||||
this._childView
|
||||
? html`
|
||||
<ha-more-info-info
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
></ha-more-info-info>
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: this._currView === "history"
|
||||
: this._currView === "info"
|
||||
? html`
|
||||
<ha-more-info-history-and-logbook
|
||||
<ha-more-info-info
|
||||
dialogInitialFocus
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
></ha-more-info-history-and-logbook>
|
||||
.entry=${this._entry}
|
||||
.editMode=${this._infoEditMode}
|
||||
></ha-more-info-info>
|
||||
`
|
||||
: this._currView === "settings"
|
||||
: this._currView === "history"
|
||||
? html`
|
||||
<ha-more-info-settings
|
||||
<ha-more-info-history-and-logbook
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
></ha-more-info-history-and-logbook>
|
||||
`
|
||||
: this._currView === "related"
|
||||
: this._currView === "settings"
|
||||
? html`
|
||||
<ha-related-items
|
||||
<ha-more-info-settings
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
.entityId=${this._entityId}
|
||||
.entry=${this._entry}
|
||||
></ha-more-info-settings>
|
||||
`
|
||||
: nothing
|
||||
: this._currView === "related"
|
||||
? html`
|
||||
<ha-related-items
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"}
|
||||
></ha-related-items>
|
||||
`
|
||||
: nothing
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
@ -602,6 +619,19 @@ export class MoreInfoDialog extends LitElement {
|
||||
window.addEventListener("show-dialog", this._disableEscapeKeyClose);
|
||||
}
|
||||
|
||||
private _handleMoreInfoEvent(ev) {
|
||||
ev.stopPropagation();
|
||||
const entityId = ev.detail.entityId;
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
this._parentEntityIds = [...this._parentEntityIds, this._entityId!];
|
||||
this._entityId = entityId;
|
||||
this._currView = DEFAULT_VIEW;
|
||||
this._childView = undefined;
|
||||
this._loadEntityRegistryEntry();
|
||||
}
|
||||
|
||||
private _enableEscapeKeyClose = () => {
|
||||
this._isEscapeEnabled = true;
|
||||
};
|
||||
@ -667,6 +697,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin: 0 0 -10px 0;
|
||||
}
|
||||
|
||||
.title p {
|
||||
|
@ -1,11 +1,19 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import "../../components/ha-badge";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity_registry";
|
||||
import type { TileCardConfig } from "../../panels/lovelace/cards/types";
|
||||
import { importMoreInfoControl } from "../../panels/lovelace/custom-card-helpers";
|
||||
import "../../panels/lovelace/sections/hui-section";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { stateMoreInfoType } from "./state_more_info_control";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import type { LovelaceCardFeatureConfig } from "../../panels/lovelace/card-features/types";
|
||||
import { supportsLightBrightnessCardFeature } from "../../panels/lovelace/card-features/hui-light-brightness-card-feature";
|
||||
import { supportsCoverPositionCardFeature } from "../../panels/lovelace/card-features/hui-cover-position-card-feature";
|
||||
|
||||
@customElement("more-info-content")
|
||||
class MoreInfoContent extends LitElement {
|
||||
@ -33,13 +41,75 @@ class MoreInfoContent extends LitElement {
|
||||
}
|
||||
|
||||
if (!moreInfoType) return nothing;
|
||||
return dynamicElement(moreInfoType, {
|
||||
hass: this.hass,
|
||||
stateObj: this.stateObj,
|
||||
entry: this.entry,
|
||||
editMode: this.editMode,
|
||||
});
|
||||
|
||||
return html`
|
||||
${dynamicElement(moreInfoType, {
|
||||
hass: this.hass,
|
||||
stateObj: this.stateObj,
|
||||
entry: this.entry,
|
||||
editMode: this.editMode,
|
||||
})}
|
||||
${this._showEntityMembers(this.stateObj)
|
||||
? html`
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.config=${this._entitiesSectionConfig(
|
||||
this.stateObj.attributes.entity_id
|
||||
)}
|
||||
>
|
||||
</hui-section>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _showEntityMembers(stateObj: HassEntity): boolean {
|
||||
if (computeStateDomain(stateObj) === "group") {
|
||||
// Don't show entity members for legacy groups as they already show
|
||||
// the members in their more info dialog.
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes &&
|
||||
stateObj.attributes.entity_id &&
|
||||
Array.isArray(stateObj.attributes.entity_id)
|
||||
);
|
||||
}
|
||||
|
||||
private _entitiesSectionConfig = memoizeOne((entityIds: string[]) => {
|
||||
const cards = entityIds.map((entityId) => {
|
||||
const features: LovelaceCardFeatureConfig[] = [];
|
||||
const context = { entity_id: entityId };
|
||||
if (supportsCoverPositionCardFeature(this.hass!, context)) {
|
||||
features.push({
|
||||
type: "cover-position",
|
||||
});
|
||||
} else if (supportsLightBrightnessCardFeature(this.hass!, context)) {
|
||||
features.push({
|
||||
type: "light-brightness",
|
||||
});
|
||||
}
|
||||
return {
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
features_position: "inline",
|
||||
features,
|
||||
grid_options: { columns: 12 },
|
||||
} as TileCardConfig;
|
||||
});
|
||||
return {
|
||||
type: "grid",
|
||||
cards,
|
||||
};
|
||||
});
|
||||
|
||||
static styles = css`
|
||||
hui-section {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -208,6 +208,7 @@ class DialogEditSidebar extends LitElement {
|
||||
ha-md-dialog {
|
||||
min-width: 600px;
|
||||
max-height: 90%;
|
||||
--dialog-content-padding: 8px 24px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 600px), all and (max-height: 500px) {
|
||||
|
@ -2,12 +2,19 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-selector/ha-selector-media";
|
||||
import "../../../../../components/ha-selector/ha-selector";
|
||||
import type { PlayMediaAction } from "../../../../../data/script";
|
||||
import type { MediaSelectorValue } from "../../../../../data/selector";
|
||||
import type {
|
||||
MediaSelectorValue,
|
||||
Selector,
|
||||
} from "../../../../../data/selector";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
const MEDIA_SELECTOR_SCHEMA: Selector = {
|
||||
media: {},
|
||||
};
|
||||
|
||||
@customElement("ha-automation-action-play_media")
|
||||
export class HaPlayMediaAction extends LitElement implements ActionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -38,12 +45,13 @@ export class HaPlayMediaAction extends LitElement implements ActionElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-selector-media
|
||||
<ha-selector
|
||||
.selector=${MEDIA_SELECTOR_SCHEMA}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this._getSelectorValue(this.action)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-selector-media>
|
||||
></ha-selector>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,19 @@
|
||||
import "@material/mwc-button";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mdiClose, mdiPlus, mdiStarFourPoints } from "@mdi/js";
|
||||
import { mdiClose, mdiPlus } from "@mdi/js";
|
||||
import { dump } from "js-yaml";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-textarea";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-labels-picker";
|
||||
import "../../../../components/ha-suggest-with-ai-button";
|
||||
import type { SuggestWithAIGenerateTask } from "../../../../components/ha-suggest-with-ai-button";
|
||||
import "../../category/ha-category-picker";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/chips/ha-chip-set";
|
||||
@ -25,11 +28,12 @@ import type {
|
||||
SaveDialogParams,
|
||||
} from "./show-dialog-automation-save";
|
||||
import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support";
|
||||
import {
|
||||
fetchAITaskPreferences,
|
||||
generateDataAITask,
|
||||
} from "../../../../data/ai_task";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import type { GenDataTaskResult } from "../../../../data/ai_task";
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
import { subscribeOne } from "../../../../common/util/subscribe-one";
|
||||
import { subscribeLabelRegistry } from "../../../../data/label_registry";
|
||||
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||
import { fetchCategoryRegistry } from "../../../../data/category_registry";
|
||||
|
||||
@customElement("ha-dialog-automation-save")
|
||||
class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
@ -43,8 +47,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
|
||||
@state() private _entryUpdates!: EntityRegistryUpdate;
|
||||
|
||||
@state() private _canSuggest = false;
|
||||
|
||||
private _params!: SaveDialogParams;
|
||||
|
||||
@state() private _newName?: string;
|
||||
@ -75,7 +77,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
this._entryUpdates.category ? "category" : "",
|
||||
this._entryUpdates.labels.length > 0 ? "labels" : "",
|
||||
this._entryUpdates.area ? "area" : "",
|
||||
];
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@ -89,15 +91,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (isComponentLoaded(this.hass, "ai_task")) {
|
||||
fetchAITaskPreferences(this.hass).then((prefs) => {
|
||||
this._canSuggest = prefs.gen_data_entity_id !== null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderOptionalChip(id: string, label: string) {
|
||||
if (this._visibleOptionals.includes(id)) {
|
||||
return nothing;
|
||||
@ -267,21 +260,12 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${this._params.title || title}</span>
|
||||
${this._canSuggest
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
id="suggest"
|
||||
slot="actionItems"
|
||||
@click=${this._suggest}
|
||||
label=${this.hass.localize("ui.common.suggest_ai")}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiStarFourPoints}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
: nothing}
|
||||
<ha-suggest-with-ai-button
|
||||
slot="actionItems"
|
||||
.hass=${this.hass}
|
||||
.generateTask=${this._generateTask}
|
||||
@suggestion=${this._handleSuggestion}
|
||||
></ha-suggest-with-ai-button>
|
||||
</ha-dialog-header>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
@ -345,18 +329,182 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _suggest() {
|
||||
const result = await generateDataAITask(this.hass, {
|
||||
task_name: "frontend:automation:save",
|
||||
instructions: `Suggest one name for the following Home Assistant automation.
|
||||
Your answer should only contain the name, without any additional text or formatting.
|
||||
The name should be relevant to the automation's purpose and should not exceed 50 characters.
|
||||
The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}.
|
||||
private _getSuggestData() {
|
||||
return Promise.all([
|
||||
subscribeOne(this.hass.connection, subscribeLabelRegistry).then((labs) =>
|
||||
Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
|
||||
),
|
||||
subscribeOne(this.hass.connection, subscribeEntityRegistry).then((ents) =>
|
||||
Object.fromEntries(ents.map((ent) => [ent.entity_id, ent]))
|
||||
),
|
||||
fetchCategoryRegistry(this.hass.connection, "automation").then((cats) =>
|
||||
Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name]))
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
private _generateTask = async (): Promise<SuggestWithAIGenerateTask> => {
|
||||
const [labels, entities, categories] = await this._getSuggestData();
|
||||
const inspirations: string[] = [];
|
||||
|
||||
const domain = this._params.domain;
|
||||
|
||||
for (const entity of Object.values(this.hass.states)) {
|
||||
const entityEntry = entities[entity.entity_id];
|
||||
if (
|
||||
computeStateDomain(entity) !== domain ||
|
||||
entity.attributes.restored ||
|
||||
!entity.attributes.friendly_name ||
|
||||
!entityEntry
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let inspiration = `- ${entity.attributes.friendly_name}`;
|
||||
|
||||
const category = categories[entityEntry.categories.automation];
|
||||
if (category) {
|
||||
inspiration += ` (category: ${category})`;
|
||||
}
|
||||
|
||||
if (entityEntry.labels.length) {
|
||||
inspiration += ` (labels: ${entityEntry.labels
|
||||
.map((label) => labels[label])
|
||||
.join(", ")})`;
|
||||
}
|
||||
|
||||
inspirations.push(inspiration);
|
||||
}
|
||||
|
||||
const term = this._params.domain === "script" ? "script" : "automation";
|
||||
|
||||
return {
|
||||
type: "data",
|
||||
task: {
|
||||
task_name: `frontend:${term}:save`,
|
||||
instructions: `Suggest in language "${this.hass.language}" a name, description, category and labels for the following Home Assistant ${term}.
|
||||
|
||||
The name should be relevant to the ${term}'s purpose.
|
||||
${
|
||||
inspirations.length
|
||||
? `The name should be in same style and sentence capitalization as existing ${term}s.
|
||||
Suggest a category and labels if relevant to the ${term}'s purpose.
|
||||
Only suggest category and labels that are already used by existing ${term}s.`
|
||||
: `The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}.`
|
||||
}
|
||||
If the ${term} contains 5+ steps, include a short description.
|
||||
|
||||
For inspiration, here are existing ${term}s:
|
||||
${inspirations.join("\n")}
|
||||
|
||||
The ${term} configuration is as follows:
|
||||
|
||||
${dump(this._params.config)}
|
||||
`,
|
||||
});
|
||||
this._newName = result.data.trim();
|
||||
structure: {
|
||||
name: {
|
||||
description: "The name of the automation",
|
||||
required: true,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
description: {
|
||||
description: "A short description of the automation",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
labels: {
|
||||
description: "Labels for the automation",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
category: {
|
||||
description: "The category of the automation",
|
||||
required: false,
|
||||
selector: {
|
||||
select: {
|
||||
options: Object.entries(categories).map(([id, name]) => ({
|
||||
value: id,
|
||||
label: name,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
private async _handleSuggestion(
|
||||
event: CustomEvent<
|
||||
GenDataTaskResult<{
|
||||
name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
labels?: string[];
|
||||
}>
|
||||
>
|
||||
) {
|
||||
const result = event.detail;
|
||||
const [labels, _entities, categories] = await this._getSuggestData();
|
||||
|
||||
this._newName = result.data.name;
|
||||
if (result.data.description) {
|
||||
this._newDescription = result.data.description;
|
||||
if (!this._visibleOptionals.includes("description")) {
|
||||
this._visibleOptionals = [...this._visibleOptionals, "description"];
|
||||
}
|
||||
}
|
||||
if (result.data.category) {
|
||||
// We get back category name, convert it to ID
|
||||
const categoryId = Object.entries(categories).find(
|
||||
([, name]) => name === result.data.category
|
||||
)?.[0];
|
||||
if (categoryId) {
|
||||
this._entryUpdates = {
|
||||
...this._entryUpdates,
|
||||
category: categoryId,
|
||||
};
|
||||
if (!this._visibleOptionals.includes("category")) {
|
||||
this._visibleOptionals = [...this._visibleOptionals, "category"];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.data.labels?.length) {
|
||||
// We get back label names, convert them to IDs
|
||||
const newLabels: Record<string, undefined | string> = Object.fromEntries(
|
||||
result.data.labels.map((name) => [name, undefined])
|
||||
);
|
||||
let toFind = result.data.labels.length;
|
||||
for (const [labelId, labelName] of Object.entries(labels)) {
|
||||
if (labelName in newLabels && newLabels[labelName] === undefined) {
|
||||
newLabels[labelName] = labelId;
|
||||
toFind--;
|
||||
if (toFind === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const foundLabels = Object.values(newLabels).filter(
|
||||
(labelId) => labelId !== undefined
|
||||
);
|
||||
if (foundLabels.length) {
|
||||
this._entryUpdates = {
|
||||
...this._entryUpdates,
|
||||
labels: foundLabels,
|
||||
};
|
||||
if (!this._visibleOptionals.includes("labels")) {
|
||||
this._visibleOptionals = [...this._visibleOptionals, "labels"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
@ -428,7 +576,7 @@ ${dump(this._params.config)}
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
#suggest {
|
||||
ha-suggest-with-ai-button {
|
||||
margin: 8px 16px;
|
||||
}
|
||||
`,
|
||||
|
@ -502,6 +502,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.readOnly=${this._readOnly}
|
||||
@value-changed=${this._yamlChanged}
|
||||
.showErrors=${false}
|
||||
disable-fullscreen
|
||||
></ha-yaml-editor>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
@ -566,6 +566,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
text: html`
|
||||
<ha-yaml-editor
|
||||
read-only
|
||||
disable-fullscreen
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._triggered}
|
||||
></ha-yaml-editor>
|
||||
|
@ -24,7 +24,10 @@ import { baseTriggerStruct, forDictStruct } from "../../structs";
|
||||
import type { TriggerElement } from "../ha-automation-trigger-row";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../../components/ha-form/types";
|
||||
|
||||
const stateTriggerStruct = assign(
|
||||
baseTriggerStruct,
|
||||
@ -54,7 +57,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, entityId, attribute) =>
|
||||
(localize: LocalizeFunc, attribute) =>
|
||||
[
|
||||
{
|
||||
name: "entity_id",
|
||||
@ -63,9 +66,11 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
},
|
||||
{
|
||||
name: "attribute",
|
||||
context: {
|
||||
filter_entity: "entity_id",
|
||||
},
|
||||
selector: {
|
||||
attribute: {
|
||||
entity_id: entityId ? entityId[0] : undefined,
|
||||
hide_attributes: [
|
||||
"access_token",
|
||||
"available_modes",
|
||||
@ -121,6 +126,9 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
},
|
||||
{
|
||||
name: "from",
|
||||
context: {
|
||||
filter_entity: "entity_id",
|
||||
},
|
||||
selector: {
|
||||
state: {
|
||||
extra_options: (attribute
|
||||
@ -133,13 +141,15 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
value: ANY_STATE_VALUE,
|
||||
},
|
||||
]) as any,
|
||||
entity_id: entityId ? entityId[0] : undefined,
|
||||
attribute: attribute,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "to",
|
||||
context: {
|
||||
filter_entity: "entity_id",
|
||||
},
|
||||
selector: {
|
||||
state: {
|
||||
extra_options: (attribute
|
||||
@ -152,13 +162,12 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
value: ANY_STATE_VALUE,
|
||||
},
|
||||
]) as any,
|
||||
entity_id: entityId ? entityId[0] : undefined,
|
||||
attribute: attribute,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "for", selector: { duration: {} } },
|
||||
] as const
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
public shouldUpdate(changedProperties: PropertyValues) {
|
||||
@ -204,11 +213,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
if (!data.attribute && data.from === null) {
|
||||
data.from = ANY_STATE_VALUE;
|
||||
}
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
this.trigger.entity_id,
|
||||
this.trigger.attribute
|
||||
);
|
||||
const schema = this._schema(this.hass.localize, this.trigger.attribute);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
@ -173,6 +173,7 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
.content=${value?.description}
|
||||
></ha-markdown>
|
||||
${html`<ha-selector
|
||||
narrow
|
||||
.hass=${this.hass}
|
||||
.selector=${selector}
|
||||
.key=${key}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-settings-row";
|
||||
@ -14,6 +14,7 @@ import {
|
||||
type AITaskPreferences,
|
||||
} from "../../../data/ai_task";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
|
||||
@customElement("ai-task-pref")
|
||||
export class AITaskPref extends LitElement {
|
||||
@ -25,16 +26,15 @@ export class AITaskPref extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
if (!this.hass || !isComponentLoaded(this.hass, "ai_task")) {
|
||||
return;
|
||||
}
|
||||
fetchAITaskPreferences(this.hass).then((prefs) => {
|
||||
this._prefs = prefs;
|
||||
});
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._prefs) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">
|
||||
@ -84,7 +84,9 @@ export class AITaskPref extends LitElement {
|
||||
<ha-entity-picker
|
||||
data-name="gen_data_entity_id"
|
||||
.hass=${this.hass}
|
||||
.value=${this._prefs.gen_data_entity_id}
|
||||
.disabled=${this._prefs === undefined &&
|
||||
isComponentLoaded(this.hass, "ai_task")}
|
||||
.value=${this._prefs?.gen_data_entity_id}
|
||||
.includeDomains=${["ai_task"]}
|
||||
@value-changed=${this._handlePrefChange}
|
||||
></ha-entity-picker>
|
||||
|
@ -1,9 +1,19 @@
|
||||
import { mdiDotsVertical, mdiDownload } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import "./ha-config-analytics";
|
||||
import {
|
||||
downloadFileSupported,
|
||||
fileDownload,
|
||||
} from "../../../util/file_download";
|
||||
|
||||
@customElement("ha-config-section-analytics")
|
||||
class HaConfigSectionAnalytics extends LitElement {
|
||||
@ -21,6 +31,26 @@ class HaConfigSectionAnalytics extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.analytics.caption")}
|
||||
>
|
||||
${downloadFileSupported(this.hass)
|
||||
? html`
|
||||
<ha-button-menu
|
||||
@action=${this._handleOverflowAction}
|
||||
slot="toolbar-icon"
|
||||
>
|
||||
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
|
||||
</ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiDownload}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.analytics.download_device_info"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: nothing}
|
||||
<div class="content">
|
||||
<ha-config-analytics .hass=${this.hass}></ha-config-analytics>
|
||||
</div>
|
||||
@ -28,6 +58,11 @@ class HaConfigSectionAnalytics extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleOverflowAction(): Promise<void> {
|
||||
const signedPath = await getSignedPath(this.hass, "/api/analytics/devices");
|
||||
fileDownload(signedPath.path);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
@ -26,9 +26,9 @@ import { saveCoreConfig } from "../../../data/core";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "./ai-task-pref";
|
||||
import type { AITaskPref } from "./ai-task-pref";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
|
||||
@customElement("ha-config-section-general")
|
||||
class HaConfigSectionGeneral extends LitElement {
|
||||
@ -58,6 +58,8 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
|
||||
@state() private _updateUnits?: boolean;
|
||||
|
||||
@query("ai-task-pref") private _aiTaskPref!: AITaskPref;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
@ -267,12 +269,10 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
${isComponentLoaded(this.hass, "ai_task")
|
||||
? html`<ai-task-pref
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ai-task-pref>`
|
||||
: nothing}
|
||||
<ai-task-pref
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ai-task-pref>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@ -293,6 +293,12 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
this._timeZone = this.hass.config.time_zone || "Etc/GMT";
|
||||
this._name = this.hass.config.location_name;
|
||||
this._updateUnits = true;
|
||||
|
||||
if (window.location.hash === "#ai-task") {
|
||||
this._aiTaskPref.updateComplete.then(() => {
|
||||
this._aiTaskPref.scrollIntoView();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _handleValueChanged(ev: ValueChangedEvent<string>) {
|
||||
@ -394,7 +400,8 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
ha-card {
|
||||
ha-card,
|
||||
ai-task-pref {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.card-content {
|
||||
|
@ -58,7 +58,7 @@ export class DashboardCard extends LitElement {
|
||||
.card-header {
|
||||
padding: 12px;
|
||||
display: block;
|
||||
text-align: left;
|
||||
text-align: var(--float-start);
|
||||
gap: 8px;
|
||||
}
|
||||
.preview {
|
||||
|
@ -54,6 +54,9 @@ class HaConfigNavigation extends LitElement {
|
||||
`,
|
||||
}));
|
||||
return html`
|
||||
<div class="visually-hidden" role="heading" aria-level="2">
|
||||
${this.hass.localize("panel.config")}
|
||||
</div>
|
||||
<ha-navigation-list
|
||||
has-secondary
|
||||
.hass=${this.hass}
|
||||
@ -68,6 +71,17 @@ class HaConfigNavigation extends LitElement {
|
||||
ha-navigation-list {
|
||||
--navigation-list-item-title-font-size: var(--ha-font-size-l);
|
||||
}
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
></ha-spinner>`
|
||||
: nothing}
|
||||
</div>
|
||||
<span
|
||||
<span slot="headline"
|
||||
>${deviceEntry
|
||||
? computeDeviceNameDisplay(deviceEntry, this.hass)
|
||||
: entity.attributes.friendly_name}</span
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
mdiPlusCircle,
|
||||
mdiRestore,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
@ -273,22 +273,24 @@ export class HaConfigDevicePage extends LitElement {
|
||||
findBatteryChargingEntity(this.hass, entities)
|
||||
);
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("deviceId") || changedProps.has("entries")) {
|
||||
if (changedProps.has("deviceId")) {
|
||||
this._deviceActions = [];
|
||||
this._deviceAlerts = [];
|
||||
this._deleteButtons = [];
|
||||
this._diagnosticDownloadLinks = [];
|
||||
}
|
||||
|
||||
if (changedProps.has("deviceId") || changedProps.has("entries")) {
|
||||
this._fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadDeviceRegistryDetailDialog();
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
@ -989,6 +991,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
}
|
||||
|
||||
private _getDeleteActions() {
|
||||
const deviceId = this.deviceId;
|
||||
const device = this.hass.devices[this.deviceId];
|
||||
|
||||
if (!device) {
|
||||
@ -1058,12 +1061,18 @@ export class HaConfigDevicePage extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
if (this.deviceId !== deviceId) {
|
||||
// abort if the device has changed
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttons.length > 0) {
|
||||
this._deleteButtons = buttons;
|
||||
}
|
||||
}
|
||||
|
||||
private async _getDeviceActions() {
|
||||
const deviceId = this.deviceId;
|
||||
const device = this.hass.devices[this.deviceId];
|
||||
|
||||
if (!device) {
|
||||
@ -1157,14 +1166,25 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
// load matter device actions async to avoid an UI with 0 actions when the matter integration needs very long to get node diagnostics
|
||||
matter.getMatterDeviceActions(this, this.hass, device).then((actions) => {
|
||||
if (this.deviceId !== deviceId) {
|
||||
// abort if the device has changed
|
||||
return;
|
||||
}
|
||||
this._deviceActions = [...actions, ...(this._deviceActions || [])];
|
||||
});
|
||||
}
|
||||
|
||||
if (this.deviceId !== deviceId) {
|
||||
// abort if the device has changed
|
||||
return;
|
||||
}
|
||||
|
||||
this._deviceActions = deviceActions;
|
||||
}
|
||||
|
||||
private async _getDeviceAlerts() {
|
||||
const deviceId = this.deviceId;
|
||||
|
||||
const device = this.hass.devices[this.deviceId];
|
||||
|
||||
if (!device) {
|
||||
@ -1188,6 +1208,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
deviceAlerts.push(...alerts);
|
||||
}
|
||||
|
||||
if (this.deviceId !== deviceId) {
|
||||
// abort if the device has changed
|
||||
return;
|
||||
}
|
||||
|
||||
this._deviceAlerts = deviceAlerts;
|
||||
if (deviceAlerts.length) {
|
||||
this._deviceAlertsActionsTimeout = window.setTimeout(() => {
|
||||
@ -1317,9 +1342,13 @@ export class HaConfigDevicePage extends LitElement {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_disable_config_entry",
|
||||
{ entry_name: config_entry.title }
|
||||
"ui.panel.config.devices.confirm_disable_config_entry_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.devices.confirm_disable_config_entry_message",
|
||||
{ name: config_entry.title }
|
||||
),
|
||||
destructive: true,
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
}))
|
||||
|
@ -19,6 +19,7 @@ import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeFloorName } from "../../../common/entity/compute_floor_name";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
@ -424,6 +425,18 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
);
|
||||
|
||||
let floorName = "—";
|
||||
if (
|
||||
device.area_id &&
|
||||
areas[device.area_id]?.floor_id &&
|
||||
this.hass.floors
|
||||
) {
|
||||
const floorId = areas[device.area_id].floor_id;
|
||||
if (this.hass.floors[floorId!]) {
|
||||
floorName = computeFloorName(this.hass.floors[floorId!]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...device,
|
||||
name: computeDeviceNameDisplay(
|
||||
@ -441,6 +454,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
device.area_id && areas[device.area_id]
|
||||
? areas[device.area_id].name
|
||||
: "—",
|
||||
floor: floorName,
|
||||
integration: deviceEntries.length
|
||||
? deviceEntries
|
||||
.map(
|
||||
@ -524,6 +538,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
groupable: true,
|
||||
minWidth: "120px",
|
||||
},
|
||||
floor: {
|
||||
title: localize("ui.panel.config.devices.data_table.floor"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
minWidth: "120px",
|
||||
defaultHidden: true,
|
||||
},
|
||||
integration: {
|
||||
title: localize("ui.panel.config.devices.data_table.integration"),
|
||||
sortable: true,
|
||||
|
@ -1099,10 +1099,10 @@ ${
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._setFiltersFromUrl();
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
this._setFiltersFromUrl();
|
||||
if (Object.keys(this._filters).length) {
|
||||
return;
|
||||
}
|
||||
@ -1116,7 +1116,7 @@ ${
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const device = this._searchParms.get("device");
|
||||
const label = this._searchParms.has("label");
|
||||
const label = this._searchParms.get("label");
|
||||
|
||||
if (!domain && !configEntry && !label && !device) {
|
||||
return;
|
||||
@ -1128,21 +1128,10 @@ ${
|
||||
"ha-filter-states": [],
|
||||
"ha-filter-integrations": domain ? [domain] : [],
|
||||
"ha-filter-devices": device ? [device] : [],
|
||||
"ha-filter-labels": label ? [label] : [],
|
||||
config_entry: configEntry ? [configEntry] : [],
|
||||
sub_entry: subEntry ? [subEntry] : [],
|
||||
};
|
||||
this._filterLabel();
|
||||
}
|
||||
|
||||
private _filterLabel() {
|
||||
const label = this._searchParms.get("label");
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-labels": [label],
|
||||
};
|
||||
}
|
||||
|
||||
private _clearFilter() {
|
||||
@ -1152,6 +1141,11 @@ ${
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass");
|
||||
let changed = false;
|
||||
if (!this.hass || !this._entities) {
|
||||
|
@ -74,6 +74,7 @@ import type {
|
||||
UpdateEntityRegistryEntryResult,
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
entityRegistryByEntityId,
|
||||
subscribeEntityRegistry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
@ -520,9 +521,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
: true
|
||||
)
|
||||
.map((item) => {
|
||||
const entityRegEntry = entityReg.find(
|
||||
(reg) => reg.entity_id === item.entity_id
|
||||
);
|
||||
const entityRegEntry =
|
||||
entityRegistryByEntityId(entityReg)[item.entity_id];
|
||||
const labels = labelReg && entityRegEntry?.labels;
|
||||
const category = entityRegEntry?.categories.helpers;
|
||||
return {
|
||||
@ -547,7 +547,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
private _labelsForEntity(entityId: string): string[] {
|
||||
return (
|
||||
this.hass.entities[entityId]?.labels ||
|
||||
this._entityReg.find((e) => e.entity_id === entityId)?.labels ||
|
||||
entityRegistryByEntityId(this._entityReg)[entityId]?.labels ||
|
||||
[]
|
||||
);
|
||||
}
|
||||
@ -885,9 +885,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
const labelItems = new Set<string>();
|
||||
this._stateItems
|
||||
.filter((stateItem) =>
|
||||
this._entityReg
|
||||
.find((reg) => reg.entity_id === stateItem.entity_id)
|
||||
?.labels.some((lbl) => filter.includes(lbl))
|
||||
entityRegistryByEntityId(this._entityReg)[
|
||||
stateItem.entity_id
|
||||
]?.labels.some((lbl) => filter.includes(lbl))
|
||||
)
|
||||
.forEach((stateItem) => labelItems.add(stateItem.entity_id));
|
||||
(this._disabledEntityEntries || [])
|
||||
@ -913,9 +913,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.filter(
|
||||
(stateItem) =>
|
||||
filter[0] ===
|
||||
this._entityReg.find(
|
||||
(reg) => reg.entity_id === stateItem.entity_id
|
||||
)?.categories.helpers
|
||||
entityRegistryByEntityId(this._entityReg)[stateItem.entity_id]
|
||||
?.categories.helpers
|
||||
)
|
||||
.forEach((stateItem) => categoryItems.add(stateItem.entity_id));
|
||||
(this._disabledEntityEntries || [])
|
||||
@ -943,9 +942,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _editCategory(helper: any) {
|
||||
const entityReg = this._entityReg.find(
|
||||
(reg) => reg.entity_id === helper.entity_id
|
||||
);
|
||||
const entityReg = entityRegistryByEntityId(this._entityReg)[
|
||||
helper.entity_id
|
||||
];
|
||||
if (!entityReg) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
|
@ -97,15 +97,12 @@ class AddIntegrationDialog extends LitElement {
|
||||
|
||||
public async showDialog(params?: AddIntegrationDialogParams): Promise<void> {
|
||||
const loadPromise = this._load();
|
||||
this._open = true;
|
||||
this._pickedBrand = params?.brand;
|
||||
this._initialFilter = params?.initialFilter;
|
||||
this._narrow = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
if (params?.domain) {
|
||||
this._createFlow(params.domain);
|
||||
// Just open the config flow dialog, do not show this dialog
|
||||
await this._createFlow(params.domain);
|
||||
return;
|
||||
}
|
||||
|
||||
if (params?.brand) {
|
||||
await loadPromise;
|
||||
const brand = this._integrations?.[params.brand];
|
||||
@ -113,6 +110,13 @@ class AddIntegrationDialog extends LitElement {
|
||||
this._fetchFlowsInProgress(Object.keys(brand.integrations));
|
||||
}
|
||||
}
|
||||
// Only open the dialog if no domain is provided
|
||||
this._open = true;
|
||||
this._pickedBrand = params?.brand;
|
||||
this._initialFilter = params?.initialFilter;
|
||||
this._narrow = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user