mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 23:06:40 +00:00
Merge branch 'dev' into frenck-2025-0321
This commit is contained in:
commit
f29f9484b9
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
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -53,3 +53,7 @@ src/cast/dev_const.ts
|
|||||||
|
|
||||||
# test coverage
|
# test coverage
|
||||||
test/coverage/
|
test/coverage/
|
||||||
|
|
||||||
|
# AI tooling
|
||||||
|
.claude
|
||||||
|
|
||||||
|
56
package.json
56
package.json
@ -30,11 +30,11 @@
|
|||||||
"@braintree/sanitize-url": "7.1.1",
|
"@braintree/sanitize-url": "7.1.1",
|
||||||
"@codemirror/autocomplete": "6.18.6",
|
"@codemirror/autocomplete": "6.18.6",
|
||||||
"@codemirror/commands": "6.8.1",
|
"@codemirror/commands": "6.8.1",
|
||||||
"@codemirror/language": "6.11.1",
|
"@codemirror/language": "6.11.2",
|
||||||
"@codemirror/legacy-modes": "6.5.1",
|
"@codemirror/legacy-modes": "6.5.1",
|
||||||
"@codemirror/search": "6.5.11",
|
"@codemirror/search": "6.5.11",
|
||||||
"@codemirror/state": "6.5.2",
|
"@codemirror/state": "6.5.2",
|
||||||
"@codemirror/view": "6.37.2",
|
"@codemirror/view": "6.38.0",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
"@formatjs/intl-datetimeformat": "6.18.0",
|
||||||
"@formatjs/intl-displaynames": "6.8.11",
|
"@formatjs/intl-displaynames": "6.8.11",
|
||||||
@ -45,12 +45,12 @@
|
|||||||
"@formatjs/intl-numberformat": "8.15.4",
|
"@formatjs/intl-numberformat": "8.15.4",
|
||||||
"@formatjs/intl-pluralrules": "5.4.4",
|
"@formatjs/intl-pluralrules": "5.4.4",
|
||||||
"@formatjs/intl-relativetimeformat": "11.4.11",
|
"@formatjs/intl-relativetimeformat": "11.4.11",
|
||||||
"@fullcalendar/core": "6.1.17",
|
"@fullcalendar/core": "6.1.18",
|
||||||
"@fullcalendar/daygrid": "6.1.17",
|
"@fullcalendar/daygrid": "6.1.18",
|
||||||
"@fullcalendar/interaction": "6.1.17",
|
"@fullcalendar/interaction": "6.1.18",
|
||||||
"@fullcalendar/list": "6.1.17",
|
"@fullcalendar/list": "6.1.18",
|
||||||
"@fullcalendar/luxon3": "6.1.17",
|
"@fullcalendar/luxon3": "6.1.18",
|
||||||
"@fullcalendar/timegrid": "6.1.17",
|
"@fullcalendar/timegrid": "6.1.18",
|
||||||
"@lezer/highlight": "1.2.1",
|
"@lezer/highlight": "1.2.1",
|
||||||
"@lit-labs/motion": "1.0.8",
|
"@lit-labs/motion": "1.0.8",
|
||||||
"@lit-labs/observers": "2.0.5",
|
"@lit-labs/observers": "2.0.5",
|
||||||
@ -89,14 +89,14 @@
|
|||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@tsparticles/engine": "3.8.1",
|
"@tsparticles/engine": "3.8.1",
|
||||||
"@tsparticles/preset-links": "3.2.0",
|
"@tsparticles/preset-links": "3.2.0",
|
||||||
"@vaadin/combo-box": "24.8.0",
|
"@vaadin/combo-box": "24.7.9",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.8.0",
|
"@vaadin/vaadin-themable-mixin": "24.7.9",
|
||||||
"@vibrant/color": "4.0.0",
|
"@vibrant/color": "4.0.0",
|
||||||
"@vue/web-component-wrapper": "1.3.0",
|
"@vue/web-component-wrapper": "1.3.0",
|
||||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||||
"app-datepicker": "5.1.1",
|
"app-datepicker": "5.1.1",
|
||||||
"barcode-detector": "3.0.4",
|
"barcode-detector": "3.0.5",
|
||||||
"color-name": "2.0.0",
|
"color-name": "2.0.0",
|
||||||
"comlink": "4.4.2",
|
"comlink": "4.4.2",
|
||||||
"core-js": "3.43.0",
|
"core-js": "3.43.0",
|
||||||
@ -122,7 +122,7 @@
|
|||||||
"lit": "3.3.0",
|
"lit": "3.3.0",
|
||||||
"lit-html": "3.3.0",
|
"lit-html": "3.3.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "15.0.12",
|
"marked": "16.0.0",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "4.0.3",
|
"node-vibrant": "4.0.3",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
@ -135,7 +135,7 @@
|
|||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"superstruct": "2.0.2",
|
"superstruct": "2.0.2",
|
||||||
"tinykeys": "3.0.0",
|
"tinykeys": "3.0.0",
|
||||||
"ua-parser-js": "2.0.3",
|
"ua-parser-js": "2.0.4",
|
||||||
"vis-data": "7.1.9",
|
"vis-data": "7.1.9",
|
||||||
"vue": "2.7.16",
|
"vue": "2.7.16",
|
||||||
"vue2-daterange-picker": "0.6.8",
|
"vue2-daterange-picker": "0.6.8",
|
||||||
@ -149,18 +149,18 @@
|
|||||||
"xss": "1.0.15"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.27.4",
|
"@babel/core": "7.27.7",
|
||||||
"@babel/helper-define-polyfill-provider": "0.6.4",
|
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||||
"@babel/plugin-transform-runtime": "7.27.4",
|
"@babel/plugin-transform-runtime": "7.27.4",
|
||||||
"@babel/preset-env": "7.27.2",
|
"@babel/preset-env": "7.27.2",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.20.2",
|
"@bundle-stats/plugin-webpack-filter": "4.21.0",
|
||||||
"@lokalise/node-api": "14.8.0",
|
"@lokalise/node-api": "14.9.0",
|
||||||
"@octokit/auth-oauth-device": "8.0.1",
|
"@octokit/auth-oauth-device": "8.0.1",
|
||||||
"@octokit/plugin-retry": "8.0.1",
|
"@octokit/plugin-retry": "8.0.1",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@rsdoctor/rspack-plugin": "1.1.4",
|
"@rsdoctor/rspack-plugin": "1.1.5",
|
||||||
"@rspack/cli": "1.3.12",
|
"@rspack/cli": "1.4.2",
|
||||||
"@rspack/core": "1.3.12",
|
"@rspack/core": "1.4.2",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.22",
|
"@types/chromecast-caf-receiver": "6.0.22",
|
||||||
"@types/chromecast-caf-sender": "1.0.11",
|
"@types/chromecast-caf-sender": "1.0.11",
|
||||||
@ -168,7 +168,7 @@
|
|||||||
"@types/glob": "8.1.0",
|
"@types/glob": "8.1.0",
|
||||||
"@types/html-minifier-terser": "7.0.2",
|
"@types/html-minifier-terser": "7.0.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/leaflet": "1.9.18",
|
"@types/leaflet": "1.9.19",
|
||||||
"@types/leaflet-draw": "1.0.12",
|
"@types/leaflet-draw": "1.0.12",
|
||||||
"@types/leaflet.markercluster": "1.5.5",
|
"@types/leaflet.markercluster": "1.5.5",
|
||||||
"@types/lodash.merge": "4.6.9",
|
"@types/lodash.merge": "4.6.9",
|
||||||
@ -184,13 +184,13 @@
|
|||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
"del": "8.0.0",
|
"del": "8.0.0",
|
||||||
"eslint": "9.29.0",
|
"eslint": "9.30.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-prettier": "10.1.5",
|
"eslint-config-prettier": "10.1.5",
|
||||||
"eslint-import-resolver-webpack": "0.13.10",
|
"eslint-import-resolver-webpack": "0.13.10",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-lit": "2.1.1",
|
"eslint-plugin-lit": "2.1.1",
|
||||||
"eslint-plugin-lit-a11y": "5.0.1",
|
"eslint-plugin-lit-a11y": "5.1.0",
|
||||||
"eslint-plugin-unused-imports": "4.1.4",
|
"eslint-plugin-unused-imports": "4.1.4",
|
||||||
"eslint-plugin-wc": "3.0.1",
|
"eslint-plugin-wc": "3.0.1",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
@ -199,7 +199,7 @@
|
|||||||
"gulp": "5.0.1",
|
"gulp": "5.0.1",
|
||||||
"gulp-brotli": "3.0.0",
|
"gulp-brotli": "3.0.0",
|
||||||
"gulp-json-transform": "0.5.0",
|
"gulp-json-transform": "0.5.0",
|
||||||
"gulp-rename": "2.0.0",
|
"gulp-rename": "2.1.0",
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
@ -210,7 +210,7 @@
|
|||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.6.2",
|
||||||
"rspack-manifest-plugin": "5.0.3",
|
"rspack-manifest-plugin": "5.0.3",
|
||||||
"serve": "14.2.4",
|
"serve": "14.2.4",
|
||||||
"sinon": "21.0.0",
|
"sinon": "21.0.0",
|
||||||
@ -218,7 +218,7 @@
|
|||||||
"terser-webpack-plugin": "5.3.14",
|
"terser-webpack-plugin": "5.3.14",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"typescript-eslint": "8.34.1",
|
"typescript-eslint": "8.35.1",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "3.2.4",
|
"vitest": "3.2.4",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
@ -231,8 +231,8 @@
|
|||||||
"lit-html": "3.3.0",
|
"lit-html": "3.3.0",
|
||||||
"clean-css": "5.3.3",
|
"clean-css": "5.3.3",
|
||||||
"@lit/reactive-element": "2.1.0",
|
"@lit/reactive-element": "2.1.0",
|
||||||
"@fullcalendar/daygrid": "6.1.17",
|
"@fullcalendar/daygrid": "6.1.18",
|
||||||
"globals": "16.2.0",
|
"globals": "16.3.0",
|
||||||
"tslib": "2.8.1",
|
"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"
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20250430.0"
|
version = "20250625.0"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
license-files = ["LICENSE*"]
|
license-files = ["LICENSE*"]
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
|
@ -202,7 +202,6 @@ export function storage(options: {
|
|||||||
// Don't set the initial value if we have a value in localStorage
|
// Don't set the initial value if we have a value in localStorage
|
||||||
if (this.__initialized || getValue() === undefined) {
|
if (this.__initialized || getValue() === undefined) {
|
||||||
setValue(this, value);
|
setValue(this, value);
|
||||||
this.requestUpdate(propertyKey, undefined);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@ -212,11 +211,13 @@ export function storage(options: {
|
|||||||
const oldSetter = descriptor.set;
|
const oldSetter = descriptor.set;
|
||||||
newDescriptor = {
|
newDescriptor = {
|
||||||
...descriptor,
|
...descriptor,
|
||||||
|
get(this: ReactiveStorageElement) {
|
||||||
|
return getValue();
|
||||||
|
},
|
||||||
set(this: ReactiveStorageElement, value) {
|
set(this: ReactiveStorageElement, value) {
|
||||||
// Don't set the initial value if we have a value in localStorage
|
// Don't set the initial value if we have a value in localStorage
|
||||||
if (this.__initialized || getValue() === undefined) {
|
if (this.__initialized || getValue() === undefined) {
|
||||||
setValue(this, value);
|
setValue(this, value);
|
||||||
this.requestUpdate(propertyKey, undefined);
|
|
||||||
}
|
}
|
||||||
oldSetter?.call(this, value);
|
oldSetter?.call(this, value);
|
||||||
},
|
},
|
||||||
|
68
src/common/entity/group_entities.ts
Normal file
68
src/common/entity/group_entities.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
||||||
|
if (!states.length) {
|
||||||
|
return UNAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validState = states.filter((stateObj) => isUnavailableState(stateObj));
|
||||||
|
|
||||||
|
if (!validState) {
|
||||||
|
return UNAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first state to determine the domain
|
||||||
|
// This assumes all states in the group have the same domain
|
||||||
|
const domain = computeStateDomain(states[0]);
|
||||||
|
|
||||||
|
if (domain === "cover") {
|
||||||
|
for (const s of ["opening", "closing", "open"]) {
|
||||||
|
if (states.some((stateObj) => stateObj.state === s)) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "closed";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (states.some((stateObj) => stateObj.state === "on")) {
|
||||||
|
return "on";
|
||||||
|
}
|
||||||
|
return "off";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleGroupEntities = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
states: HassEntity[]
|
||||||
|
) => {
|
||||||
|
if (!states.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first state to determine the domain
|
||||||
|
// This assumes all states in the group have the same domain
|
||||||
|
const domain = computeStateDomain(states[0]);
|
||||||
|
|
||||||
|
const state = computeGroupEntitiesState(states);
|
||||||
|
|
||||||
|
const isOn = state === "on" || state === "open";
|
||||||
|
|
||||||
|
let service = isOn ? "turn_off" : "turn_on";
|
||||||
|
if (domain === "cover") {
|
||||||
|
if (state === "opening" || state === "closing") {
|
||||||
|
// If the cover is opening or closing, we toggle it to stop it
|
||||||
|
service = "stop_cover";
|
||||||
|
} else {
|
||||||
|
// For covers, we use the open/close service
|
||||||
|
service = isOn ? "close_cover" : "open_cover";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entitiesIds = states.map((stateObj) => stateObj.entity_id);
|
||||||
|
|
||||||
|
hass.callService(domain, service, {
|
||||||
|
entity_id: entitiesIds,
|
||||||
|
});
|
||||||
|
};
|
@ -64,15 +64,27 @@ export const domainStateColorProperties = (
|
|||||||
const compareState = state !== undefined ? state : stateObj.state;
|
const compareState = state !== undefined ? state : stateObj.state;
|
||||||
const active = stateActive(stateObj, state);
|
const active = stateActive(stateObj, state);
|
||||||
|
|
||||||
|
return domainColorProperties(
|
||||||
|
domain,
|
||||||
|
stateObj.attributes.device_class,
|
||||||
|
compareState,
|
||||||
|
active
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const domainColorProperties = (
|
||||||
|
domain: string,
|
||||||
|
deviceClass: string | undefined,
|
||||||
|
state: string,
|
||||||
|
active: boolean
|
||||||
|
) => {
|
||||||
const properties: string[] = [];
|
const properties: string[] = [];
|
||||||
|
|
||||||
const stateKey = slugify(compareState, "_");
|
const stateKey = slugify(state, "_");
|
||||||
const activeKey = active ? "active" : "inactive";
|
const activeKey = active ? "active" : "inactive";
|
||||||
|
|
||||||
const dc = stateObj.attributes.device_class;
|
if (deviceClass) {
|
||||||
|
properties.push(`--state-${domain}-${deviceClass}-${stateKey}-color`);
|
||||||
if (dc) {
|
|
||||||
properties.push(`--state-${domain}-${dc}-${stateKey}-color`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
properties.push(
|
properties.push(
|
||||||
|
@ -2,12 +2,13 @@ import memoizeOne from "memoize-one";
|
|||||||
import { isIPAddress } from "./is_ip_address";
|
import { isIPAddress } from "./is_ip_address";
|
||||||
|
|
||||||
const collator = memoizeOne(
|
const collator = memoizeOne(
|
||||||
(language: string | undefined) => new Intl.Collator(language)
|
(language: string | undefined) =>
|
||||||
|
new Intl.Collator(language, { numeric: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
const caseInsensitiveCollator = memoizeOne(
|
const caseInsensitiveCollator = memoizeOne(
|
||||||
(language: string | undefined) =>
|
(language: string | undefined) =>
|
||||||
new Intl.Collator(language, { sensitivity: "accent" })
|
new Intl.Collator(language, { sensitivity: "accent", numeric: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
const fallbackStringCompare = (a: string, b: string) => {
|
const fallbackStringCompare = (a: string, b: string) => {
|
||||||
|
@ -390,6 +390,7 @@ export class HaChartBase extends LitElement {
|
|||||||
type: "inside",
|
type: "inside",
|
||||||
orient: "horizontal",
|
orient: "horizontal",
|
||||||
filterMode: "none",
|
filterMode: "none",
|
||||||
|
xAxisIndex: 0,
|
||||||
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||||
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||||
zoomLock: !this._isTouchDevice && !this._modifierPressed,
|
zoomLock: !this._isTouchDevice && !this._modifierPressed,
|
||||||
|
@ -366,6 +366,7 @@ export class HaAreaPicker extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
|
.helper=${this.helper}
|
||||||
.notFoundLabel=${this.hass.localize(
|
.notFoundLabel=${this.hass.localize(
|
||||||
"ui.components.area-picker.no_match"
|
"ui.components.area-picker.no_match"
|
||||||
)}
|
)}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { mdiTextureBox } from "@mdi/js";
|
import { mdiDrag, mdiTextureBox } from "@mdi/js";
|
||||||
import type { TemplateResult } from "lit";
|
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 { customElement, property } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||||
import { stringCompare } from "../common/string/compare";
|
|
||||||
import { areaCompare } from "../data/area_registry";
|
import { areaCompare } from "../data/area_registry";
|
||||||
import type { FloorRegistryEntry } from "../data/floor_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 type { HomeAssistant } from "../types";
|
||||||
import "./ha-expansion-panel";
|
import "./ha-expansion-panel";
|
||||||
import "./ha-floor-icon";
|
import "./ha-floor-icon";
|
||||||
@ -17,9 +18,14 @@ import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
|||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./ha-textfield";
|
import "./ha-textfield";
|
||||||
|
|
||||||
export interface AreasDisplayValue {
|
export interface AreasFloorsDisplayValue {
|
||||||
hidden?: string[];
|
areas_display?: {
|
||||||
order?: string[];
|
hidden?: string[];
|
||||||
|
order?: string[];
|
||||||
|
};
|
||||||
|
floors_display?: {
|
||||||
|
order?: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
const UNASSIGNED_FLOOR = "__unassigned__";
|
||||||
@ -30,12 +36,10 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: AreasDisplayValue;
|
@property({ attribute: false }) public value?: AreasFloorsDisplayValue;
|
||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@property({ type: Boolean }) public expanded = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
@ -44,51 +48,79 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
|||||||
public showNavigationButton = false;
|
public showNavigationButton = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
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) =>
|
(floor) =>
|
||||||
// Only include floors that have areas assigned to them
|
// Only include floors that have areas assigned to them
|
||||||
groupedItems[floor.floor_id]?.length > 0
|
groupedAreasItems[floor.floor_id]?.length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
const value: DisplayValue = {
|
const value: DisplayValue = {
|
||||||
order: this.value?.order ?? [],
|
order: this.value?.areas_display?.order ?? [],
|
||||||
hidden: this.value?.hidden ?? [],
|
hidden: this.value?.areas_display?.hidden ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canReorderFloors =
|
||||||
|
filteredFloors.filter((floor) => floor.floor_id !== UNASSIGNED_FLOOR)
|
||||||
|
.length > 1;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-expansion-panel
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
outlined
|
<ha-sortable
|
||||||
.header=${this.label}
|
draggable-selector=".draggable"
|
||||||
.expanded=${this.expanded}
|
handle-selector=".handle"
|
||||||
|
@item-moved=${this._floorMoved}
|
||||||
|
.disabled=${this.disabled || !canReorderFloors}
|
||||||
|
invert-swap
|
||||||
>
|
>
|
||||||
<ha-svg-icon slot="leading-icon" .path=${mdiTextureBox}></ha-svg-icon>
|
<div>
|
||||||
${filteredFloors.map(
|
${repeat(
|
||||||
(floor) => html`
|
filteredFloors,
|
||||||
<div class="floor">
|
(floor) => floor.floor_id,
|
||||||
<div class="header">
|
(floor: FloorRegistryEntry) => html`
|
||||||
<ha-floor-icon .floor=${floor}></ha-floor-icon>
|
<ha-expansion-panel
|
||||||
<p>${computeFloorName(floor)}</p>
|
outlined
|
||||||
</div>
|
.header=${computeFloorName(floor)}
|
||||||
<div class="areas">
|
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
|
<ha-items-display-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.items=${groupedItems[floor.floor_id] || []}
|
.items=${groupedAreasItems[floor.floor_id]}
|
||||||
.value=${value}
|
.value=${value}
|
||||||
.floorId=${floor.floor_id}
|
.floorId=${floor.floor_id}
|
||||||
@value-changed=${this._areaDisplayChanged}
|
@value-changed=${this._areaDisplayChanged}
|
||||||
.showNavigationButton=${this.showNavigationButton}
|
.showNavigationButton=${this.showNavigationButton}
|
||||||
></ha-items-display-editor>
|
></ha-items-display-editor>
|
||||||
</div>
|
</ha-expansion-panel>
|
||||||
</div>
|
`
|
||||||
`
|
)}
|
||||||
)}
|
</div>
|
||||||
</ha-expansion-panel>
|
</ha-sortable>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _groupedItems = memoizeOne(
|
private _groupedAreasItems = memoizeOne(
|
||||||
(
|
(
|
||||||
hassAreas: HomeAssistant["areas"],
|
hassAreas: HomeAssistant["areas"],
|
||||||
// update items if floors change
|
// update items if floors change
|
||||||
@ -112,7 +144,6 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
|||||||
label: area.name,
|
label: area.name,
|
||||||
icon: area.icon ?? undefined,
|
icon: area.icon ?? undefined,
|
||||||
iconPath: mdiTextureBox,
|
iconPath: mdiTextureBox,
|
||||||
description: floor?.name,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
@ -124,20 +155,19 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private _sortedFloors = memoizeOne(
|
private _sortedFloors = memoizeOne(
|
||||||
(hassFloors: HomeAssistant["floors"]): FloorRegistryEntry[] => {
|
(
|
||||||
const floors = Object.values(hassFloors).sort((floorA, floorB) => {
|
hassFloors: HomeAssistant["floors"],
|
||||||
if (floorA.level !== floorB.level) {
|
order: string[] | undefined
|
||||||
return (floorA.level ?? 0) - (floorB.level ?? 0);
|
): FloorRegistryEntry[] => {
|
||||||
}
|
const floors = getFloors(hassFloors, order);
|
||||||
return stringCompare(floorA.name, floorB.name);
|
const noFloors = floors.length === 0;
|
||||||
});
|
|
||||||
floors.push({
|
floors.push({
|
||||||
floor_id: UNASSIGNED_FLOOR,
|
floor_id: UNASSIGNED_FLOOR,
|
||||||
name: this.hass.localize(
|
name: noFloors
|
||||||
"ui.panel.lovelace.strategy.areas.unassigned_areas"
|
? this.hass.localize("ui.panel.lovelace.strategy.areas.areas")
|
||||||
),
|
: this.hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
||||||
icon: null,
|
icon: null,
|
||||||
level: 999999,
|
level: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
@ -146,68 +176,101 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private async _areaDisplayChanged(ev) {
|
private _floorMoved(ev: CustomEvent<HASSDomEvents["item-moved"]>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = ev.detail.value as DisplayValue;
|
const newIndex = ev.detail.newIndex;
|
||||||
const currentFloorId = ev.currentTarget.floorId;
|
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(
|
private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) {
|
||||||
(floor) => floor.floor_id
|
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 newHidden: string[] = [];
|
||||||
const newOrder: string[] = [];
|
const newOrder: string[] = [];
|
||||||
|
|
||||||
for (const floorId of floorIds) {
|
for (const floorId of floorIds) {
|
||||||
if (currentFloorId === floorId) {
|
if ((currentFloorId ?? UNASSIGNED_FLOOR) === floorId) {
|
||||||
newHidden.push(...(value.hidden ?? []));
|
newHidden.push(...(value.hidden ?? []));
|
||||||
newOrder.push(...(value.order ?? []));
|
newOrder.push(...(value.order ?? []));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const hidden = this.value?.hidden?.filter(
|
const hidden = oldHidden.filter((areaId) => {
|
||||||
(areaId) => this.hass.areas[areaId]?.floor_id === floorId
|
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||||
);
|
return id === floorId;
|
||||||
if (hidden) {
|
});
|
||||||
|
if (hidden?.length) {
|
||||||
newHidden.push(...hidden);
|
newHidden.push(...hidden);
|
||||||
}
|
}
|
||||||
const order = this.value?.order?.filter(
|
const order = oldOrder.filter((areaId) => {
|
||||||
(areaId) => this.hass.areas[areaId]?.floor_id === floorId
|
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||||
);
|
return id === floorId;
|
||||||
if (order) {
|
});
|
||||||
|
if (order?.length) {
|
||||||
newOrder.push(...order);
|
newOrder.push(...order);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newValue: AreasDisplayValue = {
|
const newValue: AreasFloorsDisplayValue = {
|
||||||
hidden: newHidden,
|
areas_display: {
|
||||||
order: newOrder,
|
hidden: newHidden,
|
||||||
|
order: newOrder,
|
||||||
|
},
|
||||||
|
floors_display: this.value?.floors_display,
|
||||||
};
|
};
|
||||||
if (newValue.hidden?.length === 0) {
|
if (newValue.areas_display?.hidden?.length === 0) {
|
||||||
delete newValue.hidden;
|
delete newValue.areas_display.hidden;
|
||||||
}
|
}
|
||||||
if (newValue.order?.length === 0) {
|
if (newValue.areas_display?.order?.length === 0) {
|
||||||
delete newValue.order;
|
delete newValue.areas_display.order;
|
||||||
|
}
|
||||||
|
if (newValue.floors_display?.order?.length === 0) {
|
||||||
|
delete newValue.floors_display.order;
|
||||||
}
|
}
|
||||||
|
|
||||||
fireEvent(this, "value-changed", { value: newValue });
|
fireEvent(this, "value-changed", { value: newValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
.floor .header p {
|
ha-expansion-panel {
|
||||||
margin: 0;
|
margin-bottom: 8px;
|
||||||
overflow: hidden;
|
--expansion-panel-summary-padding: 0 16px;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1:
|
|
||||||
}
|
}
|
||||||
.floor .header {
|
ha-expansion-panel [slot="leading-icon"] {
|
||||||
margin: 16px 0 8px 0;
|
margin-inline-end: 16px;
|
||||||
padding: 0 8px;
|
}
|
||||||
display: flex;
|
label {
|
||||||
flex-direction: row;
|
display: block;
|
||||||
align-items: center;
|
font-weight: var(--ha-font-weight-bold);
|
||||||
gap: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import type {
|
|||||||
} from "@codemirror/autocomplete";
|
} from "@codemirror/autocomplete";
|
||||||
import type { Extension, TransactionSpec } from "@codemirror/state";
|
import type { Extension, TransactionSpec } from "@codemirror/state";
|
||||||
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||||
|
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
|
||||||
import type { HassEntities } from "home-assistant-js-websocket";
|
import type { HassEntities } from "home-assistant-js-websocket";
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, ReactiveElement } from "lit";
|
import { css, ReactiveElement } from "lit";
|
||||||
@ -15,6 +16,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
|||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@ -59,8 +61,13 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public error = false;
|
@property({ type: Boolean }) public error = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "disable-fullscreen" })
|
||||||
|
public disableFullscreen = false;
|
||||||
|
|
||||||
@state() private _value = "";
|
@state() private _value = "";
|
||||||
|
|
||||||
|
@state() private _isFullscreen = false;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
||||||
|
|
||||||
@ -92,6 +99,7 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
this.addEventListener("keydown", stopPropagation);
|
this.addEventListener("keydown", stopPropagation);
|
||||||
|
this.addEventListener("keydown", this._handleKeyDown);
|
||||||
// This is unreachable as editor will not exist yet,
|
// This is unreachable as editor will not exist yet,
|
||||||
// but focus should not behave like this for good a11y.
|
// but focus should not behave like this for good a11y.
|
||||||
// (@steverep to fix in autofocus PR)
|
// (@steverep to fix in autofocus PR)
|
||||||
@ -106,6 +114,10 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.removeEventListener("keydown", stopPropagation);
|
this.removeEventListener("keydown", stopPropagation);
|
||||||
|
this.removeEventListener("keydown", this._handleKeyDown);
|
||||||
|
if (this._isFullscreen) {
|
||||||
|
this._toggleFullscreen();
|
||||||
|
}
|
||||||
this.updateComplete.then(() => {
|
this.updateComplete.then(() => {
|
||||||
this.codemirror!.destroy();
|
this.codemirror!.destroy();
|
||||||
delete this.codemirror;
|
delete this.codemirror;
|
||||||
@ -164,6 +176,12 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
if (changedProps.has("error")) {
|
if (changedProps.has("error")) {
|
||||||
this.classList.toggle("error-state", this.error);
|
this.classList.toggle("error-state", this.error);
|
||||||
}
|
}
|
||||||
|
if (changedProps.has("_isFullscreen")) {
|
||||||
|
this.classList.toggle("fullscreen", this._isFullscreen);
|
||||||
|
}
|
||||||
|
if (changedProps.has("disableFullscreen")) {
|
||||||
|
this._updateFullscreenButton();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _mode() {
|
private get _mode() {
|
||||||
@ -238,8 +256,74 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
}),
|
}),
|
||||||
parent: this.renderRoot,
|
parent: this.renderRoot,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._updateFullscreenButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _updateFullscreenButton() {
|
||||||
|
const existingButton = this.renderRoot.querySelector(".fullscreen-button");
|
||||||
|
|
||||||
|
if (this.disableFullscreen) {
|
||||||
|
// Remove button if it exists and fullscreen is disabled
|
||||||
|
if (existingButton) {
|
||||||
|
existingButton.remove();
|
||||||
|
}
|
||||||
|
// Exit fullscreen if currently in fullscreen mode
|
||||||
|
if (this._isFullscreen) {
|
||||||
|
this._isFullscreen = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create button if it doesn't exist
|
||||||
|
if (!existingButton) {
|
||||||
|
const button = document.createElement("ha-icon-button");
|
||||||
|
(button as any).path = this._isFullscreen
|
||||||
|
? mdiArrowCollapse
|
||||||
|
: mdiArrowExpand;
|
||||||
|
button.setAttribute(
|
||||||
|
"label",
|
||||||
|
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
|
||||||
|
);
|
||||||
|
button.classList.add("fullscreen-button");
|
||||||
|
// Use bound method to ensure proper this context
|
||||||
|
button.addEventListener("click", this._handleFullscreenClick);
|
||||||
|
this.renderRoot.appendChild(button);
|
||||||
|
} else {
|
||||||
|
// Update existing button
|
||||||
|
(existingButton as any).path = this._isFullscreen
|
||||||
|
? mdiArrowCollapse
|
||||||
|
: mdiArrowExpand;
|
||||||
|
existingButton.setAttribute(
|
||||||
|
"label",
|
||||||
|
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleFullscreenClick = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this._toggleFullscreen();
|
||||||
|
};
|
||||||
|
|
||||||
|
private _toggleFullscreen() {
|
||||||
|
this._isFullscreen = !this._isFullscreen;
|
||||||
|
this._updateFullscreenButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (this._isFullscreen && e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this._toggleFullscreen();
|
||||||
|
} else if (e.key === "F11" && !this.disableFullscreen) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this._toggleFullscreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
|
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
|
||||||
if (!states) {
|
if (!states) {
|
||||||
return [];
|
return [];
|
||||||
@ -460,9 +544,77 @@ export class HaCodeEditor extends ReactiveElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
:host(.error-state) .cm-gutters {
|
:host(.error-state) .cm-gutters {
|
||||||
border-color: var(--error-state-color, red);
|
border-color: var(--error-state-color, red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fullscreen-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
background-color: var(--secondary-background-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
--mdc-icon-button-size: 32px;
|
||||||
|
--mdc-icon-size: 18px;
|
||||||
|
/* Ensure button is clickable on iOS */
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-button:hover,
|
||||||
|
.fullscreen-button:active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.fullscreen-button {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.fullscreen) {
|
||||||
|
position: fixed !important;
|
||||||
|
top: calc(var(--header-height, 56px) + 8px) !important;
|
||||||
|
left: 8px !important;
|
||||||
|
right: 8px !important;
|
||||||
|
bottom: 8px !important;
|
||||||
|
z-index: 9999 !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-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: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.fullscreen) .cm-editor {
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.fullscreen) .fullscreen-button {
|
||||||
|
top: calc(var(--safe-area-inset-top, 0px) + 8px);
|
||||||
|
right: calc(var(--safe-area-inset-right, 0px) + 8px);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,7 +369,6 @@ export class HaComboBox extends LitElement {
|
|||||||
}
|
}
|
||||||
vaadin-combo-box-light {
|
vaadin-combo-box-light {
|
||||||
position: relative;
|
position: relative;
|
||||||
--vaadin-combo-box-overlay-max-height: calc(45vh - 56px);
|
|
||||||
}
|
}
|
||||||
ha-combo-box-textfield {
|
ha-combo-box-textfield {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -18,6 +18,8 @@ export class HaDomainIcon extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public deviceClass?: string;
|
@property({ attribute: false }) public deviceClass?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public state?: string;
|
||||||
|
|
||||||
@property() public icon?: string;
|
@property() public icon?: string;
|
||||||
|
|
||||||
@property({ attribute: "brand-fallback", type: Boolean })
|
@property({ attribute: "brand-fallback", type: Boolean })
|
||||||
@ -36,14 +38,17 @@ export class HaDomainIcon extends LitElement {
|
|||||||
return this._renderFallback();
|
return this._renderFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = domainIcon(this.hass, this.domain, this.deviceClass).then(
|
const icon = domainIcon(
|
||||||
(icn) => {
|
this.hass,
|
||||||
if (icn) {
|
this.domain,
|
||||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
this.deviceClass,
|
||||||
}
|
this.state
|
||||||
return this._renderFallback();
|
).then((icn) => {
|
||||||
|
if (icn) {
|
||||||
|
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||||
}
|
}
|
||||||
);
|
return this._renderFallback();
|
||||||
|
});
|
||||||
|
|
||||||
return html`${until(icon)}`;
|
return html`${until(icon)}`;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import "./ha-check-list-item";
|
|||||||
import "./ha-expansion-panel";
|
import "./ha-expansion-panel";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import "./ha-list";
|
import "./ha-list";
|
||||||
|
import { deepEqual } from "../common/util/deep-equal";
|
||||||
|
|
||||||
@customElement("ha-filter-blueprints")
|
@customElement("ha-filter-blueprints")
|
||||||
export class HaFilterBlueprints extends LitElement {
|
export class HaFilterBlueprints extends LitElement {
|
||||||
@ -34,10 +35,11 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
public willUpdate(properties: PropertyValues) {
|
public willUpdate(properties: PropertyValues) {
|
||||||
super.willUpdate(properties);
|
super.willUpdate(properties);
|
||||||
|
|
||||||
if (!this.hasUpdated) {
|
if (
|
||||||
if (this.value?.length) {
|
properties.has("value") &&
|
||||||
this._findRelated();
|
!deepEqual(this.value, properties.get("value"))
|
||||||
}
|
) {
|
||||||
|
this._findRelated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,17 +132,15 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
|
||||||
this._findRelated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _findRelated() {
|
private async _findRelated() {
|
||||||
if (!this.value?.length) {
|
if (!this.value?.length) {
|
||||||
|
this.value = [];
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
value: [],
|
value: [],
|
||||||
items: undefined,
|
items: undefined,
|
||||||
});
|
});
|
||||||
this.value = [];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
|
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
|
||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import { deepEqual } from "../common/util/deep-equal";
|
||||||
import type { RelatedResult } from "../data/search";
|
import type { RelatedResult } from "../data/search";
|
||||||
import { findRelated } from "../data/search";
|
import { findRelated } from "../data/search";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
@ -37,9 +38,13 @@ export class HaFilterDevices extends LitElement {
|
|||||||
|
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
loadVirtualizer();
|
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];
|
this.value = [...(this.value || []), value];
|
||||||
}
|
}
|
||||||
listItem.selected = this.value?.includes(value);
|
listItem.selected = this.value?.includes(value);
|
||||||
this._findRelated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changed) {
|
protected updated(changed) {
|
||||||
@ -160,11 +164,11 @@ export class HaFilterDevices extends LitElement {
|
|||||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
if (!this.value?.length) {
|
if (!this.value?.length) {
|
||||||
|
this.value = [];
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
value: [],
|
value: [],
|
||||||
items: undefined,
|
items: undefined,
|
||||||
});
|
});
|
||||||
this.value = [];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +180,6 @@ export class HaFilterDevices extends LitElement {
|
|||||||
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.value = value;
|
|
||||||
const results = await Promise.all(relatedPromises);
|
const results = await Promise.all(relatedPromises);
|
||||||
const items = new Set<string>();
|
const items = new Set<string>();
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
|
@ -17,6 +17,7 @@ import "./ha-expansion-panel";
|
|||||||
import "./ha-list";
|
import "./ha-list";
|
||||||
import "./ha-state-icon";
|
import "./ha-state-icon";
|
||||||
import "./search-input-outlined";
|
import "./search-input-outlined";
|
||||||
|
import { deepEqual } from "../common/util/deep-equal";
|
||||||
|
|
||||||
@customElement("ha-filter-entities")
|
@customElement("ha-filter-entities")
|
||||||
export class HaFilterEntities extends LitElement {
|
export class HaFilterEntities extends LitElement {
|
||||||
@ -39,9 +40,13 @@ export class HaFilterEntities extends LitElement {
|
|||||||
|
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
loadVirtualizer();
|
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];
|
this.value = [...(this.value || []), value];
|
||||||
}
|
}
|
||||||
listItem.selected = this.value?.includes(value);
|
listItem.selected = this.value?.includes(value);
|
||||||
this._findRelated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _expandedWillChange(ev) {
|
private _expandedWillChange(ev) {
|
||||||
@ -178,11 +182,11 @@ export class HaFilterEntities extends LitElement {
|
|||||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
if (!this.value?.length) {
|
if (!this.value?.length) {
|
||||||
|
this.value = [];
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
value: [],
|
value: [],
|
||||||
items: undefined,
|
items: undefined,
|
||||||
});
|
});
|
||||||
this.value = [];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import "./ha-icon-button";
|
|||||||
import "./ha-list";
|
import "./ha-list";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./ha-tree-indicator";
|
import "./ha-tree-indicator";
|
||||||
|
import { deepEqual } from "../common/util/deep-equal";
|
||||||
|
|
||||||
@customElement("ha-filter-floor-areas")
|
@customElement("ha-filter-floor-areas")
|
||||||
export class HaFilterFloorAreas extends LitElement {
|
export class HaFilterFloorAreas extends LitElement {
|
||||||
@ -41,10 +42,11 @@ export class HaFilterFloorAreas extends LitElement {
|
|||||||
public willUpdate(properties: PropertyValues) {
|
public willUpdate(properties: PropertyValues) {
|
||||||
super.willUpdate(properties);
|
super.willUpdate(properties);
|
||||||
|
|
||||||
if (!this.hasUpdated) {
|
if (
|
||||||
if (this.value?.floors?.length || this.value?.areas?.length) {
|
properties.has("value") &&
|
||||||
this._findRelated();
|
!deepEqual(this.value, properties.get("value"))
|
||||||
}
|
) {
|
||||||
|
this._findRelated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,8 +176,6 @@ export class HaFilterFloorAreas extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listItem.selected = this.value[type]?.includes(value);
|
listItem.selected = this.value[type]?.includes(value);
|
||||||
|
|
||||||
this._findRelated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changed) {
|
protected updated(changed) {
|
||||||
@ -188,10 +188,6 @@ export class HaFilterFloorAreas extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
|
||||||
this._findRelated();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _expandedWillChange(ev) {
|
private _expandedWillChange(ev) {
|
||||||
this._shouldRender = ev.detail.expanded;
|
this._shouldRender = ev.detail.expanded;
|
||||||
}
|
}
|
||||||
@ -226,6 +222,7 @@ export class HaFilterFloorAreas extends LitElement {
|
|||||||
!this.value ||
|
!this.value ||
|
||||||
(!this.value.areas?.length && !this.value.floors?.length)
|
(!this.value.areas?.length && !this.value.floors?.length)
|
||||||
) {
|
) {
|
||||||
|
this.value = {};
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
value: {},
|
value: {},
|
||||||
items: undefined,
|
items: undefined,
|
||||||
|
@ -24,12 +24,16 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public localizeValue?: (key: string) => string;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
private _selectSchema = memoizeOne(
|
private _selectSchema = memoizeOne(
|
||||||
(options): SelectSelector => ({
|
(schema: HaFormSelectSchema): SelectSelector => ({
|
||||||
select: {
|
select: {
|
||||||
options: options.map((option) => ({
|
translation_key: schema.name,
|
||||||
|
options: schema.options.map((option) => ({
|
||||||
value: option[0],
|
value: option[0],
|
||||||
label: option[1],
|
label: option[1],
|
||||||
})),
|
})),
|
||||||
@ -41,13 +45,13 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-selector-select
|
<ha-selector-select
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.schema=${this.schema}
|
|
||||||
.value=${this.data}
|
.value=${this.data}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.schema.required}
|
.required=${this.schema.required || false}
|
||||||
.selector=${this._selectSchema(this.schema.options)}
|
.selector=${this._selectSchema(this.schema)}
|
||||||
|
.localizeValue=${this.localizeValue}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></ha-selector-select>
|
></ha-selector-select>
|
||||||
`;
|
`;
|
||||||
|
@ -122,22 +122,6 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
${description
|
${description
|
||||||
? html`<span slot="supporting-text">${description}</span>`
|
? html`<span slot="supporting-text">${description}</span>`
|
||||||
: nothing}
|
: 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
|
${!showIcon
|
||||||
? nothing
|
? nothing
|
||||||
: icon
|
: icon
|
||||||
@ -162,6 +146,9 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
<span slot="end"> ${this.actionsRenderer(item)} </span>
|
<span slot="end"> ${this.actionsRenderer(item)} </span>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
${this.showNavigationButton
|
||||||
|
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||||
|
: nothing}
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||||
slot="end"
|
slot="end"
|
||||||
@ -174,9 +161,22 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
.value=${value}
|
.value=${value}
|
||||||
@click=${this._toggle}
|
@click=${this._toggle}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
${this.showNavigationButton
|
${isVisible && !disableSorting
|
||||||
? html` <ha-icon-next slot="end"></ha-icon-next> `
|
? html`
|
||||||
: nothing}
|
<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>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ class HaNavigationList extends LitElement {
|
|||||||
>
|
>
|
||||||
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
|
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
|
||||||
</div>
|
</div>
|
||||||
<span>${page.name}</span>
|
<span slot="headline">${page.name}</span>
|
||||||
${this.hasSecondary
|
${this.hasSecondary
|
||||||
? html`<span slot="supporting-text">${page.description}</span>`
|
? html`<span slot="supporting-text">${page.description}</span>`
|
||||||
: ""}
|
: ""}
|
||||||
|
@ -54,7 +54,7 @@ export class HaAreaSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
if (changedProperties.has("selector") && this.value !== undefined) {
|
if (changedProperties.get("selector") && this.value !== undefined) {
|
||||||
if (this.selector.area?.multiple && !Array.isArray(this.value)) {
|
if (this.selector.area?.multiple && !Array.isArray(this.value)) {
|
||||||
this.value = [this.value];
|
this.value = [this.value];
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
@ -56,7 +56,7 @@ export class HaDeviceSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
if (changedProperties.has("selector") && this.value !== undefined) {
|
if (changedProperties.get("selector") && this.value !== undefined) {
|
||||||
if (this.selector.device?.multiple && !Array.isArray(this.value)) {
|
if (this.selector.device?.multiple && !Array.isArray(this.value)) {
|
||||||
this.value = [this.value];
|
this.value = [this.value];
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
@ -43,7 +43,7 @@ export class HaEntitySelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
if (changedProperties.has("selector") && this.value !== undefined) {
|
if (changedProperties.get("selector") && this.value !== undefined) {
|
||||||
if (this.selector.entity?.multiple && !Array.isArray(this.value)) {
|
if (this.selector.entity?.multiple && !Array.isArray(this.value)) {
|
||||||
this.value = [this.value];
|
this.value = [this.value];
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
@ -54,7 +54,7 @@ export class HaFloorSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
if (changedProperties.has("selector") && this.value !== undefined) {
|
if (changedProperties.get("selector") && this.value !== undefined) {
|
||||||
if (this.selector.floor?.multiple && !Array.isArray(this.value)) {
|
if (this.selector.floor?.multiple && !Array.isArray(this.value)) {
|
||||||
this.value = [this.value];
|
this.value = [this.value];
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
@ -24,6 +24,10 @@ const MANUAL_SCHEMA = [
|
|||||||
{ name: "media_content_type", required: false, selector: { text: {} } },
|
{ name: "media_content_type", required: false, selector: { text: {} } },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const INCLUDE_DOMAINS = ["media_player"];
|
||||||
|
|
||||||
|
const EMPTY_FORM = {};
|
||||||
|
|
||||||
@customElement("ha-selector-media")
|
@customElement("ha-selector-media")
|
||||||
export class HaMediaSelector extends LitElement {
|
export class HaMediaSelector extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -84,7 +88,7 @@ export class HaMediaSelector extends LitElement {
|
|||||||
(stateObj &&
|
(stateObj &&
|
||||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
|
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
|
||||||
|
|
||||||
const hasAccept = this.selector.media?.accept?.length;
|
const hasAccept = this.selector?.media?.accept?.length;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${hasAccept
|
${hasAccept
|
||||||
@ -100,7 +104,7 @@ export class HaMediaSelector extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
include-domains='["media_player"]'
|
.includeDomains=${INCLUDE_DOMAINS}
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
@value-changed=${this._entityChanged}
|
@value-changed=${this._entityChanged}
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
@ -114,7 +118,7 @@ export class HaMediaSelector extends LitElement {
|
|||||||
</ha-alert>
|
</ha-alert>
|
||||||
<ha-form
|
<ha-form
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.data=${this.value}
|
.data=${this.value || EMPTY_FORM}
|
||||||
.schema=${MANUAL_SCHEMA}
|
.schema=${MANUAL_SCHEMA}
|
||||||
.computeLabel=${this._computeLabelCallback}
|
.computeLabel=${this._computeLabelCallback}
|
||||||
></ha-form>
|
></ha-form>
|
||||||
@ -122,63 +126,62 @@ export class HaMediaSelector extends LitElement {
|
|||||||
: html`
|
: html`
|
||||||
<ha-card
|
<ha-card
|
||||||
outlined
|
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}
|
@click=${this._pickMedia}
|
||||||
|
@keydown=${this._handleKeyDown}
|
||||||
class=${this.disabled || (!this.value?.entity_id && !hasAccept)
|
class=${this.disabled || (!this.value?.entity_id && !hasAccept)
|
||||||
? "disabled"
|
? "disabled"
|
||||||
: ""}
|
: ""}
|
||||||
>
|
>
|
||||||
<div
|
<div class="content-container">
|
||||||
class="thumbnail ${classMap({
|
<div class="thumbnail">
|
||||||
portrait:
|
${this.value?.metadata?.thumbnail
|
||||||
!!this.value?.metadata?.media_class &&
|
? html`
|
||||||
MediaClassBrowserSettings[
|
<div
|
||||||
this.value.metadata.children_media_class ||
|
class="${classMap({
|
||||||
this.value.metadata.media_class
|
"centered-image":
|
||||||
].thumbnail_ratio === "portrait",
|
!!this.value.metadata.media_class &&
|
||||||
})}"
|
["app", "directory"].includes(
|
||||||
>
|
this.value.metadata.media_class
|
||||||
${this.value?.metadata?.thumbnail
|
),
|
||||||
? html`
|
})}
|
||||||
<div
|
image"
|
||||||
class="${classMap({
|
style=${this._thumbnailUrl
|
||||||
"centered-image":
|
? `background-image: url(${this._thumbnailUrl});`
|
||||||
!!this.value.metadata.media_class &&
|
: ""}
|
||||||
["app", "directory"].includes(
|
></div>
|
||||||
this.value.metadata.media_class
|
`
|
||||||
),
|
: html`
|
||||||
})}
|
<div class="icon-holder image">
|
||||||
image"
|
<ha-svg-icon
|
||||||
style=${this._thumbnailUrl
|
class="folder"
|
||||||
? `background-image: url(${this._thumbnailUrl});`
|
.path=${!this.value?.media_content_id
|
||||||
: ""}
|
? mdiPlus
|
||||||
></div>
|
: this.value?.metadata?.media_class
|
||||||
`
|
? MediaClassBrowserSettings[
|
||||||
: html`
|
this.value.metadata.media_class ===
|
||||||
<div class="icon-holder image">
|
"directory"
|
||||||
<ha-svg-icon
|
? this.value.metadata
|
||||||
class="folder"
|
.children_media_class ||
|
||||||
.path=${!this.value?.media_content_id
|
this.value.metadata.media_class
|
||||||
? mdiPlus
|
: this.value.metadata.media_class
|
||||||
: this.value?.metadata?.media_class
|
].icon
|
||||||
? MediaClassBrowserSettings[
|
: mdiPlayBox}
|
||||||
this.value.metadata.media_class ===
|
></ha-svg-icon>
|
||||||
"directory"
|
</div>
|
||||||
? this.value.metadata
|
`}
|
||||||
.children_media_class ||
|
</div>
|
||||||
this.value.metadata.media_class
|
<div class="title">
|
||||||
: this.value.metadata.media_class
|
${!this.value?.media_content_id
|
||||||
].icon
|
? this.hass.localize(
|
||||||
: mdiPlayBox}
|
"ui.components.selectors.media.pick_media"
|
||||||
></ha-svg-icon>
|
)
|
||||||
</div>
|
: this.value.metadata?.title || this.value.media_content_id}
|
||||||
`}
|
</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>
|
</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`
|
static styles = css`
|
||||||
ha-entity-picker {
|
ha-entity-picker {
|
||||||
display: block;
|
display: block;
|
||||||
@ -243,41 +253,52 @@ export class HaMediaSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
ha-card {
|
ha-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 200px;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
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 {
|
ha-card.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: var(--disabled-text-color);
|
color: var(--disabled-text-color);
|
||||||
}
|
}
|
||||||
|
.content-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
ha-card .thumbnail {
|
ha-card .thumbnail {
|
||||||
width: 100%;
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: padding-bottom 0.1s ease-out;
|
border-radius: 8px;
|
||||||
padding-bottom: 100%;
|
overflow: hidden;
|
||||||
}
|
|
||||||
ha-card .thumbnail.portrait {
|
|
||||||
padding-bottom: 150%;
|
|
||||||
}
|
}
|
||||||
ha-card .image {
|
ha-card .image {
|
||||||
border-radius: 3px 3px 0 0;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.folder {
|
.folder {
|
||||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: var(--ha-font-size-m);
|
||||||
padding-top: 16px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
margin-bottom: 16px;
|
|
||||||
padding-left: 16px;
|
|
||||||
padding-right: 4px;
|
|
||||||
padding-inline-start: 16px;
|
|
||||||
padding-inline-end: 4px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
line-height: 1.4;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.image {
|
.image {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -290,13 +311,15 @@ export class HaMediaSelector extends LitElement {
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
.centered-image {
|
.centered-image {
|
||||||
margin: 0 8px;
|
margin: 4px;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
}
|
}
|
||||||
.icon-holder {
|
.icon-holder {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,9 @@ export class HaNumberSelector extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public localizeValue?: (key: string) => string;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = true;
|
@property({ type: Boolean }) public required = true;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@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`
|
return html`
|
||||||
${this.label && !isBox
|
${this.label && !isBox
|
||||||
? html`${this.label}${this.required ? "*" : ""}`
|
? html`${this.label}${this.required ? "*" : ""}`
|
||||||
@ -97,7 +108,7 @@ export class HaNumberSelector extends LitElement {
|
|||||||
.helper=${isBox ? this.helper : undefined}
|
.helper=${isBox ? this.helper : undefined}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.suffix=${this.selector.number?.unit_of_measurement}
|
.suffix=${unit}
|
||||||
type="number"
|
type="number"
|
||||||
autoValidate
|
autoValidate
|
||||||
?no-spinner=${!isBox}
|
?no-spinner=${!isBox}
|
||||||
|
@ -122,11 +122,7 @@ export class HaObjectSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.selector.object) {
|
if (this.selector.object?.fields) {
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.selector.object.fields) {
|
|
||||||
if (this.selector.object.multiple) {
|
if (this.selector.object.multiple) {
|
||||||
const items = ensureArray(this.value ?? []);
|
const items = ensureArray(this.value ?? []);
|
||||||
return html`
|
return html`
|
||||||
|
@ -276,6 +276,16 @@ export class HaServiceControl extends LitElement {
|
|||||||
|
|
||||||
private _getTargetedEntities = memoizeOne((target, value) => {
|
private _getTargetedEntities = memoizeOne((target, value) => {
|
||||||
const targetSelector = target ? { target } : { target: {} };
|
const targetSelector = target ? { target } : { target: {} };
|
||||||
|
if (
|
||||||
|
hasTemplate(value?.target) ||
|
||||||
|
hasTemplate(value?.data?.entity_id) ||
|
||||||
|
hasTemplate(value?.data?.device_id) ||
|
||||||
|
hasTemplate(value?.data?.area_id) ||
|
||||||
|
hasTemplate(value?.data?.floor_id) ||
|
||||||
|
hasTemplate(value?.data?.label_id)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const targetEntities =
|
const targetEntities =
|
||||||
ensureArray(
|
ensureArray(
|
||||||
value?.target?.entity_id || value?.data?.entity_id
|
value?.target?.entity_id || value?.data?.entity_id
|
||||||
@ -349,8 +359,11 @@ export class HaServiceControl extends LitElement {
|
|||||||
|
|
||||||
private _filterField(
|
private _filterField(
|
||||||
filter: ExtHassService["fields"][number]["filter"],
|
filter: ExtHassService["fields"][number]["filter"],
|
||||||
targetEntities: string[]
|
targetEntities: string[] | null
|
||||||
) {
|
) {
|
||||||
|
if (targetEntities === null) {
|
||||||
|
return true; // Target is a template, show all fields
|
||||||
|
}
|
||||||
if (!targetEntities.length) {
|
if (!targetEntities.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -386,8 +399,21 @@ export class HaServiceControl extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _targetSelector = memoizeOne(
|
private _targetSelector = memoizeOne(
|
||||||
(targetSelector: TargetSelector | null | undefined) =>
|
(targetSelector: TargetSelector | null | undefined, value) => {
|
||||||
targetSelector ? { target: { ...targetSelector } } : { target: {} }
|
if (!value || (typeof value === "object" && !Object.keys(value).length)) {
|
||||||
|
delete this._stickySelector.target;
|
||||||
|
} else if (hasTemplate(value)) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
this._stickySelector.target = { template: null };
|
||||||
|
} else {
|
||||||
|
this._stickySelector.target = { object: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
this._stickySelector.target ??
|
||||||
|
(targetSelector ? { target: { ...targetSelector } } : { target: {} })
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -482,7 +508,8 @@ export class HaServiceControl extends LitElement {
|
|||||||
><ha-selector
|
><ha-selector
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.selector=${this._targetSelector(
|
.selector=${this._targetSelector(
|
||||||
serviceData.target as TargetSelector
|
serviceData.target as TargetSelector,
|
||||||
|
this._value?.target
|
||||||
)}
|
)}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@value-changed=${this._targetChanged}
|
@value-changed=${this._targetChanged}
|
||||||
@ -575,7 +602,7 @@ export class HaServiceControl extends LitElement {
|
|||||||
|
|
||||||
private _hasFilteredFields(
|
private _hasFilteredFields(
|
||||||
dataFields: ExtHassService["fields"],
|
dataFields: ExtHassService["fields"],
|
||||||
targetEntities: string[]
|
targetEntities: string[] | null
|
||||||
) {
|
) {
|
||||||
return dataFields.some(
|
return dataFields.some(
|
||||||
(dataField) =>
|
(dataField) =>
|
||||||
@ -588,7 +615,7 @@ export class HaServiceControl extends LitElement {
|
|||||||
hasOptional: boolean,
|
hasOptional: boolean,
|
||||||
domain: string | undefined,
|
domain: string | undefined,
|
||||||
serviceName: string | undefined,
|
serviceName: string | undefined,
|
||||||
targetEntities: string[]
|
targetEntities: string[] | null
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
dataField.filter &&
|
dataField.filter &&
|
||||||
@ -822,6 +849,10 @@ export class HaServiceControl extends LitElement {
|
|||||||
|
|
||||||
private _targetChanged(ev: CustomEvent) {
|
private _targetChanged(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
if (ev.detail.isValid === false) {
|
||||||
|
// Don't clear an object selector that returns invalid YAML
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
if (this._value?.target === newValue) {
|
if (this._value?.target === newValue) {
|
||||||
return;
|
return;
|
||||||
|
@ -89,6 +89,7 @@ export class HaSettingsRow extends LitElement {
|
|||||||
display: var(--settings-row-content-display, flex);
|
display: var(--settings-row-content-display, flex);
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
}
|
}
|
||||||
.content ::slotted(*) {
|
.content ::slotted(*) {
|
||||||
|
@ -44,6 +44,9 @@ export class HaYamlEditor extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: "read-only", type: Boolean }) public readOnly = false;
|
@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({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@property({ attribute: "copy-clipboard", type: Boolean })
|
@property({ attribute: "copy-clipboard", type: Boolean })
|
||||||
@ -110,6 +113,7 @@ export class HaYamlEditor extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._yaml}
|
.value=${this._yaml}
|
||||||
.readOnly=${this.readOnly}
|
.readOnly=${this.readOnly}
|
||||||
|
.disableFullscreen=${this.disableFullscreen}
|
||||||
mode="yaml"
|
mode="yaml"
|
||||||
autocomplete-entities
|
autocomplete-entities
|
||||||
autocomplete-icons
|
autocomplete-icons
|
||||||
|
@ -1114,12 +1114,16 @@ export const formatConsumptionShort = (
|
|||||||
if (!consumption) {
|
if (!consumption) {
|
||||||
return `0 ${unit}`;
|
return `0 ${unit}`;
|
||||||
}
|
}
|
||||||
const units = ["kWh", "MWh", "GWh", "TWh"];
|
const units = ["Wh", "kWh", "MWh", "GWh", "TWh"];
|
||||||
let pickedUnit = unit;
|
let pickedUnit = unit;
|
||||||
let val = consumption;
|
let val = consumption;
|
||||||
let unitIndex = units.findIndex((u) => u === unit);
|
let unitIndex = units.findIndex((u) => u === unit);
|
||||||
if (unitIndex >= 0) {
|
if (unitIndex >= 0) {
|
||||||
while (val >= 1000 && unitIndex < units.length - 1) {
|
while (Math.abs(val) < 1 && unitIndex > 0) {
|
||||||
|
val *= 1000;
|
||||||
|
unitIndex--;
|
||||||
|
}
|
||||||
|
while (Math.abs(val) >= 1000 && unitIndex < units.length - 1) {
|
||||||
val /= 1000;
|
val /= 1000;
|
||||||
unitIndex++;
|
unitIndex++;
|
||||||
}
|
}
|
||||||
@ -1127,7 +1131,8 @@ export const formatConsumptionShort = (
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
formatNumber(val, hass.locale, {
|
formatNumber(val, hass.locale, {
|
||||||
maximumFractionDigits: val < 10 ? 2 : val < 100 ? 1 : 0,
|
maximumFractionDigits:
|
||||||
|
Math.abs(val) < 10 ? 2 : Math.abs(val) < 100 ? 1 : 0,
|
||||||
}) +
|
}) +
|
||||||
" " +
|
" " +
|
||||||
pickedUnit
|
pickedUnit
|
||||||
|
@ -504,14 +504,25 @@ export const serviceSectionIcon = async (
|
|||||||
export const domainIcon = async (
|
export const domainIcon = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
domain: string,
|
domain: string,
|
||||||
deviceClass?: string
|
deviceClass?: string,
|
||||||
|
state?: string
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
const entityComponentIcons = await getComponentIcons(hass, domain);
|
const entityComponentIcons = await getComponentIcons(hass, domain);
|
||||||
if (entityComponentIcons) {
|
if (entityComponentIcons) {
|
||||||
const translations =
|
const translations =
|
||||||
(deviceClass && entityComponentIcons[deviceClass]) ||
|
(deviceClass && entityComponentIcons[deviceClass]) ||
|
||||||
entityComponentIcons._;
|
entityComponentIcons._;
|
||||||
return translations?.default;
|
// First check for exact state match
|
||||||
|
if (state && translations.state?.[state]) {
|
||||||
|
return translations.state[state];
|
||||||
|
}
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
// Fallback to default icon
|
||||||
|
return translations.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { mdiContentSave, mdiMedal, mdiTrophy } from "@mdi/js";
|
import { mdiContentSave, mdiMedal, mdiTrophy } from "@mdi/js";
|
||||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
|
||||||
import type { LocalizeKeys } from "../common/translations/localize";
|
import type { LocalizeKeys } from "../common/translations/localize";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,11 +25,6 @@ export const QUALITY_SCALE_MAP: Record<
|
|||||||
translationKey:
|
translationKey:
|
||||||
"ui.panel.config.integrations.config_entry.platinum_quality",
|
"ui.panel.config.integrations.config_entry.platinum_quality",
|
||||||
},
|
},
|
||||||
internal: {
|
|
||||||
icon: mdiHomeAssistant,
|
|
||||||
translationKey:
|
|
||||||
"ui.panel.config.integrations.config_entry.internal_integration",
|
|
||||||
},
|
|
||||||
legacy: {
|
legacy: {
|
||||||
icon: mdiContentSave,
|
icon: mdiContentSave,
|
||||||
translationKey:
|
translationKey:
|
||||||
|
@ -114,9 +114,13 @@ const getLogbookDataFromServer = (
|
|||||||
|
|
||||||
export const subscribeLogbook = (
|
export const subscribeLogbook = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
callbackFunction: (message: LogbookStreamMessage) => void,
|
callbackFunction: (
|
||||||
|
message: LogbookStreamMessage,
|
||||||
|
subscriptionId: number
|
||||||
|
) => void,
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string,
|
endDate: string,
|
||||||
|
subscriptionId: number,
|
||||||
entityIds?: string[],
|
entityIds?: string[],
|
||||||
deviceIds?: string[]
|
deviceIds?: string[]
|
||||||
): Promise<UnsubscribeFunc> => {
|
): Promise<UnsubscribeFunc> => {
|
||||||
@ -140,7 +144,7 @@ export const subscribeLogbook = (
|
|||||||
params.device_ids = deviceIds;
|
params.device_ids = deviceIds;
|
||||||
}
|
}
|
||||||
return hass.connection.subscribeMessage<LogbookStreamMessage>(
|
return hass.connection.subscribeMessage<LogbookStreamMessage>(
|
||||||
(message) => callbackFunction(message),
|
(message) => callbackFunction(message, subscriptionId),
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
literal,
|
literal,
|
||||||
is,
|
is,
|
||||||
boolean,
|
boolean,
|
||||||
|
refine,
|
||||||
} from "superstruct";
|
} from "superstruct";
|
||||||
import { arrayLiteralIncludes } from "../common/array/literal-includes";
|
import { arrayLiteralIncludes } from "../common/array/literal-includes";
|
||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
@ -49,13 +50,18 @@ export const targetStruct = object({
|
|||||||
label_id: optional(union([string(), array(string())])),
|
label_id: optional(union([string(), array(string())])),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const serviceActionStruct: Describe<ServiceAction> = assign(
|
export const serviceActionStruct: Describe<ServiceActionWithTemplate> = assign(
|
||||||
baseActionStruct,
|
baseActionStruct,
|
||||||
object({
|
object({
|
||||||
action: optional(string()),
|
action: optional(string()),
|
||||||
service_template: optional(string()),
|
service_template: optional(string()),
|
||||||
entity_id: optional(string()),
|
entity_id: optional(string()),
|
||||||
target: optional(targetStruct),
|
target: optional(
|
||||||
|
union([
|
||||||
|
targetStruct,
|
||||||
|
refine(string(), "has_template", (val) => hasTemplate(val)),
|
||||||
|
])
|
||||||
|
),
|
||||||
data: optional(object()),
|
data: optional(object()),
|
||||||
response_variable: optional(string()),
|
response_variable: optional(string()),
|
||||||
metadata: optional(object()),
|
metadata: optional(object()),
|
||||||
@ -132,6 +138,12 @@ export interface ServiceAction extends BaseAction {
|
|||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServiceActionWithTemplate = ServiceAction & {
|
||||||
|
target?: HassServiceTarget | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { ServiceActionWithTemplate };
|
||||||
|
|
||||||
export interface DeviceAction extends BaseAction {
|
export interface DeviceAction extends BaseAction {
|
||||||
type: string;
|
type: string;
|
||||||
device_id: string;
|
device_id: string;
|
||||||
|
@ -333,6 +333,7 @@ export interface NumberSelector {
|
|||||||
mode?: "box" | "slider";
|
mode?: "box" | "slider";
|
||||||
unit_of_measurement?: string;
|
unit_of_measurement?: string;
|
||||||
slider_ticks?: boolean;
|
slider_ticks?: boolean;
|
||||||
|
translation_key?: string;
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,7 +286,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
scrimClickAction
|
scrimClickAction
|
||||||
escapeKeyAction
|
escapeKeyAction
|
||||||
hideActions
|
hideActions
|
||||||
.heading=${dialogTitle}
|
.heading=${dialogTitle || true}
|
||||||
>
|
>
|
||||||
<ha-dialog-header slot="heading">
|
<ha-dialog-header slot="heading">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
|
@ -35,10 +35,16 @@ export const showConfigFlowDialog = (
|
|||||||
return step;
|
return step;
|
||||||
},
|
},
|
||||||
fetchFlow: async (hass, flowId) => {
|
fetchFlow: async (hass, flowId) => {
|
||||||
const step = await fetchConfigFlow(hass, flowId);
|
const [step] = await Promise.all([
|
||||||
await hass.loadFragmentTranslation("config");
|
fetchConfigFlow(hass, flowId),
|
||||||
await hass.loadBackendTranslation("config", step.handler);
|
hass.loadFragmentTranslation("config"),
|
||||||
await hass.loadBackendTranslation("selector", step.handler);
|
]);
|
||||||
|
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;
|
return step;
|
||||||
},
|
},
|
||||||
handleFlowStep: handleConfigFlowStep,
|
handleFlowStep: handleConfigFlowStep,
|
||||||
|
@ -2,12 +2,19 @@ import { html, LitElement } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
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 { PlayMediaAction } from "../../../../../data/script";
|
||||||
import type { MediaSelectorValue } from "../../../../../data/selector";
|
import type {
|
||||||
|
MediaSelectorValue,
|
||||||
|
Selector,
|
||||||
|
} from "../../../../../data/selector";
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
import type { ActionElement } from "../ha-automation-action-row";
|
import type { ActionElement } from "../ha-automation-action-row";
|
||||||
|
|
||||||
|
const MEDIA_SELECTOR_SCHEMA: Selector = {
|
||||||
|
media: {},
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("ha-automation-action-play_media")
|
@customElement("ha-automation-action-play_media")
|
||||||
export class HaPlayMediaAction extends LitElement implements ActionElement {
|
export class HaPlayMediaAction extends LitElement implements ActionElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -38,12 +45,13 @@ export class HaPlayMediaAction extends LitElement implements ActionElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-selector-media
|
<ha-selector
|
||||||
|
.selector=${MEDIA_SELECTOR_SCHEMA}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.value=${this._getSelectorValue(this.action)}
|
.value=${this._getSelectorValue(this.action)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></ha-selector-media>
|
></ha-selector>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
|||||||
if (
|
if (
|
||||||
this.action &&
|
this.action &&
|
||||||
Object.entries(this.action).some(
|
Object.entries(this.action).some(
|
||||||
([key, val]) => key !== "data" && hasTemplate(val)
|
([key, val]) => !["data", "target"].includes(key) && hasTemplate(val)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
fireEvent(
|
fireEvent(
|
||||||
|
@ -502,6 +502,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
.readOnly=${this._readOnly}
|
.readOnly=${this._readOnly}
|
||||||
@value-changed=${this._yamlChanged}
|
@value-changed=${this._yamlChanged}
|
||||||
.showErrors=${false}
|
.showErrors=${false}
|
||||||
|
disable-fullscreen
|
||||||
></ha-yaml-editor>`
|
></ha-yaml-editor>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
|
@ -566,6 +566,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
text: html`
|
text: html`
|
||||||
<ha-yaml-editor
|
<ha-yaml-editor
|
||||||
read-only
|
read-only
|
||||||
|
disable-fullscreen
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.defaultValue=${this._triggered}
|
.defaultValue=${this._triggered}
|
||||||
></ha-yaml-editor>
|
></ha-yaml-editor>
|
||||||
|
@ -173,6 +173,7 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
|||||||
.content=${value?.description}
|
.content=${value?.description}
|
||||||
></ha-markdown>
|
></ha-markdown>
|
||||||
${html`<ha-selector
|
${html`<ha-selector
|
||||||
|
narrow
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.selector=${selector}
|
.selector=${selector}
|
||||||
.key=${key}
|
.key=${key}
|
||||||
|
@ -54,6 +54,9 @@ class HaConfigNavigation extends LitElement {
|
|||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
return html`
|
return html`
|
||||||
|
<div class="visually-hidden" role="heading" aria-level="2">
|
||||||
|
${this.hass.localize("panel.config")}
|
||||||
|
</div>
|
||||||
<ha-navigation-list
|
<ha-navigation-list
|
||||||
has-secondary
|
has-secondary
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -68,6 +71,17 @@ class HaConfigNavigation extends LitElement {
|
|||||||
ha-navigation-list {
|
ha-navigation-list {
|
||||||
--navigation-list-item-title-font-size: var(--ha-font-size-l);
|
--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;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
|||||||
const updates = this.updateEntities;
|
const updates = this.updateEntities;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="title">
|
<div class="title" role="heading" aria-level="2">
|
||||||
${this.hass.localize("ui.panel.config.updates.title", {
|
${this.hass.localize("ui.panel.config.updates.title", {
|
||||||
count: this.total || this.updateEntities.length,
|
count: this.total || this.updateEntities.length,
|
||||||
})}
|
})}
|
||||||
@ -115,7 +115,7 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
|||||||
></ha-spinner>`
|
></ha-spinner>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span slot="headline"
|
||||||
>${deviceEntry
|
>${deviceEntry
|
||||||
? computeDeviceNameDisplay(deviceEntry, this.hass)
|
? computeDeviceNameDisplay(deviceEntry, this.hass)
|
||||||
: entity.attributes.friendly_name}</span
|
: entity.attributes.friendly_name}</span
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
mdiPlusCircle,
|
mdiPlusCircle,
|
||||||
mdiRestore,
|
mdiRestore,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
@ -273,22 +273,24 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
findBatteryChargingEntity(this.hass, entities)
|
findBatteryChargingEntity(this.hass, entities)
|
||||||
);
|
);
|
||||||
|
|
||||||
public willUpdate(changedProps) {
|
public willUpdate(changedProps: PropertyValues<this>) {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
if (changedProps.has("deviceId") || changedProps.has("entries")) {
|
if (changedProps.has("deviceId")) {
|
||||||
this._deviceActions = [];
|
this._deviceActions = [];
|
||||||
this._deviceAlerts = [];
|
this._deviceAlerts = [];
|
||||||
this._deleteButtons = [];
|
this._deleteButtons = [];
|
||||||
this._diagnosticDownloadLinks = [];
|
this._diagnosticDownloadLinks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("deviceId") || changedProps.has("entries")) {
|
||||||
this._fetchData();
|
this._fetchData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
loadDeviceRegistryDetailDialog();
|
loadDeviceRegistryDetailDialog();
|
||||||
this._fetchData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps) {
|
protected updated(changedProps) {
|
||||||
@ -989,6 +991,7 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getDeleteActions() {
|
private _getDeleteActions() {
|
||||||
|
const deviceId = this.deviceId;
|
||||||
const device = this.hass.devices[this.deviceId];
|
const device = this.hass.devices[this.deviceId];
|
||||||
|
|
||||||
if (!device) {
|
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) {
|
if (buttons.length > 0) {
|
||||||
this._deleteButtons = buttons;
|
this._deleteButtons = buttons;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getDeviceActions() {
|
private async _getDeviceActions() {
|
||||||
|
const deviceId = this.deviceId;
|
||||||
const device = this.hass.devices[this.deviceId];
|
const device = this.hass.devices[this.deviceId];
|
||||||
|
|
||||||
if (!device) {
|
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
|
// 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) => {
|
matter.getMatterDeviceActions(this, this.hass, device).then((actions) => {
|
||||||
|
if (this.deviceId !== deviceId) {
|
||||||
|
// abort if the device has changed
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._deviceActions = [...actions, ...(this._deviceActions || [])];
|
this._deviceActions = [...actions, ...(this._deviceActions || [])];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.deviceId !== deviceId) {
|
||||||
|
// abort if the device has changed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._deviceActions = deviceActions;
|
this._deviceActions = deviceActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getDeviceAlerts() {
|
private async _getDeviceAlerts() {
|
||||||
|
const deviceId = this.deviceId;
|
||||||
|
|
||||||
const device = this.hass.devices[this.deviceId];
|
const device = this.hass.devices[this.deviceId];
|
||||||
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
@ -1188,6 +1208,11 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
deviceAlerts.push(...alerts);
|
deviceAlerts.push(...alerts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.deviceId !== deviceId) {
|
||||||
|
// abort if the device has changed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._deviceAlerts = deviceAlerts;
|
this._deviceAlerts = deviceAlerts;
|
||||||
if (deviceAlerts.length) {
|
if (deviceAlerts.length) {
|
||||||
this._deviceAlertsActionsTimeout = window.setTimeout(() => {
|
this._deviceAlertsActionsTimeout = window.setTimeout(() => {
|
||||||
@ -1317,9 +1342,13 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
(await showConfirmationDialog(this, {
|
(await showConfirmationDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
"ui.panel.config.devices.confirm_disable_config_entry",
|
"ui.panel.config.devices.confirm_disable_config_entry_title"
|
||||||
{ entry_name: 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"),
|
confirmText: this.hass.localize("ui.common.yes"),
|
||||||
dismissText: this.hass.localize("ui.common.no"),
|
dismissText: this.hass.localize("ui.common.no"),
|
||||||
}))
|
}))
|
||||||
|
@ -1099,10 +1099,10 @@ ${
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
|
this._setFiltersFromUrl();
|
||||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||||
this._entitySources = sources;
|
this._entitySources = sources;
|
||||||
});
|
});
|
||||||
this._setFiltersFromUrl();
|
|
||||||
if (Object.keys(this._filters).length) {
|
if (Object.keys(this._filters).length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1116,7 +1116,7 @@ ${
|
|||||||
const configEntry = this._searchParms.get("config_entry");
|
const configEntry = this._searchParms.get("config_entry");
|
||||||
const subEntry = this._searchParms.get("sub_entry");
|
const subEntry = this._searchParms.get("sub_entry");
|
||||||
const device = this._searchParms.get("device");
|
const device = this._searchParms.get("device");
|
||||||
const label = this._searchParms.has("label");
|
const label = this._searchParms.get("label");
|
||||||
|
|
||||||
if (!domain && !configEntry && !label && !device) {
|
if (!domain && !configEntry && !label && !device) {
|
||||||
return;
|
return;
|
||||||
@ -1128,21 +1128,10 @@ ${
|
|||||||
"ha-filter-states": [],
|
"ha-filter-states": [],
|
||||||
"ha-filter-integrations": domain ? [domain] : [],
|
"ha-filter-integrations": domain ? [domain] : [],
|
||||||
"ha-filter-devices": device ? [device] : [],
|
"ha-filter-devices": device ? [device] : [],
|
||||||
|
"ha-filter-labels": label ? [label] : [],
|
||||||
config_entry: configEntry ? [configEntry] : [],
|
config_entry: configEntry ? [configEntry] : [],
|
||||||
sub_entry: subEntry ? [subEntry] : [],
|
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() {
|
private _clearFilter() {
|
||||||
@ -1152,6 +1141,11 @@ ${
|
|||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues): void {
|
public willUpdate(changedProps: PropertyValues): void {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
this._setFiltersFromUrl();
|
||||||
|
}
|
||||||
|
|
||||||
const oldHass = changedProps.get("hass");
|
const oldHass = changedProps.get("hass");
|
||||||
let changed = false;
|
let changed = false;
|
||||||
if (!this.hass || !this._entities) {
|
if (!this.hass || !this._entities) {
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
mdiCogOutline,
|
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
|
mdiDevices,
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
|
mdiPencil,
|
||||||
|
mdiShapeOutline,
|
||||||
mdiStopCircleOutline,
|
mdiStopCircleOutline,
|
||||||
|
mdiTransitConnectionVariant,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||||
import { getDeviceContext } from "../../../common/entity/context/get_device_context";
|
import { getDeviceContext } from "../../../common/entity/context/get_device_context";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import type { ConfigEntry } from "../../../data/config_entries";
|
import {
|
||||||
|
disableConfigEntry,
|
||||||
|
type ConfigEntry,
|
||||||
|
type DisableConfigEntryResult,
|
||||||
|
} from "../../../data/config_entries";
|
||||||
import {
|
import {
|
||||||
removeConfigEntryFromDevice,
|
removeConfigEntryFromDevice,
|
||||||
updateDeviceRegistryEntry,
|
updateDeviceRegistryEntry,
|
||||||
@ -49,99 +58,118 @@ class HaConfigEntryDeviceRow extends LitElement {
|
|||||||
area ? area.name : undefined,
|
area ? area.name : undefined,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
return html`<ha-md-list-item>
|
return html`<ha-md-list-item
|
||||||
|
type="button"
|
||||||
|
@click=${this._handleNavigateToDevice}
|
||||||
|
class=${classMap({ disabled: Boolean(device.disabled_by) })}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${device.entry_type === "service"
|
||||||
|
? mdiTransitConnectionVariant
|
||||||
|
: mdiDevices}
|
||||||
|
slot="start"
|
||||||
|
></ha-svg-icon>
|
||||||
<div slot="headline">${computeDeviceNameDisplay(device, this.hass)}</div>
|
<div slot="headline">${computeDeviceNameDisplay(device, this.hass)}</div>
|
||||||
<span slot="supporting-text"
|
<span slot="supporting-text"
|
||||||
>${supportingText.join(" • ")}
|
>${supportingText.join(" • ")}
|
||||||
${supportingText.length && entities.length ? " • " : nothing}
|
${supportingText.length && entities.length ? " • " : nothing}
|
||||||
${
|
${entities.length
|
||||||
entities.length
|
? this.hass.localize(
|
||||||
? html`<a
|
"ui.panel.config.integrations.config_entry.entities",
|
||||||
href=${`/config/entities/?historyBack=1&device=${device.id}`}
|
{ count: entities.length }
|
||||||
>${this.hass.localize(
|
)
|
||||||
"ui.panel.config.integrations.config_entry.entities",
|
: nothing}</span
|
||||||
{ count: entities.length }
|
|
||||||
)}</a
|
|
||||||
>`
|
|
||||||
: nothing
|
|
||||||
}</span
|
|
||||||
>
|
>
|
||||||
<ha-icon-button-next slot="end" @click=${this._handleNavigateToDevice}
|
${!this.narrow
|
||||||
|
? html`<ha-icon-next slot="end"> </ha-icon-next>`
|
||||||
|
: nothing}
|
||||||
|
<div class="vertical-divider" slot="end" @click=${stopPropagation}></div>
|
||||||
|
${!this.narrow
|
||||||
|
? html`<ha-icon-button
|
||||||
|
slot="end"
|
||||||
|
@click=${this._handleEditDevice}
|
||||||
|
.path=${mdiPencil}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.device.edit"
|
||||||
|
)}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
|
<ha-md-button-menu
|
||||||
|
positioning="popover"
|
||||||
|
slot="end"
|
||||||
|
@click=${stopPropagation}
|
||||||
>
|
>
|
||||||
</ha-icon-button-next>
|
|
||||||
</ha-icon-button>
|
|
||||||
<div class="vertical-divider" slot="end"></div>
|
|
||||||
${
|
|
||||||
!this.narrow
|
|
||||||
? html`<ha-icon-button
|
|
||||||
slot="end"
|
|
||||||
@click=${this._handleConfigureDevice}
|
|
||||||
.path=${mdiCogOutline}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.device.configure"
|
|
||||||
)}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing
|
|
||||||
}
|
|
||||||
</ha-icon-button>
|
|
||||||
<ha-md-button-menu positioning="popover" slot="end">
|
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
.path=${mdiDotsVertical}
|
.path=${mdiDotsVertical}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
${
|
${this.narrow
|
||||||
this.narrow
|
? html`<ha-md-menu-item @click=${this._handleEditDevice}>
|
||||||
? html`<ha-md-menu-item @click=${this._handleConfigureDevice}>
|
<ha-svg-icon .path=${mdiPencil} slot="start"></ha-svg-icon>
|
||||||
<ha-svg-icon .path=${mdiCogOutline} slot="start"></ha-svg-icon>
|
${this.hass.localize(
|
||||||
${this.hass.localize(
|
"ui.panel.config.integrations.config_entry.device.edit"
|
||||||
"ui.panel.config.integrations.config_entry.device.configure"
|
)}
|
||||||
)}
|
</ha-md-menu-item>`
|
||||||
</ha-md-menu-item>`
|
: nothing}
|
||||||
: nothing
|
${entities.length
|
||||||
}
|
? html`
|
||||||
<ha-md-menu-item class=${device.disabled_by !== "user" ? "warning" : ""} @click=${this._handleDisableDevice} .disabled=${device.disabled_by !== "user" && device.disabled_by}>
|
<ha-md-menu-item
|
||||||
<ha-svg-icon .path=${mdiStopCircleOutline} slot="start"></ha-svg-icon>
|
href=${`/config/entities/?historyBack=1&device=${device.id}`}
|
||||||
|
|
||||||
${
|
|
||||||
device.disabled_by && device.disabled_by !== "user"
|
|
||||||
? this.hass.localize(
|
|
||||||
"ui.dialogs.device-registry-detail.enabled_cause",
|
|
||||||
{
|
|
||||||
type: this.hass.localize(
|
|
||||||
`ui.dialogs.device-registry-detail.type.${
|
|
||||||
device.entry_type || "device"
|
|
||||||
}`
|
|
||||||
),
|
|
||||||
cause: this.hass.localize(
|
|
||||||
`config_entry.disabled_by.${device.disabled_by}`
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
: device.disabled_by
|
|
||||||
? this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.device.enable"
|
|
||||||
)
|
|
||||||
: this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.device.disable"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</ha-md-menu-item>
|
|
||||||
${
|
|
||||||
this.entry.supports_remove_device
|
|
||||||
? html` <ha-md-menu-item
|
|
||||||
class="warning"
|
|
||||||
@click=${this._handleDeleteDevice}
|
|
||||||
>
|
>
|
||||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
<ha-svg-icon
|
||||||
|
.path=${mdiShapeOutline}
|
||||||
|
slot="start"
|
||||||
|
></ha-svg-icon>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.integrations.config_entry.device.delete"
|
`ui.panel.config.integrations.config_entry.entities`,
|
||||||
|
{ count: entities.length }
|
||||||
)}
|
)}
|
||||||
</ha-md-menu-item>`
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
: nothing
|
</ha-md-menu-item>
|
||||||
}
|
`
|
||||||
|
: nothing}
|
||||||
|
<ha-md-menu-item
|
||||||
|
class=${device.disabled_by !== "user" ? "warning" : ""}
|
||||||
|
@click=${this._handleDisableDevice}
|
||||||
|
.disabled=${device.disabled_by !== "user" && device.disabled_by}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiStopCircleOutline} slot="start"></ha-svg-icon>
|
||||||
|
|
||||||
|
${device.disabled_by && device.disabled_by !== "user"
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.dialogs.device-registry-detail.enabled_cause",
|
||||||
|
{
|
||||||
|
type: this.hass.localize(
|
||||||
|
`ui.dialogs.device-registry-detail.type.${
|
||||||
|
device.entry_type || "device"
|
||||||
|
}`
|
||||||
|
),
|
||||||
|
cause: this.hass.localize(
|
||||||
|
`config_entry.disabled_by.${device.disabled_by}`
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: device.disabled_by
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.device.enable"
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.device.disable"
|
||||||
|
)}
|
||||||
|
</ha-md-menu-item>
|
||||||
|
${this.entry.supports_remove_device
|
||||||
|
? html`<ha-md-menu-item
|
||||||
|
class="warning"
|
||||||
|
@click=${this._handleDeleteDevice}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.device.delete"
|
||||||
|
)}
|
||||||
|
</ha-md-menu-item>`
|
||||||
|
: nothing}
|
||||||
</ha-md-button-menu>
|
</ha-md-button-menu>
|
||||||
</ha-md-list-item> `;
|
</ha-md-list-item> `;
|
||||||
}
|
}
|
||||||
@ -149,7 +177,8 @@ class HaConfigEntryDeviceRow extends LitElement {
|
|||||||
private _getEntities = (): EntityRegistryEntry[] =>
|
private _getEntities = (): EntityRegistryEntry[] =>
|
||||||
this.entities?.filter((entity) => entity.device_id === this.device.id);
|
this.entities?.filter((entity) => entity.device_id === this.device.id);
|
||||||
|
|
||||||
private _handleConfigureDevice() {
|
private _handleEditDevice(ev: MouseEvent) {
|
||||||
|
ev.stopPropagation(); // Prevent triggering the click handler on the list item
|
||||||
showDeviceRegistryDetailDialog(this, {
|
showDeviceRegistryDetailDialog(this, {
|
||||||
device: this.device,
|
device: this.device,
|
||||||
updateEntry: async (updates) => {
|
updateEntry: async (updates) => {
|
||||||
@ -159,8 +188,78 @@ class HaConfigEntryDeviceRow extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _handleDisableDevice() {
|
private async _handleDisableDevice() {
|
||||||
|
const disable = this.device.disabled_by === null;
|
||||||
|
|
||||||
|
if (disable) {
|
||||||
|
if (
|
||||||
|
!Object.values(this.hass.devices).some(
|
||||||
|
(dvc) =>
|
||||||
|
dvc.id !== this.device.id &&
|
||||||
|
dvc.config_entries.includes(this.entry.entry_id)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const config_entry = this.entry;
|
||||||
|
if (
|
||||||
|
config_entry &&
|
||||||
|
!config_entry.disabled_by &&
|
||||||
|
(await showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"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"),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
let result: DisableConfigEntryResult;
|
||||||
|
try {
|
||||||
|
result = await disableConfigEntry(this.hass, this.entry.entry_id);
|
||||||
|
} catch (err: any) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.disable_error"
|
||||||
|
),
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.require_restart) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disable) {
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.device.confirm_disable_title"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.device.confirm_disable_message",
|
||||||
|
{ name: computeDeviceNameDisplay(this.device, this.hass) }
|
||||||
|
),
|
||||||
|
destructive: true,
|
||||||
|
confirmText: this.hass.localize("ui.common.yes"),
|
||||||
|
dismissText: this.hass.localize("ui.common.no"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await updateDeviceRegistryEntry(this.hass, this.device.id, {
|
await updateDeviceRegistryEntry(this.hass, this.device.id, {
|
||||||
disabled_by: this.device.disabled_by === "user" ? null : "user",
|
disabled_by: disable ? "user" : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,6 +302,14 @@ class HaConfigEntryDeviceRow extends LitElement {
|
|||||||
}
|
}
|
||||||
ha-md-list-item {
|
ha-md-list-item {
|
||||||
--md-list-item-leading-space: 56px;
|
--md-list-item-leading-space: 56px;
|
||||||
|
--md-ripple-hover-color: transparent;
|
||||||
|
--md-ripple-pressed-color: transparent;
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
:host([narrow]) ha-md-list-item {
|
||||||
|
--md-list-item-leading-space: 16px;
|
||||||
}
|
}
|
||||||
.vertical-divider {
|
.vertical-divider {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
mdiAlertCircle,
|
mdiAlertCircle,
|
||||||
mdiChevronDown,
|
mdiChevronDown,
|
||||||
mdiChevronUp,
|
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
mdiDevices,
|
mdiDevices,
|
||||||
@ -58,6 +57,7 @@ import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entr
|
|||||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||||
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
|
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { documentationUrl } from "../../../util/documentation-url";
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
import { fileDownload } from "../../../util/file_download";
|
import { fileDownload } from "../../../util/file_download";
|
||||||
@ -69,7 +69,6 @@ import {
|
|||||||
import "./ha-config-entry-device-row";
|
import "./ha-config-entry-device-row";
|
||||||
import { renderConfigEntryError } from "./ha-config-integration-page";
|
import { renderConfigEntryError } from "./ha-config-integration-page";
|
||||||
import "./ha-config-sub-entry-row";
|
import "./ha-config-sub-entry-row";
|
||||||
import { haStyle } from "../../../resources/styles";
|
|
||||||
|
|
||||||
@customElement("ha-config-entry-row")
|
@customElement("ha-config-entry-row")
|
||||||
class HaConfigEntryRow extends LitElement {
|
class HaConfigEntryRow extends LitElement {
|
||||||
@ -155,7 +154,10 @@ class HaConfigEntryRow extends LitElement {
|
|||||||
statusLine.push(
|
statusLine.push(
|
||||||
html`<a
|
html`<a
|
||||||
href=${`/config/entities/?historyBack=1&config_entry=${item.entry_id}`}
|
href=${`/config/entities/?historyBack=1&config_entry=${item.entry_id}`}
|
||||||
>${entities.length} entities</a
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.entities",
|
||||||
|
{ count: entities.length }
|
||||||
|
)}</a
|
||||||
>`
|
>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -178,8 +180,8 @@ class HaConfigEntryRow extends LitElement {
|
|||||||
>
|
>
|
||||||
${subEntries.length || ownDevices.length
|
${subEntries.length || ownDevices.length
|
||||||
? html`<ha-icon-button
|
? html`<ha-icon-button
|
||||||
class="expand-button"
|
class="expand-button ${classMap({ expanded: this._expanded })}"
|
||||||
.path=${this._expanded ? mdiChevronDown : mdiChevronUp}
|
.path=${mdiChevronDown}
|
||||||
slot="start"
|
slot="start"
|
||||||
@click=${this._toggleExpand}
|
@click=${this._toggleExpand}
|
||||||
></ha-icon-button>`
|
></ha-icon-button>`
|
||||||
@ -405,47 +407,55 @@ class HaConfigEntryRow extends LitElement {
|
|||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
${this._expanded
|
${this._expanded
|
||||||
? subEntries.length
|
? subEntries.length
|
||||||
? html`<ha-md-list class="devices">
|
? html`${ownDevices.length
|
||||||
<ha-md-list-item @click=${this._toggleOwnDevices} type="button">
|
? html`<ha-md-list class="devices">
|
||||||
<ha-icon-button
|
<ha-md-list-item
|
||||||
class="expand-button"
|
@click=${this._toggleOwnDevices}
|
||||||
.path=${this._devicesExpanded
|
type="button"
|
||||||
? mdiChevronDown
|
class="toggle-devices-row ${classMap({
|
||||||
: mdiChevronUp}
|
expanded: this._devicesExpanded,
|
||||||
slot="start"
|
})}"
|
||||||
>
|
>
|
||||||
</ha-icon-button>
|
<ha-icon-button
|
||||||
${this.hass.localize(
|
class="expand-button ${classMap({
|
||||||
"ui.panel.config.integrations.config_entry.devices_without_subentry"
|
expanded: this._devicesExpanded,
|
||||||
)}
|
})}"
|
||||||
</ha-md-list-item>
|
.path=${mdiChevronDown}
|
||||||
${this._devicesExpanded
|
slot="start"
|
||||||
? ownDevices.map(
|
>
|
||||||
(device) =>
|
</ha-icon-button>
|
||||||
html`<ha-config-entry-device-row
|
${this.hass.localize(
|
||||||
.hass=${this.hass}
|
"ui.panel.config.integrations.config_entry.devices_without_subentry"
|
||||||
.narrow=${this.narrow}
|
)}
|
||||||
.entry=${item}
|
</ha-md-list-item>
|
||||||
.device=${device}
|
${this._devicesExpanded
|
||||||
.entities=${entities}
|
? ownDevices.map(
|
||||||
></ha-config-entry-device-row>`
|
(device) =>
|
||||||
)
|
html`<ha-config-entry-device-row
|
||||||
: nothing}
|
.hass=${this.hass}
|
||||||
</ha-md-list>
|
.narrow=${this.narrow}
|
||||||
${subEntries.map(
|
.entry=${item}
|
||||||
(subEntry) => html`
|
.device=${device}
|
||||||
<ha-config-sub-entry-row
|
.entities=${entities}
|
||||||
.hass=${this.hass}
|
></ha-config-entry-device-row>`
|
||||||
.narrow=${this.narrow}
|
)
|
||||||
.manifest=${this.manifest}
|
: nothing}
|
||||||
.diagnosticHandler=${this.diagnosticHandler}
|
</ha-md-list>`
|
||||||
.entities=${this.entities}
|
: nothing}
|
||||||
.entry=${item}
|
${subEntries.map(
|
||||||
.subEntry=${subEntry}
|
(subEntry) => html`
|
||||||
data-entry-id=${item.entry_id}
|
<ha-config-sub-entry-row
|
||||||
></ha-config-sub-entry-row>
|
.hass=${this.hass}
|
||||||
`
|
.narrow=${this.narrow}
|
||||||
)}`
|
.manifest=${this.manifest}
|
||||||
|
.diagnosticHandler=${this.diagnosticHandler}
|
||||||
|
.entities=${this.entities}
|
||||||
|
.entry=${item}
|
||||||
|
.subEntry=${subEntry}
|
||||||
|
data-entry-id=${item.entry_id}
|
||||||
|
></ha-config-sub-entry-row>
|
||||||
|
`
|
||||||
|
)}`
|
||||||
: html`
|
: html`
|
||||||
${ownDevices.map(
|
${ownDevices.map(
|
||||||
(device) =>
|
(device) =>
|
||||||
@ -734,12 +744,20 @@ class HaConfigEntryRow extends LitElement {
|
|||||||
css`
|
css`
|
||||||
.expand-button {
|
.expand-button {
|
||||||
margin: 0 -12px;
|
margin: 0 -12px;
|
||||||
|
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
.expand-button.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
ha-md-list {
|
ha-md-list {
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
border-radius: var(--ha-card-border-radius, 12px);
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
:host([narrow]) {
|
||||||
|
margin-left: -12px;
|
||||||
|
margin-right: -12px;
|
||||||
|
}
|
||||||
ha-md-list.devices {
|
ha-md-list.devices {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@ -750,6 +768,14 @@ class HaConfigEntryRow extends LitElement {
|
|||||||
var(--md-sys-color-on-surface-variant, #49454f)
|
var(--md-sys-color-on-surface-variant, #49454f)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
.toggle-devices-row {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
}
|
||||||
|
.toggle-devices-row.expanded {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -380,6 +380,14 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>${domainToName(this.hass.localize, this.domain)}</h1>
|
<h1>${domainToName(this.hass.localize, this.domain)}</h1>
|
||||||
<div class="sub">
|
<div class="sub">
|
||||||
|
${this._manifest?.version != null
|
||||||
|
? html`<span class="version"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.version",
|
||||||
|
{ version: this._manifest.version }
|
||||||
|
)}</span
|
||||||
|
>`
|
||||||
|
: nothing}
|
||||||
${this._manifest?.is_built_in === false
|
${this._manifest?.is_built_in === false
|
||||||
? html`<div
|
? html`<div
|
||||||
class=${`integration-info ${
|
class=${`integration-info ${
|
||||||
@ -403,14 +411,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
</a>
|
</a>
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this._logInfo?.level === LogSeverity.DEBUG
|
|
||||||
? html`<div class="integration-info">
|
|
||||||
<ha-svg-icon .path=${mdiBugPlay}></ha-svg-icon>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.debug_logging_enabled"
|
|
||||||
)}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
${this._manifest?.iot_class?.startsWith("cloud_")
|
${this._manifest?.iot_class?.startsWith("cloud_")
|
||||||
? html`<div class="integration-info">
|
? html`<div class="integration-info">
|
||||||
<ha-svg-icon .path=${mdiWeb}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiWeb}></ha-svg-icon>
|
||||||
@ -432,7 +432,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
)}
|
)}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this._manifest?.quality_scale &&
|
${this._manifest?.is_built_in &&
|
||||||
|
this._manifest.quality_scale &&
|
||||||
Object.keys(QUALITY_SCALE_MAP).includes(
|
Object.keys(QUALITY_SCALE_MAP).includes(
|
||||||
this._manifest.quality_scale
|
this._manifest.quality_scale
|
||||||
)
|
)
|
||||||
@ -539,6 +540,22 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${this._logInfo?.level === LogSeverity.DEBUG
|
||||||
|
? html`<div class="section">
|
||||||
|
<ha-alert alert-type="warning">
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiBugPlay}></ha-svg-icon>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.debug_logging_enabled"
|
||||||
|
)}
|
||||||
|
<ha-button
|
||||||
|
slot="action"
|
||||||
|
@click=${this._handleDisableDebugLogging}
|
||||||
|
>
|
||||||
|
${this.hass.localize("ui.common.disable")}
|
||||||
|
</ha-button>
|
||||||
|
</ha-alert>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
${discoveryFlows.length
|
${discoveryFlows.length
|
||||||
? html`
|
? html`
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@ -885,7 +902,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
@ -903,6 +920,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px 16px;
|
gap: 8px 16px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.card-content {
|
.card-content {
|
||||||
padding: 16px 0 8px;
|
padding: 16px 0 8px;
|
||||||
@ -926,9 +944,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
.version {
|
.version {
|
||||||
padding-top: 8px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
.overview .card-actions {
|
.overview .card-actions {
|
||||||
@ -946,6 +961,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.section {
|
.section {
|
||||||
@ -1000,9 +1016,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
ha-svg-icon.platinum-quality {
|
ha-svg-icon.platinum-quality {
|
||||||
color: #727272;
|
color: #727272;
|
||||||
}
|
}
|
||||||
ha-svg-icon.internal-quality {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
ha-svg-icon.legacy-quality {
|
ha-svg-icon.legacy-quality {
|
||||||
color: var(--mdc-theme-text-icon-on-background, rgba(0, 0, 0, 0.38));
|
color: var(--mdc-theme-text-icon-on-background, rgba(0, 0, 0, 0.38));
|
||||||
animation: unset;
|
animation: unset;
|
||||||
|
@ -406,11 +406,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
|||||||
${!this._showDisabled && this.narrow && disabledConfigEntries.length
|
${!this._showDisabled && this.narrow && disabledConfigEntries.length
|
||||||
? html`<span class="badge">${disabledConfigEntries.length}</span>`
|
? html`<span class="badge">${disabledConfigEntries.length}</span>`
|
||||||
: ""}
|
: ""}
|
||||||
<ha-button-menu
|
<ha-button-menu multi @action=${this._handleMenuAction}>
|
||||||
multi
|
|
||||||
@action=${this._handleMenuAction}
|
|
||||||
@click=${this._preventDefault}
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
mdiChevronDown,
|
mdiChevronDown,
|
||||||
mdiChevronUp,
|
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
mdiDevices,
|
mdiDevices,
|
||||||
@ -10,6 +9,7 @@ import {
|
|||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||||
import { deleteSubEntry } from "../../../data/config_entries";
|
import { deleteSubEntry } from "../../../data/config_entries";
|
||||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||||
@ -56,8 +56,8 @@ class HaConfigSubEntryRow extends LitElement {
|
|||||||
>
|
>
|
||||||
${devices.length || services.length
|
${devices.length || services.length
|
||||||
? html`<ha-icon-button
|
? html`<ha-icon-button
|
||||||
class="expand-button"
|
class="expand-button ${classMap({ expanded: this._expanded })}"
|
||||||
.path=${this._expanded ? mdiChevronDown : mdiChevronUp}
|
.path=${mdiChevronDown}
|
||||||
slot="start"
|
slot="start"
|
||||||
@click=${this._toggleExpand}
|
@click=${this._toggleExpand}
|
||||||
></ha-icon-button>`
|
></ha-icon-button>`
|
||||||
@ -239,6 +239,10 @@ class HaConfigSubEntryRow extends LitElement {
|
|||||||
static styles = css`
|
static styles = css`
|
||||||
.expand-button {
|
.expand-button {
|
||||||
margin: 0 -12px;
|
margin: 0 -12px;
|
||||||
|
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
.expand-button.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
ha-md-list {
|
ha-md-list {
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
|
@ -41,7 +41,7 @@ class HaConfigRepairs extends LitElement {
|
|||||||
const issues = this.repairsIssues;
|
const issues = this.repairsIssues;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="title">
|
<div class="title" role="heading" aria-level="2">
|
||||||
${this.hass.localize("ui.panel.config.repairs.title", {
|
${this.hass.localize("ui.panel.config.repairs.title", {
|
||||||
count: this.total || this.repairsIssues.length,
|
count: this.total || this.repairsIssues.length,
|
||||||
})}
|
})}
|
||||||
|
@ -321,6 +321,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
|||||||
.defaultValue=${this._config}
|
.defaultValue=${this._config}
|
||||||
@value-changed=${this._yamlChanged}
|
@value-changed=${this._yamlChanged}
|
||||||
.showErrors=${false}
|
.showErrors=${false}
|
||||||
|
disable-fullscreen
|
||||||
></ha-yaml-editor>`;
|
></ha-yaml-editor>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,6 +440,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.defaultValue=${this._preprocessYaml()}
|
.defaultValue=${this._preprocessYaml()}
|
||||||
.readOnly=${this._readOnly}
|
.readOnly=${this._readOnly}
|
||||||
|
disable-fullscreen
|
||||||
@value-changed=${this._yamlChanged}
|
@value-changed=${this._yamlChanged}
|
||||||
.showErrors=${false}
|
.showErrors=${false}
|
||||||
></ha-yaml-editor>`
|
></ha-yaml-editor>`
|
||||||
|
@ -535,7 +535,7 @@ class HaPanelDevAction extends LitElement {
|
|||||||
if (
|
if (
|
||||||
this._serviceData &&
|
this._serviceData &&
|
||||||
Object.entries(this._serviceData).some(
|
Object.entries(this._serviceData).some(
|
||||||
([key, val]) => key !== "data" && hasTemplate(val)
|
([key, val]) => !["data", "target"].includes(key) && hasTemplate(val)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this._yamlMode = true;
|
this._yamlMode = true;
|
||||||
|
@ -88,6 +88,8 @@ export class HaLogbook extends LitElement {
|
|||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _logbookSubscriptionId = 0;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!isComponentLoaded(this.hass, "logbook")) {
|
if (!isComponentLoaded(this.hass, "logbook")) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -278,13 +280,20 @@ export class HaLogbook extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this._logbookSubscriptionId++;
|
||||||
|
|
||||||
this._unsubLogbook = subscribeLogbook(
|
this._unsubLogbook = subscribeLogbook(
|
||||||
this.hass,
|
this.hass,
|
||||||
(streamMessage) => {
|
(streamMessage, subscriptionId) => {
|
||||||
|
if (subscriptionId !== this._logbookSubscriptionId) {
|
||||||
|
// Ignore messages from previous subscriptions
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._processOrQueueStreamMessage(streamMessage);
|
this._processOrQueueStreamMessage(streamMessage);
|
||||||
},
|
},
|
||||||
logbookPeriod.startTime.toISOString(),
|
logbookPeriod.startTime.toISOString(),
|
||||||
logbookPeriod.endTime.toISOString(),
|
logbookPeriod.endTime.toISOString(),
|
||||||
|
this._logbookSubscriptionId,
|
||||||
this.entityIds,
|
this.entityIds,
|
||||||
this.deviceIds
|
this.deviceIds
|
||||||
);
|
);
|
||||||
|
@ -1,59 +1,81 @@
|
|||||||
import { mdiFan, mdiLightbulb, mdiToggleSwitch } from "@mdi/js";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { callService, type HassEntity } from "home-assistant-js-websocket";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
|
import { generateEntityFilter } from "../../../common/entity/entity_filter";
|
||||||
import {
|
import {
|
||||||
generateEntityFilter,
|
computeGroupEntitiesState,
|
||||||
type EntityFilter,
|
toggleGroupEntities,
|
||||||
} from "../../../common/entity/entity_filter";
|
} from "../../../common/entity/group_entities";
|
||||||
import { stateActive } from "../../../common/entity/state_active";
|
import { stateActive } from "../../../common/entity/state_active";
|
||||||
|
import { domainColorProperties } from "../../../common/entity/state_color";
|
||||||
import "../../../components/ha-control-button";
|
import "../../../components/ha-control-button";
|
||||||
import "../../../components/ha-control-button-group";
|
import "../../../components/ha-control-button-group";
|
||||||
|
import "../../../components/ha-domain-icon";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
|
import { forwardHaptic } from "../../../data/haptics";
|
||||||
|
import { computeCssVariable } from "../../../resources/css-variables";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import type { AreaCardFeatureContext } from "../cards/hui-area-card";
|
||||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||||
import type {
|
import type {
|
||||||
AreaControl,
|
AreaControl,
|
||||||
AreaControlsCardFeatureConfig,
|
AreaControlsCardFeatureConfig,
|
||||||
LovelaceCardFeatureContext,
|
LovelaceCardFeatureContext,
|
||||||
|
LovelaceCardFeaturePosition,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { AREA_CONTROLS } from "./types";
|
import { AREA_CONTROLS } from "./types";
|
||||||
|
|
||||||
interface AreaControlsButton {
|
interface AreaControlsButton {
|
||||||
iconPath: string;
|
offIcon?: string;
|
||||||
onService: string;
|
onIcon?: string;
|
||||||
offService: string;
|
filter: {
|
||||||
filter: EntityFilter;
|
domain: string;
|
||||||
|
device_class?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const coverButton = (deviceClass: string) => ({
|
||||||
|
filter: {
|
||||||
|
domain: "cover",
|
||||||
|
device_class: deviceClass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const AREA_CONTROLS_BUTTONS: Record<AreaControl, AreaControlsButton> = {
|
export const AREA_CONTROLS_BUTTONS: Record<AreaControl, AreaControlsButton> = {
|
||||||
light: {
|
light: {
|
||||||
iconPath: mdiLightbulb,
|
// Overrides the icons for lights
|
||||||
|
offIcon: "mdi:lightbulb-off",
|
||||||
|
onIcon: "mdi:lightbulb",
|
||||||
filter: {
|
filter: {
|
||||||
domain: "light",
|
domain: "light",
|
||||||
},
|
},
|
||||||
onService: "light.turn_on",
|
|
||||||
offService: "light.turn_off",
|
|
||||||
},
|
},
|
||||||
fan: {
|
fan: {
|
||||||
iconPath: mdiFan,
|
|
||||||
filter: {
|
filter: {
|
||||||
domain: "fan",
|
domain: "fan",
|
||||||
},
|
},
|
||||||
onService: "fan.turn_on",
|
|
||||||
offService: "fan.turn_off",
|
|
||||||
},
|
},
|
||||||
switch: {
|
switch: {
|
||||||
iconPath: mdiToggleSwitch,
|
|
||||||
filter: {
|
filter: {
|
||||||
domain: "switch",
|
domain: "switch",
|
||||||
},
|
},
|
||||||
onService: "switch.turn_on",
|
|
||||||
offService: "switch.turn_off",
|
|
||||||
},
|
},
|
||||||
|
"cover-blind": coverButton("blind"),
|
||||||
|
"cover-curtain": coverButton("curtain"),
|
||||||
|
"cover-damper": coverButton("damper"),
|
||||||
|
"cover-awning": coverButton("awning"),
|
||||||
|
"cover-door": coverButton("door"),
|
||||||
|
"cover-garage": coverButton("garage"),
|
||||||
|
"cover-gate": coverButton("gate"),
|
||||||
|
"cover-shade": coverButton("shade"),
|
||||||
|
"cover-shutter": coverButton("shutter"),
|
||||||
|
"cover-window": coverButton("window"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const supportsAreaControlsCardFeature = (
|
export const supportsAreaControlsCardFeature = (
|
||||||
@ -67,8 +89,8 @@ export const supportsAreaControlsCardFeature = (
|
|||||||
export const getAreaControlEntities = (
|
export const getAreaControlEntities = (
|
||||||
controls: AreaControl[],
|
controls: AreaControl[],
|
||||||
areaId: string,
|
areaId: string,
|
||||||
hass: HomeAssistant,
|
excludeEntities: string[] | undefined,
|
||||||
excludeEntities: string[] = []
|
hass: HomeAssistant
|
||||||
): Record<AreaControl, string[]> =>
|
): Record<AreaControl, string[]> =>
|
||||||
controls.reduce(
|
controls.reduce(
|
||||||
(acc, control) => {
|
(acc, control) => {
|
||||||
@ -80,13 +102,15 @@ export const getAreaControlEntities = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
acc[control] = Object.keys(hass.entities).filter(
|
acc[control] = Object.keys(hass.entities).filter(
|
||||||
(entityId) => filter(entityId) && !excludeEntities.includes(entityId)
|
(entityId) => filter(entityId) && !excludeEntities?.includes(entityId)
|
||||||
);
|
);
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<AreaControl, string[]>
|
{} as Record<AreaControl, string[]>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const MAX_DEFAULT_AREA_CONTROLS = 4;
|
||||||
|
|
||||||
@customElement("hui-area-controls-card-feature")
|
@customElement("hui-area-controls-card-feature")
|
||||||
class HuiAreaControlsCardFeature
|
class HuiAreaControlsCardFeature
|
||||||
extends LitElement
|
extends LitElement
|
||||||
@ -94,7 +118,10 @@ class HuiAreaControlsCardFeature
|
|||||||
{
|
{
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
@property({ attribute: false }) public context?: AreaCardFeatureContext;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public position?: LovelaceCardFeaturePosition;
|
||||||
|
|
||||||
@state() private _config?: AreaControlsCardFeatureConfig;
|
@state() private _config?: AreaControlsCardFeatureConfig;
|
||||||
|
|
||||||
@ -144,24 +171,19 @@ class HuiAreaControlsCardFeature
|
|||||||
const controlEntities = this._controlEntities(
|
const controlEntities = this._controlEntities(
|
||||||
this._controls,
|
this._controls,
|
||||||
this.context.area_id,
|
this.context.area_id,
|
||||||
this._config.exclude_entities,
|
this.context.exclude_entities,
|
||||||
this.hass!.entities,
|
this.hass!.entities,
|
||||||
this.hass!.devices,
|
this.hass!.devices,
|
||||||
this.hass!.areas
|
this.hass!.areas
|
||||||
);
|
);
|
||||||
const entitiesIds = controlEntities[control];
|
const entitiesIds = controlEntities[control];
|
||||||
|
|
||||||
const { onService, offService } = AREA_CONTROLS_BUTTONS[control];
|
const entities = entitiesIds
|
||||||
|
.map((entityId) => this.hass!.states[entityId] as HassEntity | undefined)
|
||||||
|
.filter((v): v is HassEntity => Boolean(v));
|
||||||
|
|
||||||
const isOn = entitiesIds.some((entityId) =>
|
forwardHaptic("light");
|
||||||
stateActive(this.hass!.states[entityId] as HassEntity)
|
toggleGroupEntities(this.hass, entities);
|
||||||
);
|
|
||||||
|
|
||||||
const [domain, service] = (isOn ? offService : onService).split(".");
|
|
||||||
|
|
||||||
callService(this.hass!.connection, domain, service, {
|
|
||||||
entity_id: entitiesIds,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _controlEntities = memoizeOne(
|
private _controlEntities = memoizeOne(
|
||||||
@ -173,7 +195,7 @@ class HuiAreaControlsCardFeature
|
|||||||
_entities: HomeAssistant["entities"],
|
_entities: HomeAssistant["entities"],
|
||||||
_devices: HomeAssistant["devices"],
|
_devices: HomeAssistant["devices"],
|
||||||
_areas: HomeAssistant["areas"]
|
_areas: HomeAssistant["areas"]
|
||||||
) => getAreaControlEntities(controls, areaId, this.hass!, excludeEntities)
|
) => getAreaControlEntities(controls, areaId, excludeEntities, this.hass!)
|
||||||
);
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -190,7 +212,7 @@ class HuiAreaControlsCardFeature
|
|||||||
const controlEntities = this._controlEntities(
|
const controlEntities = this._controlEntities(
|
||||||
this._controls,
|
this._controls,
|
||||||
this.context.area_id!,
|
this.context.area_id!,
|
||||||
this._config.exclude_entities,
|
this.context.exclude_entities,
|
||||||
this.hass!.entities,
|
this.hass!.entities,
|
||||||
this.hass!.devices,
|
this.hass!.devices,
|
||||||
this.hass!.areas
|
this.hass!.areas
|
||||||
@ -200,33 +222,71 @@ class HuiAreaControlsCardFeature
|
|||||||
(control) => controlEntities[control].length > 0
|
(control) => controlEntities[control].length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!supportedControls.length) {
|
const displayControls = this._config.controls
|
||||||
|
? supportedControls
|
||||||
|
: supportedControls.slice(0, MAX_DEFAULT_AREA_CONTROLS); // Limit to max if using default controls
|
||||||
|
|
||||||
|
if (!displayControls.length) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-control-button-group>
|
<ha-control-button-group
|
||||||
${supportedControls.map((control) => {
|
class=${classMap({
|
||||||
|
"no-stretch": this.position === "inline",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${displayControls.map((control) => {
|
||||||
const button = AREA_CONTROLS_BUTTONS[control];
|
const button = AREA_CONTROLS_BUTTONS[control];
|
||||||
|
|
||||||
const entities = controlEntities[control];
|
const entityIds = controlEntities[control];
|
||||||
const active = entities.some((entityId) => {
|
|
||||||
const stateObj = this.hass!.states[entityId] as
|
const entities = entityIds
|
||||||
| HassEntity
|
.map(
|
||||||
| undefined;
|
(entityId) =>
|
||||||
if (!stateObj) {
|
this.hass!.states[entityId] as HassEntity | undefined
|
||||||
return false;
|
)
|
||||||
}
|
.filter((v): v is HassEntity => Boolean(v));
|
||||||
return stateActive(stateObj);
|
|
||||||
});
|
const groupState = computeGroupEntitiesState(entities);
|
||||||
|
|
||||||
|
const active = entities[0]
|
||||||
|
? stateActive(entities[0], groupState)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const label = this.hass!.localize(
|
||||||
|
`ui.card_features.area_controls.${control}.${active ? "off" : "on"}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const icon = active ? button.onIcon : button.offIcon;
|
||||||
|
|
||||||
|
const domain = button.filter.domain;
|
||||||
|
const deviceClass = button.filter.device_class
|
||||||
|
? ensureArray(button.filter.device_class)[0]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const activeColor = computeCssVariable(
|
||||||
|
domainColorProperties(domain, deviceClass, groupState, true)
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-control-button
|
<ha-control-button
|
||||||
|
style=${styleMap({
|
||||||
|
"--active-color": activeColor,
|
||||||
|
})}
|
||||||
|
.title=${label}
|
||||||
|
aria-label=${label}
|
||||||
class=${active ? "active" : ""}
|
class=${active ? "active" : ""}
|
||||||
.control=${control}
|
.control=${control}
|
||||||
@click=${this._handleButtonTap}
|
@click=${this._handleButtonTap}
|
||||||
>
|
>
|
||||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
<ha-domain-icon
|
||||||
|
.hass=${this.hass}
|
||||||
|
.icon=${icon}
|
||||||
|
.domain=${domain}
|
||||||
|
.deviceClass=${deviceClass}
|
||||||
|
.state=${groupState}
|
||||||
|
></ha-domain-icon>
|
||||||
</ha-control-button>
|
</ha-control-button>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
@ -238,6 +298,23 @@ class HuiAreaControlsCardFeature
|
|||||||
return [
|
return [
|
||||||
cardFeatureStyles,
|
cardFeatureStyles,
|
||||||
css`
|
css`
|
||||||
|
:host {
|
||||||
|
pointer-events: none !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
ha-control-button-group {
|
||||||
|
pointer-events: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
ha-control-button-group.no-stretch {
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
ha-control-button-group.no-stretch > ha-control-button {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
ha-control-button {
|
ha-control-button {
|
||||||
--active-color: var(--state-active-color);
|
--active-color: var(--state-active-color);
|
||||||
--control-button-focus-color: var(--state-active-color);
|
--control-button-focus-color: var(--state-active-color);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import type { HuiErrorCard } from "../cards/hui-error-card";
|
import type { HuiErrorCard } from "../cards/hui-error-card";
|
||||||
@ -7,6 +7,7 @@ import type { LovelaceCardFeature } from "../types";
|
|||||||
import type {
|
import type {
|
||||||
LovelaceCardFeatureConfig,
|
LovelaceCardFeatureConfig,
|
||||||
LovelaceCardFeatureContext,
|
LovelaceCardFeatureContext,
|
||||||
|
LovelaceCardFeaturePosition,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@customElement("hui-card-feature")
|
@customElement("hui-card-feature")
|
||||||
@ -19,6 +20,9 @@ export class HuiCardFeature extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public color?: string;
|
@property({ attribute: false }) public color?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public position?: LovelaceCardFeaturePosition;
|
||||||
|
|
||||||
private _element?: LovelaceCardFeature | HuiErrorCard;
|
private _element?: LovelaceCardFeature | HuiErrorCard;
|
||||||
|
|
||||||
private _getFeatureElement(feature: LovelaceCardFeatureConfig) {
|
private _getFeatureElement(feature: LovelaceCardFeatureConfig) {
|
||||||
@ -41,6 +45,7 @@ export class HuiCardFeature extends LitElement {
|
|||||||
element.hass = this.hass;
|
element.hass = this.hass;
|
||||||
element.context = this.context;
|
element.context = this.context;
|
||||||
element.color = this.color;
|
element.color = this.color;
|
||||||
|
element.position = this.position;
|
||||||
// Backwards compatibility from custom card features
|
// Backwards compatibility from custom card features
|
||||||
if (this.context.entity_id) {
|
if (this.context.entity_id) {
|
||||||
const stateObj = this.hass.states[this.context.entity_id];
|
const stateObj = this.hass.states[this.context.entity_id];
|
||||||
@ -51,6 +56,12 @@ export class HuiCardFeature extends LitElement {
|
|||||||
}
|
}
|
||||||
return html`${element}`;
|
return html`${element}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -5,6 +5,7 @@ import "./hui-card-feature";
|
|||||||
import type {
|
import type {
|
||||||
LovelaceCardFeatureConfig,
|
LovelaceCardFeatureConfig,
|
||||||
LovelaceCardFeatureContext,
|
LovelaceCardFeatureContext,
|
||||||
|
LovelaceCardFeaturePosition,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@customElement("hui-card-features")
|
@customElement("hui-card-features")
|
||||||
@ -17,6 +18,9 @@ export class HuiCardFeatures extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public color?: string;
|
@property({ attribute: false }) public color?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public position?: LovelaceCardFeaturePosition;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.features) {
|
if (!this.features) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -29,6 +33,7 @@ export class HuiCardFeatures extends LitElement {
|
|||||||
.context=${this.context}
|
.context=${this.context}
|
||||||
.color=${this.color}
|
.color=${this.color}
|
||||||
.feature=${feature}
|
.feature=${feature}
|
||||||
|
.position=${this.position}
|
||||||
></hui-card-feature>
|
></hui-card-feature>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
@ -41,6 +46,7 @@ export class HuiCardFeatures extends LitElement {
|
|||||||
--feature-height: 42px;
|
--feature-height: 42px;
|
||||||
--feature-border-radius: 12px;
|
--feature-border-radius: 12px;
|
||||||
--feature-button-spacing: 12px;
|
--feature-button-spacing: 12px;
|
||||||
|
pointer-events: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -158,16 +158,31 @@ export interface UpdateActionsCardFeatureConfig {
|
|||||||
backup?: "yes" | "no" | "ask";
|
backup?: "yes" | "no" | "ask";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AREA_CONTROLS = ["light", "fan", "switch"] as const;
|
export const AREA_CONTROLS = [
|
||||||
|
"light",
|
||||||
|
"fan",
|
||||||
|
"cover-shutter",
|
||||||
|
"cover-blind",
|
||||||
|
"cover-curtain",
|
||||||
|
"cover-shade",
|
||||||
|
"cover-awning",
|
||||||
|
"cover-garage",
|
||||||
|
"cover-gate",
|
||||||
|
"cover-door",
|
||||||
|
"cover-window",
|
||||||
|
"cover-damper",
|
||||||
|
"switch",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export type AreaControl = (typeof AREA_CONTROLS)[number];
|
export type AreaControl = (typeof AREA_CONTROLS)[number];
|
||||||
|
|
||||||
export interface AreaControlsCardFeatureConfig {
|
export interface AreaControlsCardFeatureConfig {
|
||||||
type: "area-controls";
|
type: "area-controls";
|
||||||
controls?: AreaControl[];
|
controls?: AreaControl[];
|
||||||
exclude_entities?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LovelaceCardFeaturePosition = "bottom" | "inline";
|
||||||
|
|
||||||
export type LovelaceCardFeatureConfig =
|
export type LovelaceCardFeatureConfig =
|
||||||
| AlarmModesCardFeatureConfig
|
| AlarmModesCardFeatureConfig
|
||||||
| ClimateFanModesCardFeatureConfig
|
| ClimateFanModesCardFeatureConfig
|
||||||
|
@ -53,6 +53,10 @@ export const DEVICE_CLASSES = {
|
|||||||
binary_sensor: ["motion", "moisture"],
|
binary_sensor: ["motion", "moisture"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface AreaCardFeatureContext extends LovelaceCardFeatureContext {
|
||||||
|
exclude_entities?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("hui-area-card")
|
@customElement("hui-area-card")
|
||||||
export class HuiAreaCard extends LitElement implements LovelaceCard {
|
export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -61,7 +65,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
@state() private _config?: AreaCardConfig;
|
@state() private _config?: AreaCardConfig;
|
||||||
|
|
||||||
@state() private _featureContext: LovelaceCardFeatureContext = {};
|
@state() private _featureContext: AreaCardFeatureContext = {};
|
||||||
|
|
||||||
private _ratio: {
|
private _ratio: {
|
||||||
w: number;
|
w: number;
|
||||||
@ -87,6 +91,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
this._featureContext = {
|
this._featureContext = {
|
||||||
area_id: config.area,
|
area_id: config.area,
|
||||||
|
exclude_entities: config.exclude_entities,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +171,8 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
(
|
(
|
||||||
entities: HomeAssistant["entities"],
|
entities: HomeAssistant["entities"],
|
||||||
areaId: string,
|
areaId: string,
|
||||||
sensorClasses: string[]
|
sensorClasses: string[],
|
||||||
|
excludeEntities?: string[]
|
||||||
): Map<string, string[]> => {
|
): Map<string, string[]> => {
|
||||||
const sensorFilter = generateEntityFilter(this.hass, {
|
const sensorFilter = generateEntityFilter(this.hass, {
|
||||||
area: areaId,
|
area: areaId,
|
||||||
@ -174,7 +180,10 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
domain: "sensor",
|
domain: "sensor",
|
||||||
device_class: sensorClasses,
|
device_class: sensorClasses,
|
||||||
});
|
});
|
||||||
const entityIds = Object.keys(entities).filter(sensorFilter);
|
const entityIds = Object.keys(entities).filter(
|
||||||
|
(id) => sensorFilter(id) && !excludeEntities?.includes(id)
|
||||||
|
);
|
||||||
|
|
||||||
return this._groupEntitiesByDeviceClass(entityIds);
|
return this._groupEntitiesByDeviceClass(entityIds);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -183,7 +192,8 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
(
|
(
|
||||||
entities: HomeAssistant["entities"],
|
entities: HomeAssistant["entities"],
|
||||||
areaId: string,
|
areaId: string,
|
||||||
binarySensorClasses: string[]
|
binarySensorClasses: string[],
|
||||||
|
excludeEntities?: string[]
|
||||||
): Map<string, string[]> => {
|
): Map<string, string[]> => {
|
||||||
const binarySensorFilter = generateEntityFilter(this.hass, {
|
const binarySensorFilter = generateEntityFilter(this.hass, {
|
||||||
area: areaId,
|
area: areaId,
|
||||||
@ -191,7 +201,11 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
domain: "binary_sensor",
|
domain: "binary_sensor",
|
||||||
device_class: binarySensorClasses,
|
device_class: binarySensorClasses,
|
||||||
});
|
});
|
||||||
const entityIds = Object.keys(entities).filter(binarySensorFilter);
|
|
||||||
|
const entityIds = Object.keys(entities).filter(
|
||||||
|
(id) => binarySensorFilter(id) && !excludeEntities?.includes(id)
|
||||||
|
);
|
||||||
|
|
||||||
return this._groupEntitiesByDeviceClass(entityIds);
|
return this._groupEntitiesByDeviceClass(entityIds);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -215,13 +229,15 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
const areaId = this._config?.area;
|
const areaId = this._config?.area;
|
||||||
const area = areaId ? this.hass.areas[areaId] : undefined;
|
const area = areaId ? this.hass.areas[areaId] : undefined;
|
||||||
const alertClasses = this._config?.alert_classes;
|
const alertClasses = this._config?.alert_classes;
|
||||||
|
const excludeEntities = this._config?.exclude_entities;
|
||||||
if (!area || !alertClasses) {
|
if (!area || !alertClasses) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const groupedEntities = this._groupedBinarySensorEntityIds(
|
const groupedEntities = this._groupedBinarySensorEntityIds(
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
area.area_id,
|
area.area_id,
|
||||||
alertClasses
|
alertClasses,
|
||||||
|
excludeEntities
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -286,6 +302,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
const areaId = this._config?.area;
|
const areaId = this._config?.area;
|
||||||
const area = areaId ? this.hass.areas[areaId] : undefined;
|
const area = areaId ? this.hass.areas[areaId] : undefined;
|
||||||
const sensorClasses = this._config?.sensor_classes;
|
const sensorClasses = this._config?.sensor_classes;
|
||||||
|
const excludeEntities = this._config?.exclude_entities;
|
||||||
if (!area || !sensorClasses) {
|
if (!area || !sensorClasses) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -293,7 +310,8 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
const groupedEntities = this._groupedSensorEntityIds(
|
const groupedEntities = this._groupedSensorEntityIds(
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
area.area_id,
|
area.area_id,
|
||||||
sensorClasses
|
sensorClasses,
|
||||||
|
excludeEntities
|
||||||
);
|
);
|
||||||
|
|
||||||
const sensorStates = sensorClasses
|
const sensorStates = sensorClasses
|
||||||
@ -330,6 +348,14 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If only one entity, return its formatted state
|
||||||
|
if (entities.length === 1) {
|
||||||
|
const stateObj = entities[0];
|
||||||
|
return isUnavailableState(stateObj.state)
|
||||||
|
? ""
|
||||||
|
: this.hass.formatEntityState(stateObj);
|
||||||
|
}
|
||||||
|
|
||||||
// Use the first entity's unit_of_measurement for formatting
|
// Use the first entity's unit_of_measurement for formatting
|
||||||
const uom = entities.find(
|
const uom = entities.find(
|
||||||
(entity) => entity.attributes.unit_of_measurement
|
(entity) => entity.attributes.unit_of_measurement
|
||||||
@ -514,6 +540,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
|||||||
.context=${this._featureContext}
|
.context=${this._featureContext}
|
||||||
.color=${this._config.color}
|
.color=${this._config.color}
|
||||||
.features=${features}
|
.features=${features}
|
||||||
|
.position=${featurePosition}
|
||||||
></hui-card-features>
|
></hui-card-features>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
@ -243,11 +243,11 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let itemsToShow = this._config?.forecast_slots ?? 5;
|
let itemsToShow = this._config?.forecast_slots ?? 5;
|
||||||
if (this._sizeController.value.width === "very-very-narrow") {
|
if (this._sizeController.value?.width === "very-very-narrow") {
|
||||||
itemsToShow = Math.min(3, itemsToShow);
|
itemsToShow = Math.min(3, itemsToShow);
|
||||||
} else if (this._sizeController.value.width === "very-narrow") {
|
} else if (this._sizeController.value?.width === "very-narrow") {
|
||||||
itemsToShow = Math.min(5, itemsToShow);
|
itemsToShow = Math.min(5, itemsToShow);
|
||||||
} else if (this._sizeController.value.width === "narrow") {
|
} else if (this._sizeController.value?.width === "narrow") {
|
||||||
itemsToShow = Math.min(7, itemsToShow);
|
itemsToShow = Math.min(7, itemsToShow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,8 +266,12 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
|||||||
return html`
|
return html`
|
||||||
<ha-card
|
<ha-card
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
[this._sizeController.value.height]: true,
|
[this._sizeController.value?.height]: Boolean(
|
||||||
[this._sizeController.value.width]: true,
|
this._sizeController.value
|
||||||
|
),
|
||||||
|
[this._sizeController.value?.width]: Boolean(
|
||||||
|
this._sizeController.value
|
||||||
|
),
|
||||||
})}
|
})}
|
||||||
@action=${this._handleAction}
|
@action=${this._handleAction}
|
||||||
.actionHandler=${actionHandler({
|
.actionHandler=${actionHandler({
|
||||||
|
@ -9,7 +9,10 @@ import type {
|
|||||||
ThemeMode,
|
ThemeMode,
|
||||||
TranslationDict,
|
TranslationDict,
|
||||||
} from "../../../types";
|
} from "../../../types";
|
||||||
import type { LovelaceCardFeatureConfig } from "../card-features/types";
|
import type {
|
||||||
|
LovelaceCardFeatureConfig,
|
||||||
|
LovelaceCardFeaturePosition,
|
||||||
|
} from "../card-features/types";
|
||||||
import type { LegacyStateFilter } from "../common/evaluate-filter";
|
import type { LegacyStateFilter } from "../common/evaluate-filter";
|
||||||
import type { Condition, LegacyCondition } from "../common/validate-condition";
|
import type { Condition, LegacyCondition } from "../common/validate-condition";
|
||||||
import type { HuiImage } from "../components/hui-image";
|
import type { HuiImage } from "../components/hui-image";
|
||||||
@ -113,7 +116,8 @@ export interface AreaCardConfig extends LovelaceCardConfig {
|
|||||||
sensor_classes?: string[];
|
sensor_classes?: string[];
|
||||||
alert_classes?: string[];
|
alert_classes?: string[];
|
||||||
features?: LovelaceCardFeatureConfig[];
|
features?: LovelaceCardFeatureConfig[];
|
||||||
features_position?: "bottom" | "inline";
|
features_position?: LovelaceCardFeaturePosition;
|
||||||
|
exclude_entities?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ButtonCardConfig extends LovelaceCardConfig {
|
export interface ButtonCardConfig extends LovelaceCardConfig {
|
||||||
@ -564,7 +568,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
|||||||
icon_hold_action?: ActionConfig;
|
icon_hold_action?: ActionConfig;
|
||||||
icon_double_tap_action?: ActionConfig;
|
icon_double_tap_action?: ActionConfig;
|
||||||
features?: LovelaceCardFeatureConfig[];
|
features?: LovelaceCardFeatureConfig[];
|
||||||
features_position?: "bottom" | "inline";
|
features_position?: LovelaceCardFeaturePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeadingCardConfig extends LovelaceCardConfig {
|
export interface HeadingCardConfig extends LovelaceCardConfig {
|
||||||
|
@ -74,8 +74,6 @@ export class HuiDialogEditCard
|
|||||||
|
|
||||||
@state() private _dirty = false;
|
@state() private _dirty = false;
|
||||||
|
|
||||||
@state() private _isEscapeEnabled = true;
|
|
||||||
|
|
||||||
public async showDialog(params: EditCardDialogParams): Promise<void> {
|
public async showDialog(params: EditCardDialogParams): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._GUImode = true;
|
this._GUImode = true;
|
||||||
@ -93,9 +91,6 @@ export class HuiDialogEditCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): boolean {
|
public closeDialog(): boolean {
|
||||||
this._isEscapeEnabled = true;
|
|
||||||
window.removeEventListener("dialog-closed", this._enableEscapeKeyClose);
|
|
||||||
window.removeEventListener("hass-more-info", this._disableEscapeKeyClose);
|
|
||||||
if (this._dirty) {
|
if (this._dirty) {
|
||||||
this._confirmCancel();
|
this._confirmCancel();
|
||||||
return false;
|
return false;
|
||||||
@ -124,16 +119,6 @@ export class HuiDialogEditCard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _enableEscapeKeyClose = (ev: any) => {
|
|
||||||
if (ev.detail.dialog === "ha-more-info-dialog") {
|
|
||||||
this._isEscapeEnabled = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private _disableEscapeKeyClose = () => {
|
|
||||||
this._isEscapeEnabled = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._params || !this._cardConfig) {
|
if (!this._params || !this._cardConfig) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -170,7 +155,7 @@ export class HuiDialogEditCard
|
|||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
scrimClickAction
|
scrimClickAction
|
||||||
.escapeKeyAction=${this._isEscapeEnabled ? undefined : ""}
|
escapeKeyAction
|
||||||
@keydown=${this._ignoreKeydown}
|
@keydown=${this._ignoreKeydown}
|
||||||
@closed=${this._cancel}
|
@closed=${this._cancel}
|
||||||
@opened=${this._opened}
|
@opened=${this._opened}
|
||||||
@ -304,8 +289,6 @@ export class HuiDialogEditCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _opened() {
|
private _opened() {
|
||||||
window.addEventListener("dialog-closed", this._enableEscapeKeyClose);
|
|
||||||
window.addEventListener("hass-more-info", this._disableEscapeKeyClose);
|
|
||||||
this._cardEditorEl?.focusYamlEditor();
|
this._cardEditorEl?.focusYamlEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,10 @@ import type {
|
|||||||
LovelaceCardFeatureConfig,
|
LovelaceCardFeatureConfig,
|
||||||
LovelaceCardFeatureContext,
|
LovelaceCardFeatureContext,
|
||||||
} from "../../card-features/types";
|
} from "../../card-features/types";
|
||||||
import { DEVICE_CLASSES } from "../../cards/hui-area-card";
|
import {
|
||||||
|
DEVICE_CLASSES,
|
||||||
|
type AreaCardFeatureContext,
|
||||||
|
} from "../../cards/hui-area-card";
|
||||||
import type { AreaCardConfig } from "../../cards/types";
|
import type { AreaCardConfig } from "../../cards/types";
|
||||||
import type { LovelaceCardEditor } from "../../types";
|
import type { LovelaceCardEditor } from "../../types";
|
||||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||||
@ -55,6 +58,7 @@ const cardConfigStruct = assign(
|
|||||||
features: optional(array(any())),
|
features: optional(array(any())),
|
||||||
features_position: optional(enums(["bottom", "inline"])),
|
features_position: optional(enums(["bottom", "inline"])),
|
||||||
aspect_ratio: optional(string()),
|
aspect_ratio: optional(string()),
|
||||||
|
exclude_entities: optional(array(string())),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -69,11 +73,7 @@ export class HuiAreaCardEditor
|
|||||||
|
|
||||||
@state() private _numericDeviceClasses?: string[];
|
@state() private _numericDeviceClasses?: string[];
|
||||||
|
|
||||||
private _featureContext = memoizeOne(
|
@state() private _featureContext: AreaCardFeatureContext = {};
|
||||||
(areaId?: string): LovelaceCardFeatureContext => ({
|
|
||||||
area_id: areaId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
private _schema = memoizeOne(
|
private _schema = memoizeOne(
|
||||||
(
|
(
|
||||||
@ -174,7 +174,10 @@ export class HuiAreaCardEditor
|
|||||||
);
|
);
|
||||||
|
|
||||||
private _binaryClassesForArea = memoizeOne(
|
private _binaryClassesForArea = memoizeOne(
|
||||||
(area: string | undefined): string[] => {
|
(
|
||||||
|
area: string | undefined,
|
||||||
|
excludeEntities: string[] | undefined
|
||||||
|
): string[] => {
|
||||||
if (!area) {
|
if (!area) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -186,7 +189,9 @@ export class HuiAreaCardEditor
|
|||||||
});
|
});
|
||||||
|
|
||||||
const classes = Object.keys(this.hass!.entities)
|
const classes = Object.keys(this.hass!.entities)
|
||||||
.filter(binarySensorFilter)
|
.filter(
|
||||||
|
(id) => binarySensorFilter(id) && !excludeEntities?.includes(id)
|
||||||
|
)
|
||||||
.map((id) => this.hass!.states[id]?.attributes.device_class)
|
.map((id) => this.hass!.states[id]?.attributes.device_class)
|
||||||
.filter((c): c is string => Boolean(c));
|
.filter((c): c is string => Boolean(c));
|
||||||
|
|
||||||
@ -195,7 +200,11 @@ export class HuiAreaCardEditor
|
|||||||
);
|
);
|
||||||
|
|
||||||
private _sensorClassesForArea = memoizeOne(
|
private _sensorClassesForArea = memoizeOne(
|
||||||
(area: string | undefined, numericDeviceClasses?: string[]): string[] => {
|
(
|
||||||
|
area: string | undefined,
|
||||||
|
excludeEntities: string[] | undefined,
|
||||||
|
numericDeviceClasses: string[] | undefined
|
||||||
|
): string[] => {
|
||||||
if (!area) {
|
if (!area) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -208,7 +217,7 @@ export class HuiAreaCardEditor
|
|||||||
});
|
});
|
||||||
|
|
||||||
const classes = Object.keys(this.hass!.entities)
|
const classes = Object.keys(this.hass!.entities)
|
||||||
.filter(sensorFilter)
|
.filter((id) => sensorFilter(id) && !excludeEntities?.includes(id))
|
||||||
.map((id) => this.hass!.states[id]?.attributes.device_class)
|
.map((id) => this.hass!.states[id]?.attributes.device_class)
|
||||||
.filter((c): c is string => Boolean(c));
|
.filter((c): c is string => Boolean(c));
|
||||||
|
|
||||||
@ -257,6 +266,11 @@ export class HuiAreaCardEditor
|
|||||||
display_type: displayType,
|
display_type: displayType,
|
||||||
};
|
};
|
||||||
delete this._config.show_camera;
|
delete this._config.show_camera;
|
||||||
|
|
||||||
|
this._featureContext = {
|
||||||
|
area_id: config.area,
|
||||||
|
exclude_entities: config.exclude_entities,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async updated() {
|
protected async updated() {
|
||||||
@ -306,11 +320,13 @@ export class HuiAreaCardEditor
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const areaId = this._config!.area;
|
const possibleBinaryClasses = this._binaryClassesForArea(
|
||||||
|
this._config.area,
|
||||||
const possibleBinaryClasses = this._binaryClassesForArea(this._config.area);
|
this._config.exclude_entities
|
||||||
|
);
|
||||||
const possibleSensorClasses = this._sensorClassesForArea(
|
const possibleSensorClasses = this._sensorClassesForArea(
|
||||||
this._config.area,
|
this._config.area,
|
||||||
|
this._config.exclude_entities,
|
||||||
this._numericDeviceClasses
|
this._numericDeviceClasses
|
||||||
);
|
);
|
||||||
const binarySelectOptions = this._buildBinaryOptions(
|
const binarySelectOptions = this._buildBinaryOptions(
|
||||||
@ -347,8 +363,9 @@ export class HuiAreaCardEditor
|
|||||||
...this._config,
|
...this._config,
|
||||||
};
|
};
|
||||||
|
|
||||||
const featureContext = this._featureContext(areaId);
|
const hasCompatibleFeatures = this._hasCompatibleFeatures(
|
||||||
const hasCompatibleFeatures = this._hasCompatibleFeatures(featureContext);
|
this._featureContext
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-form
|
<ha-form
|
||||||
@ -381,7 +398,7 @@ export class HuiAreaCardEditor
|
|||||||
: nothing}
|
: nothing}
|
||||||
<hui-card-features-editor
|
<hui-card-features-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.context=${featureContext}
|
.context=${this._featureContext}
|
||||||
.features=${this._config!.features ?? []}
|
.features=${this._config!.features ?? []}
|
||||||
@features-changed=${this._featuresChanged}
|
@features-changed=${this._featuresChanged}
|
||||||
@edit-detail-element=${this._editDetailElement}
|
@edit-detail-element=${this._editDetailElement}
|
||||||
@ -428,12 +445,11 @@ export class HuiAreaCardEditor
|
|||||||
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
|
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
|
||||||
const index = ev.detail.subElementConfig.index;
|
const index = ev.detail.subElementConfig.index;
|
||||||
const config = this._config!.features![index!];
|
const config = this._config!.features![index!];
|
||||||
const featureContext = this._featureContext(this._config!.area);
|
|
||||||
|
|
||||||
fireEvent(this, "edit-sub-element", {
|
fireEvent(this, "edit-sub-element", {
|
||||||
config: config,
|
config: config,
|
||||||
saveConfig: (newConfig) => this._updateFeature(index!, newConfig),
|
saveConfig: (newConfig) => this._updateFeature(index!, newConfig),
|
||||||
context: featureContext,
|
context: this._featureContext,
|
||||||
type: "feature",
|
type: "feature",
|
||||||
} as EditSubElementEvent<
|
} as EditSubElementEvent<
|
||||||
LovelaceCardFeatureConfig,
|
LovelaceCardFeatureConfig,
|
||||||
|
@ -9,13 +9,16 @@ import type {
|
|||||||
SchemaUnion,
|
SchemaUnion,
|
||||||
} from "../../../../components/ha-form/types";
|
} from "../../../../components/ha-form/types";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { getAreaControlEntities } from "../../card-features/hui-area-controls-card-feature";
|
import {
|
||||||
|
getAreaControlEntities,
|
||||||
|
MAX_DEFAULT_AREA_CONTROLS,
|
||||||
|
} from "../../card-features/hui-area-controls-card-feature";
|
||||||
import {
|
import {
|
||||||
AREA_CONTROLS,
|
AREA_CONTROLS,
|
||||||
type AreaControl,
|
type AreaControl,
|
||||||
type AreaControlsCardFeatureConfig,
|
type AreaControlsCardFeatureConfig,
|
||||||
type LovelaceCardFeatureContext,
|
|
||||||
} from "../../card-features/types";
|
} from "../../card-features/types";
|
||||||
|
import type { AreaCardFeatureContext } from "../../cards/hui-area-card";
|
||||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||||
|
|
||||||
type AreaControlsCardFeatureData = AreaControlsCardFeatureConfig & {
|
type AreaControlsCardFeatureData = AreaControlsCardFeatureConfig & {
|
||||||
@ -29,7 +32,7 @@ export class HuiAreaControlsCardFeatureEditor
|
|||||||
{
|
{
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
@property({ attribute: false }) public context?: AreaCardFeatureContext;
|
||||||
|
|
||||||
@state() private _config?: AreaControlsCardFeatureConfig;
|
@state() private _config?: AreaControlsCardFeatureConfig;
|
||||||
|
|
||||||
@ -72,9 +75,10 @@ export class HuiAreaControlsCardFeatureEditor
|
|||||||
] as const satisfies readonly HaFormSchema[]
|
] as const satisfies readonly HaFormSchema[]
|
||||||
);
|
);
|
||||||
|
|
||||||
private _compatibleControls = memoizeOne(
|
private _supportedControls = memoizeOne(
|
||||||
(
|
(
|
||||||
areaId: string,
|
areaId: string,
|
||||||
|
excludeEntities: string[] | undefined,
|
||||||
// needed to update memoized function when entities, devices or areas change
|
// needed to update memoized function when entities, devices or areas change
|
||||||
_entities: HomeAssistant["entities"],
|
_entities: HomeAssistant["entities"],
|
||||||
_devices: HomeAssistant["devices"],
|
_devices: HomeAssistant["devices"],
|
||||||
@ -86,6 +90,7 @@ export class HuiAreaControlsCardFeatureEditor
|
|||||||
const controlEntities = getAreaControlEntities(
|
const controlEntities = getAreaControlEntities(
|
||||||
AREA_CONTROLS as unknown as AreaControl[],
|
AREA_CONTROLS as unknown as AreaControl[],
|
||||||
areaId,
|
areaId,
|
||||||
|
excludeEntities,
|
||||||
this.hass!
|
this.hass!
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
@ -99,14 +104,15 @@ export class HuiAreaControlsCardFeatureEditor
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const compatibleControls = this._compatibleControls(
|
const supportedControls = this._supportedControls(
|
||||||
this.context.area_id,
|
this.context.area_id,
|
||||||
|
this.context.exclude_entities,
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
this.hass.devices,
|
this.hass.devices,
|
||||||
this.hass.areas
|
this.hass.areas
|
||||||
);
|
);
|
||||||
|
|
||||||
if (compatibleControls.length === 0) {
|
if (supportedControls.length === 0) {
|
||||||
return html`
|
return html`
|
||||||
<ha-alert alert-type="warning">
|
<ha-alert alert-type="warning">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@ -124,7 +130,7 @@ export class HuiAreaControlsCardFeatureEditor
|
|||||||
const schema = this._schema(
|
const schema = this._schema(
|
||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
data.customize_controls,
|
data.customize_controls,
|
||||||
compatibleControls
|
supportedControls
|
||||||
);
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@ -143,12 +149,13 @@ export class HuiAreaControlsCardFeatureEditor
|
|||||||
.value as AreaControlsCardFeatureData;
|
.value as AreaControlsCardFeatureData;
|
||||||
|
|
||||||
if (customize_controls && !config.controls) {
|
if (customize_controls && !config.controls) {
|
||||||
config.controls = this._compatibleControls(
|
config.controls = this._supportedControls(
|
||||||
this.context!.area_id!,
|
this.context!.area_id!,
|
||||||
|
this.context!.exclude_entities,
|
||||||
this.hass!.entities,
|
this.hass!.entities,
|
||||||
this.hass!.devices,
|
this.hass!.devices,
|
||||||
this.hass!.areas
|
this.hass!.areas
|
||||||
).concat();
|
).slice(0, MAX_DEFAULT_AREA_CONTROLS); // Limit to max default controls
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!customize_controls && config.controls) {
|
if (!customize_controls && config.controls) {
|
||||||
|
@ -144,6 +144,9 @@ class DialogDashboardStrategyEditor extends LitElement {
|
|||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<span slot="title" .title=${title}>${title}</span>
|
<span slot="title" .title=${title}>${title}</span>
|
||||||
|
${this._params.title
|
||||||
|
? html`<span slot="subtitle">${this._params.title}</span>`
|
||||||
|
: nothing}
|
||||||
<ha-button-menu
|
<ha-button-menu
|
||||||
corner="BOTTOM_END"
|
corner="BOTTOM_END"
|
||||||
menu-corner="END"
|
menu-corner="END"
|
||||||
|
@ -3,6 +3,7 @@ import type { LovelaceDashboardStrategyConfig } from "../../../../../data/lovela
|
|||||||
|
|
||||||
export interface DashboardStrategyEditorDialogParams {
|
export interface DashboardStrategyEditorDialogParams {
|
||||||
config: LovelaceDashboardStrategyConfig;
|
config: LovelaceDashboardStrategyConfig;
|
||||||
|
title?: string;
|
||||||
saveConfig: (config: LovelaceDashboardStrategyConfig) => void;
|
saveConfig: (config: LovelaceDashboardStrategyConfig) => void;
|
||||||
takeControl: () => void;
|
takeControl: () => void;
|
||||||
deleteDashboard: () => Promise<boolean>;
|
deleteDashboard: () => Promise<boolean>;
|
||||||
|
@ -89,6 +89,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@value-changed=${this._yamlChanged}
|
@value-changed=${this._yamlChanged}
|
||||||
@editor-save=${this._handleSave}
|
@editor-save=${this._handleSave}
|
||||||
|
disable-fullscreen
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
>
|
>
|
||||||
</ha-code-editor>
|
</ha-code-editor>
|
||||||
|
@ -782,6 +782,7 @@ class HUIRoot extends LitElement {
|
|||||||
|
|
||||||
showDashboardStrategyEditorDialog(this, {
|
showDashboardStrategyEditorDialog(this, {
|
||||||
config: this.lovelace!.rawConfig,
|
config: this.lovelace!.rawConfig,
|
||||||
|
title: this.panel ? getPanelTitle(this.hass, this.panel) : undefined,
|
||||||
saveConfig: this.lovelace!.saveConfig,
|
saveConfig: this.lovelace!.saveConfig,
|
||||||
takeControl: () => {
|
takeControl: () => {
|
||||||
showSaveDialog(this, {
|
showSaveDialog(this, {
|
||||||
|
@ -22,6 +22,9 @@ export interface AreasDashboardStrategyConfig {
|
|||||||
hidden?: string[];
|
hidden?: string[];
|
||||||
order?: string[];
|
order?: string[];
|
||||||
};
|
};
|
||||||
|
floors_display?: {
|
||||||
|
order?: string[];
|
||||||
|
};
|
||||||
areas_options?: Record<string, AreaOptions>;
|
areas_options?: Record<string, AreaOptions>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,13 +81,13 @@ export class AreasDashboardStrategy extends ReactiveElement {
|
|||||||
return {
|
return {
|
||||||
views: [
|
views: [
|
||||||
{
|
{
|
||||||
title: "Home",
|
|
||||||
icon: "mdi:home",
|
icon: "mdi:home",
|
||||||
path: "home",
|
path: "home",
|
||||||
strategy: {
|
strategy: {
|
||||||
type: "areas-overview",
|
type: "areas-overview",
|
||||||
areas_display: config.areas_display,
|
areas_display: config.areas_display,
|
||||||
areas_options: config.areas_options,
|
areas_options: config.areas_options,
|
||||||
|
floors_display: config.floors_display,
|
||||||
} satisfies AreasViewStrategyConfig,
|
} satisfies AreasViewStrategyConfig,
|
||||||
},
|
},
|
||||||
...areaViews,
|
...areaViews,
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import { stringCompare } from "../../../../common/string/compare";
|
|
||||||
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
||||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { getAreaControlEntities } from "../../card-features/hui-area-controls-card-feature";
|
import { getAreaControlEntities } from "../../card-features/hui-area-controls-card-feature";
|
||||||
import type { AreaControl } from "../../card-features/types";
|
import { AREA_CONTROLS, type AreaControl } from "../../card-features/types";
|
||||||
import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
|
import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
|
||||||
import type { EntitiesDisplay } from "./area-view-strategy";
|
import type { EntitiesDisplay } from "./area-view-strategy";
|
||||||
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper";
|
import {
|
||||||
|
computeAreaPath,
|
||||||
|
getAreas,
|
||||||
|
getFloors,
|
||||||
|
} from "./helpers/areas-strategy-helper";
|
||||||
|
|
||||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
const UNASSIGNED_FLOOR = "__unassigned__";
|
||||||
|
|
||||||
@ -23,6 +26,9 @@ export interface AreasViewStrategyConfig {
|
|||||||
hidden?: string[];
|
hidden?: string[];
|
||||||
order?: string[];
|
order?: string[];
|
||||||
};
|
};
|
||||||
|
floors_display?: {
|
||||||
|
order?: string[];
|
||||||
|
};
|
||||||
areas_options?: Record<string, AreaOptions>;
|
areas_options?: Record<string, AreaOptions>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,43 +38,35 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
|
|||||||
config: AreasViewStrategyConfig,
|
config: AreasViewStrategyConfig,
|
||||||
hass: HomeAssistant
|
hass: HomeAssistant
|
||||||
): Promise<LovelaceViewConfig> {
|
): Promise<LovelaceViewConfig> {
|
||||||
const areas = getAreas(
|
const displayedAreas = getAreas(
|
||||||
hass.areas,
|
hass.areas,
|
||||||
config.areas_display?.hidden,
|
config.areas_display?.hidden,
|
||||||
config.areas_display?.order
|
config.areas_display?.order
|
||||||
);
|
);
|
||||||
|
|
||||||
const floors = Object.values(hass.floors);
|
const floors = getFloors(hass.floors, config.floors_display?.order);
|
||||||
floors.sort((floorA, floorB) => {
|
|
||||||
if (floorA.level !== floorB.level) {
|
|
||||||
return (floorA.level ?? 0) - (floorB.level ?? 0);
|
|
||||||
}
|
|
||||||
return stringCompare(floorA.name, floorB.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const floorSections = [
|
const floorSections = [
|
||||||
...floors,
|
...floors,
|
||||||
{
|
{
|
||||||
floor_id: UNASSIGNED_FLOOR,
|
floor_id: UNASSIGNED_FLOOR,
|
||||||
name: hass.localize(
|
name: hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
||||||
"ui.panel.lovelace.strategy.areas.unassigned_areas"
|
|
||||||
),
|
|
||||||
level: null,
|
level: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
.map<LovelaceSectionConfig | undefined>((floor) => {
|
.map((floor) => {
|
||||||
const areasInFloors = areas.filter(
|
const areasInFloors = displayedAreas.filter(
|
||||||
(area) =>
|
(area) =>
|
||||||
area.floor_id === floor.floor_id ||
|
area.floor_id === floor.floor_id ||
|
||||||
(!area.floor_id && floor.floor_id === UNASSIGNED_FLOOR)
|
(!area.floor_id && floor.floor_id === UNASSIGNED_FLOOR)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (areasInFloors.length === 0) {
|
return [floor, areasInFloors] as const;
|
||||||
return undefined;
|
})
|
||||||
}
|
.filter(([_, areas]) => areas.length)
|
||||||
|
.map<LovelaceSectionConfig | undefined>(([floor, areas], _, array) => {
|
||||||
const areasCards = areasInFloors.map<AreaCardConfig>((area) => {
|
const areasCards = areas.map<AreaCardConfig>((area) => {
|
||||||
const path = computeAreaPath(area.area_id);
|
const path = computeAreaPath(area.area_id);
|
||||||
|
|
||||||
const areaOptions = config.areas_options?.[area.area_id] || {};
|
const areaOptions = config.areas_options?.[area.area_id] || {};
|
||||||
@ -77,49 +75,62 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
|
|||||||
.map((display) => display.hidden || [])
|
.map((display) => display.hidden || [])
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
const controls: AreaControl[] = ["light", "fan"];
|
const controls: AreaControl[] = AREA_CONTROLS.filter(
|
||||||
|
(a) => a !== "switch" // Exclude switches control for areas as we don't know what the switches control
|
||||||
|
);
|
||||||
const controlEntities = getAreaControlEntities(
|
const controlEntities = getAreaControlEntities(
|
||||||
controls,
|
controls,
|
||||||
area.area_id,
|
area.area_id,
|
||||||
hass,
|
hiddenEntities,
|
||||||
hiddenEntities
|
hass
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredControls = controls.filter(
|
const filteredControls = controls.filter(
|
||||||
(control) => controlEntities[control].length > 0
|
(control) => controlEntities[control].length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sensorClasses: string[] = [];
|
||||||
|
if (area.temperature_entity_id) {
|
||||||
|
sensorClasses.push("temperature");
|
||||||
|
}
|
||||||
|
if (area.humidity_entity_id) {
|
||||||
|
sensorClasses.push("humidity");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "area",
|
type: "area",
|
||||||
area: area.area_id,
|
area: area.area_id,
|
||||||
display_type: "compact",
|
display_type: "compact",
|
||||||
sensor_classes: ["temperature", "humidity"],
|
sensor_classes: sensorClasses,
|
||||||
alert_classes: [
|
exclude_entities: hiddenEntities,
|
||||||
"water_leak",
|
|
||||||
"smoke",
|
|
||||||
"gas",
|
|
||||||
"co",
|
|
||||||
"motion",
|
|
||||||
"occupancy",
|
|
||||||
"presence",
|
|
||||||
],
|
|
||||||
features: filteredControls.length
|
features: filteredControls.length
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
type: "area-controls",
|
type: "area-controls",
|
||||||
controls: filteredControls,
|
controls: filteredControls,
|
||||||
exclude_entities: hiddenEntities,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
|
grid_options: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 12,
|
||||||
|
},
|
||||||
|
features_position: "inline",
|
||||||
navigation_path: path,
|
navigation_path: path,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const noFloors =
|
||||||
|
array.length === 1 && floor.floor_id === UNASSIGNED_FLOOR;
|
||||||
|
|
||||||
|
const headingTitle = noFloors
|
||||||
|
? hass.localize("ui.panel.lovelace.strategy.areas.areas")
|
||||||
|
: floor.name;
|
||||||
|
|
||||||
const headingCard: HeadingCardConfig = {
|
const headingCard: HeadingCardConfig = {
|
||||||
type: "heading",
|
type: "heading",
|
||||||
heading_style: "title",
|
heading_style: "title",
|
||||||
heading: floor.name,
|
heading: headingTitle,
|
||||||
icon: floor.icon || floorDefaultIcon(floor),
|
icon: floor.icon || floorDefaultIcon(floor),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,28 +1,32 @@
|
|||||||
|
import { mdiThermometerWater } from "@mdi/js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
import "../../../../../components/ha-areas-display-editor";
|
import "../../../../../components/ha-areas-display-editor";
|
||||||
import type { AreasDisplayValue } from "../../../../../components/ha-areas-display-editor";
|
import type { AreasDisplayValue } from "../../../../../components/ha-areas-display-editor";
|
||||||
|
import "../../../../../components/ha-areas-floors-display-editor";
|
||||||
|
import type { AreasFloorsDisplayValue } from "../../../../../components/ha-areas-floors-display-editor";
|
||||||
import "../../../../../components/ha-entities-display-editor";
|
import "../../../../../components/ha-entities-display-editor";
|
||||||
|
import "../../../../../components/ha-icon";
|
||||||
import "../../../../../components/ha-icon-button";
|
import "../../../../../components/ha-icon-button";
|
||||||
import "../../../../../components/ha-icon-button-prev";
|
import "../../../../../components/ha-icon-button-prev";
|
||||||
import "../../../../../components/ha-icon";
|
import "../../../../../components/ha-svg-icon";
|
||||||
|
import {
|
||||||
|
updateAreaRegistryEntry,
|
||||||
|
type AreaRegistryEntry,
|
||||||
|
} from "../../../../../data/area_registry";
|
||||||
|
import { buttonLinkStyle } from "../../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
|
import { showAreaRegistryDetailDialog } from "../../../../config/areas/show-dialog-area-registry-detail";
|
||||||
|
import type { LovelaceStrategyEditor } from "../../types";
|
||||||
|
import type { AreasDashboardStrategyConfig } from "../areas-dashboard-strategy";
|
||||||
import type { AreaStrategyGroup } from "../helpers/areas-strategy-helper";
|
import type { AreaStrategyGroup } from "../helpers/areas-strategy-helper";
|
||||||
import {
|
import {
|
||||||
AREA_STRATEGY_GROUP_ICONS,
|
AREA_STRATEGY_GROUP_ICONS,
|
||||||
AREA_STRATEGY_GROUPS,
|
AREA_STRATEGY_GROUPS,
|
||||||
getAreaGroupedEntities,
|
getAreaGroupedEntities,
|
||||||
} from "../helpers/areas-strategy-helper";
|
} from "../helpers/areas-strategy-helper";
|
||||||
import type { LovelaceStrategyEditor } from "../../types";
|
|
||||||
import type { AreasDashboardStrategyConfig } from "../areas-dashboard-strategy";
|
|
||||||
import { showAreaRegistryDetailDialog } from "../../../../config/areas/show-dialog-area-registry-detail";
|
|
||||||
import {
|
|
||||||
updateAreaRegistryEntry,
|
|
||||||
type AreaRegistryEntry,
|
|
||||||
} from "../../../../../data/area_registry";
|
|
||||||
import { buttonLinkStyle } from "../../../../../resources/styles";
|
|
||||||
import "../../../../../components/ha-areas-floors-display-editor";
|
|
||||||
|
|
||||||
@customElement("hui-areas-dashboard-strategy-editor")
|
@customElement("hui-areas-dashboard-strategy-editor")
|
||||||
export class HuiAreasDashboardStrategyEditor
|
export class HuiAreasDashboardStrategyEditor
|
||||||
@ -58,14 +62,18 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
</div>
|
</div>
|
||||||
<ha-expansion-panel
|
<ha-expansion-panel
|
||||||
.header=${this.hass!.localize(
|
.header=${this.hass!.localize(
|
||||||
`ui.panel.lovelace.strategy.areas.header`
|
`ui.panel.lovelace.strategy.areas.sensors`
|
||||||
)}
|
)}
|
||||||
expanded
|
expanded
|
||||||
outlined
|
outlined
|
||||||
>
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="leading-icon"
|
||||||
|
.path=${mdiThermometerWater}
|
||||||
|
></ha-svg-icon>
|
||||||
<p>
|
<p>
|
||||||
${this.hass!.localize(
|
${this.hass!.localize(
|
||||||
`ui.panel.lovelace.strategy.areas.header_description`,
|
`ui.panel.lovelace.strategy.areas.sensors_description`,
|
||||||
{
|
{
|
||||||
edit_the_area: html`
|
edit_the_area: html`
|
||||||
<button class="link" @click=${this._editArea} .area=${area}>
|
<button class="link" @click=${this._editArea} .area=${area}>
|
||||||
@ -120,7 +128,7 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = this._config.areas_display;
|
const value = this._areasFloorsDisplayValue(this._config);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-areas-floors-display-editor
|
<ha-areas-floors-display-editor
|
||||||
@ -129,7 +137,7 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.lovelace.editor.strategy.areas.areas_display"
|
"ui.panel.lovelace.editor.strategy.areas.areas_display"
|
||||||
)}
|
)}
|
||||||
@value-changed=${this._areasDisplayChanged}
|
@value-changed=${this._areasFloorsDisplayChanged}
|
||||||
expanded
|
expanded
|
||||||
show-navigation-button
|
show-navigation-button
|
||||||
@item-display-navigate-clicked=${this._handleAreaNavigate}
|
@item-display-navigate-clicked=${this._handleAreaNavigate}
|
||||||
@ -143,6 +151,13 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _areasFloorsDisplayValue = memoizeOne(
|
||||||
|
(config: AreasDashboardStrategyConfig): AreasFloorsDisplayValue => ({
|
||||||
|
areas_display: config.areas_display,
|
||||||
|
floors_display: config.floors_display,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
private _editArea(ev: Event): void {
|
private _editArea(ev: Event): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const area = (ev.currentTarget! as any).area as AreaRegistryEntry;
|
const area = (ev.currentTarget! as any).area as AreaRegistryEntry;
|
||||||
@ -157,11 +172,11 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
this._area = ev.detail.value;
|
this._area = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _areasDisplayChanged(ev: CustomEvent): void {
|
private _areasFloorsDisplayChanged(ev: CustomEvent): void {
|
||||||
const value = ev.detail.value as AreasDisplayValue;
|
const value = ev.detail.value as AreasFloorsDisplayValue;
|
||||||
const newConfig: AreasDashboardStrategyConfig = {
|
const newConfig: AreasDashboardStrategyConfig = {
|
||||||
...this._config!,
|
...this._config!,
|
||||||
areas_display: value,
|
...value,
|
||||||
};
|
};
|
||||||
|
|
||||||
fireEvent(this, "config-changed", { config: newConfig });
|
fireEvent(this, "config-changed", { config: newConfig });
|
||||||
@ -213,9 +228,13 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
ha-expansion-panel {
|
ha-expansion-panel {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
|
--expansion-panel-summary-padding: 0 16px;
|
||||||
|
}
|
||||||
|
ha-expansion-panel [slot="leading-icon"] {
|
||||||
|
margin-inline-end: 16px;
|
||||||
}
|
}
|
||||||
ha-expansion-panel p {
|
ha-expansion-panel p {
|
||||||
margin: 8px 2px;
|
margin: 8px 8px 16px 8px;
|
||||||
}
|
}
|
||||||
button.link {
|
button.link {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
@ -3,9 +3,13 @@ import { computeStateName } from "../../../../../common/entity/compute_state_nam
|
|||||||
import type { EntityFilterFunc } from "../../../../../common/entity/entity_filter";
|
import type { EntityFilterFunc } from "../../../../../common/entity/entity_filter";
|
||||||
import { generateEntityFilter } from "../../../../../common/entity/entity_filter";
|
import { generateEntityFilter } from "../../../../../common/entity/entity_filter";
|
||||||
import { stripPrefixFromEntityName } from "../../../../../common/entity/strip_prefix_from_entity_name";
|
import { stripPrefixFromEntityName } from "../../../../../common/entity/strip_prefix_from_entity_name";
|
||||||
import { orderCompare } from "../../../../../common/string/compare";
|
import {
|
||||||
|
orderCompare,
|
||||||
|
stringCompare,
|
||||||
|
} from "../../../../../common/string/compare";
|
||||||
import type { AreaRegistryEntry } from "../../../../../data/area_registry";
|
import type { AreaRegistryEntry } from "../../../../../data/area_registry";
|
||||||
import { areaCompare } from "../../../../../data/area_registry";
|
import { areaCompare } from "../../../../../data/area_registry";
|
||||||
|
import type { FloorRegistryEntry } from "../../../../../data/floor_registry";
|
||||||
import type { LovelaceCardConfig } from "../../../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../../../data/lovelace/config/card";
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
import { supportsAlarmModesCardFeature } from "../../../card-features/hui-alarm-modes-card-feature";
|
import { supportsAlarmModesCardFeature } from "../../../card-features/hui-alarm-modes-card-feature";
|
||||||
@ -290,4 +294,23 @@ export const getAreas = (
|
|||||||
return sortedAreas;
|
return sortedAreas;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFloors = (
|
||||||
|
entries: HomeAssistant["floors"],
|
||||||
|
floorsOrder?: string[]
|
||||||
|
): FloorRegistryEntry[] => {
|
||||||
|
const floors = Object.values(entries);
|
||||||
|
const compare = orderCompare(floorsOrder || []);
|
||||||
|
|
||||||
|
return floors.sort((floorA, floorB) => {
|
||||||
|
const order = compare(floorA.floor_id, floorB.floor_id);
|
||||||
|
if (order !== 0) {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
if (floorA.level !== floorB.level) {
|
||||||
|
return (floorA.level ?? 0) - (floorB.level ?? 0);
|
||||||
|
}
|
||||||
|
return stringCompare(floorA.name, floorB.name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const computeAreaPath = (areaId: string): string => `areas-${areaId}`;
|
export const computeAreaPath = (areaId: string): string => `areas-${areaId}`;
|
||||||
|
@ -13,6 +13,7 @@ import type { Constructor, HomeAssistant } from "../../types";
|
|||||||
import type {
|
import type {
|
||||||
LovelaceCardFeatureConfig,
|
LovelaceCardFeatureConfig,
|
||||||
LovelaceCardFeatureContext,
|
LovelaceCardFeatureContext,
|
||||||
|
LovelaceCardFeaturePosition,
|
||||||
} from "./card-features/types";
|
} from "./card-features/types";
|
||||||
import type { LovelaceElement, LovelaceElementConfig } from "./elements/types";
|
import type { LovelaceElement, LovelaceElementConfig } from "./elements/types";
|
||||||
import type { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types";
|
import type { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types";
|
||||||
@ -179,6 +180,7 @@ export interface LovelaceCardFeature extends HTMLElement {
|
|||||||
context?: LovelaceCardFeatureContext;
|
context?: LovelaceCardFeatureContext;
|
||||||
setConfig(config: LovelaceCardFeatureConfig);
|
setConfig(config: LovelaceCardFeatureConfig);
|
||||||
color?: string;
|
color?: string;
|
||||||
|
position?: LovelaceCardFeaturePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LovelaceCardFeatureConstructor
|
export interface LovelaceCardFeatureConstructor
|
||||||
|
@ -10,8 +10,8 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
|||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import type { HuiCard } from "../cards/hui-card";
|
import type { HuiCard } from "../cards/hui-card";
|
||||||
import type { HuiCardOptions } from "../components/hui-card-options";
|
import type { HuiCardOptions } from "../components/hui-card-options";
|
||||||
import type { HuiWarning } from "../components/hui-warning";
|
|
||||||
import type { Lovelace } from "../types";
|
import type { Lovelace } from "../types";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
|
||||||
let editCodeLoaded = false;
|
let editCodeLoaded = false;
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public cards: HuiCard[] = [];
|
@property({ attribute: false }) public cards: HuiCard[] = [];
|
||||||
|
|
||||||
@state() private _card?: HuiCard | HuiWarning | HuiCardOptions;
|
@state() private _card?: HuiCard | HuiCardOptions;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
public setConfig(_config: LovelaceViewConfig): void {}
|
public setConfig(_config: LovelaceViewConfig): void {}
|
||||||
@ -63,11 +63,11 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
|||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
${this.cards!.length > 1
|
${this.cards!.length > 1
|
||||||
? html`<hui-warning .hass=${this.hass}>
|
? html`<ha-alert alert-type="warning"
|
||||||
${this.hass!.localize(
|
>${this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.view.panel_mode.warning_multiple_cards"
|
"ui.panel.lovelace.editor.view.panel_mode.warning_multiple_cards"
|
||||||
)}
|
)}</ha-alert
|
||||||
</hui-warning>`
|
>`
|
||||||
: ""}
|
: ""}
|
||||||
${this._card}
|
${this._card}
|
||||||
${this.lovelace?.editMode && this.cards.length === 0
|
${this.lovelace?.editMode && this.cards.length === 0
|
||||||
|
@ -51,7 +51,7 @@ export const haTheme = EditorView.theme({
|
|||||||
"&": {
|
"&": {
|
||||||
color: "var(--primary-text-color)",
|
color: "var(--primary-text-color)",
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
"var(--code-editor-background-color, var(--mdc-text-field-fill-color, whitesmoke))",
|
"var(--code-editor-background-color, var(--card-background-color))",
|
||||||
borderRadius:
|
borderRadius:
|
||||||
"var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0px 0px",
|
"var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0px 0px",
|
||||||
caretColor: "var(--secondary-text-color)",
|
caretColor: "var(--secondary-text-color)",
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
"owner": "Owner",
|
"owner": "Owner",
|
||||||
"system-admin": "Administrators",
|
"system-admin": "Administrators",
|
||||||
"system-users": "Users",
|
"system-users": "Users",
|
||||||
"system-read-only": "Read-Only Users"
|
"system-read-only": "Read-only users"
|
||||||
},
|
},
|
||||||
"config_entry": {
|
"config_entry": {
|
||||||
"disabled_by": {
|
"disabled_by": {
|
||||||
@ -325,6 +325,62 @@
|
|||||||
"low": "Low"
|
"low": "Low"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"card_features": {
|
||||||
|
"area_controls": {
|
||||||
|
"light": {
|
||||||
|
"on": "Turn on area lights",
|
||||||
|
"off": "Turn off area lights"
|
||||||
|
},
|
||||||
|
"fan": {
|
||||||
|
"on": "Turn on area fans",
|
||||||
|
"off": "Turn off area fans"
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"on": "Turn on area switches",
|
||||||
|
"off": "Turn off area switches"
|
||||||
|
},
|
||||||
|
"cover-awning": {
|
||||||
|
"on": "Open area awnings",
|
||||||
|
"off": "Close area awnings"
|
||||||
|
},
|
||||||
|
"cover-blind": {
|
||||||
|
"on": "Open area blinds",
|
||||||
|
"off": "Close area blinds"
|
||||||
|
},
|
||||||
|
"cover-curtain": {
|
||||||
|
"on": "Open area curtains",
|
||||||
|
"off": "Close area curtains"
|
||||||
|
},
|
||||||
|
"cover-damper": {
|
||||||
|
"on": "Open area dampers",
|
||||||
|
"off": "Close area dampers"
|
||||||
|
},
|
||||||
|
"cover-door": {
|
||||||
|
"on": "Open area doors",
|
||||||
|
"off": "Close area doors"
|
||||||
|
},
|
||||||
|
"cover-garage": {
|
||||||
|
"on": "Open garage door",
|
||||||
|
"off": "Close garage door"
|
||||||
|
},
|
||||||
|
"cover-gate": {
|
||||||
|
"on": "Open area gates",
|
||||||
|
"off": "Close area gates"
|
||||||
|
},
|
||||||
|
"cover-shade": {
|
||||||
|
"on": "Open area shades",
|
||||||
|
"off": "Close area shades"
|
||||||
|
},
|
||||||
|
"cover-shutter": {
|
||||||
|
"on": "Open area shutters",
|
||||||
|
"off": "Close area shutters"
|
||||||
|
},
|
||||||
|
"cover-window": {
|
||||||
|
"on": "Open area windows",
|
||||||
|
"off": "Close area windows"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
@ -383,6 +439,7 @@
|
|||||||
"markdown": "Markdown",
|
"markdown": "Markdown",
|
||||||
"suggest_ai": "Suggest with AI"
|
"suggest_ai": "Suggest with AI"
|
||||||
},
|
},
|
||||||
|
|
||||||
"components": {
|
"components": {
|
||||||
"selectors": {
|
"selectors": {
|
||||||
"media": {
|
"media": {
|
||||||
@ -414,7 +471,7 @@
|
|||||||
"radius_meters": "[%key:ui::panel::config::core::section::core::core_config::elevation_meters%]"
|
"radius_meters": "[%key:ui::panel::config::core::section::core::core_config::elevation_meters%]"
|
||||||
},
|
},
|
||||||
"selector": {
|
"selector": {
|
||||||
"options": "Selector Options",
|
"options": "Selector options",
|
||||||
"types": {
|
"types": {
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
"area": "Area",
|
"area": "Area",
|
||||||
@ -423,7 +480,7 @@
|
|||||||
"color_temp": "Color temperature",
|
"color_temp": "Color temperature",
|
||||||
"condition": "Condition",
|
"condition": "Condition",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"datetime": "Date and Time",
|
"datetime": "Date and time",
|
||||||
"device": "Device",
|
"device": "Device",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"entity": "Entity",
|
"entity": "Entity",
|
||||||
@ -433,7 +490,7 @@
|
|||||||
"media": "Media",
|
"media": "Media",
|
||||||
"number": "Number",
|
"number": "Number",
|
||||||
"object": "Object",
|
"object": "Object",
|
||||||
"color_rgb": "RGB Color",
|
"color_rgb": "RGB color",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"state": "State",
|
"state": "State",
|
||||||
"target": "Target",
|
"target": "Target",
|
||||||
@ -441,7 +498,7 @@
|
|||||||
"text": "Text",
|
"text": "Text",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"time": "Time",
|
"time": "Time",
|
||||||
"manual": "Manual Entry"
|
"manual": "Manual entry"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"template": {
|
"template": {
|
||||||
@ -800,7 +857,7 @@
|
|||||||
"cyan": "Cyan",
|
"cyan": "Cyan",
|
||||||
"teal": "Teal",
|
"teal": "Teal",
|
||||||
"green": "Green",
|
"green": "Green",
|
||||||
"light-green": "Light Green",
|
"light-green": "Light green",
|
||||||
"lime": "Lime",
|
"lime": "Lime",
|
||||||
"yellow": "Yellow",
|
"yellow": "Yellow",
|
||||||
"amber": "Amber",
|
"amber": "Amber",
|
||||||
@ -988,7 +1045,7 @@
|
|||||||
"podcast": "Podcast",
|
"podcast": "Podcast",
|
||||||
"season": "Season",
|
"season": "Season",
|
||||||
"track": "Track",
|
"track": "Track",
|
||||||
"tv_show": "TV Show",
|
"tv_show": "TV show",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
@ -2878,7 +2935,7 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"tag_id": "Tag ID",
|
"tag_id": "Tag ID",
|
||||||
"tag_id_placeholder": "Autogenerated if left empty",
|
"tag_id_placeholder": "Auto-generated if left empty",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
@ -4146,13 +4203,13 @@
|
|||||||
"mode_after": "[%key:ui::panel::config::automation::editor::conditions::type::time::after%]",
|
"mode_after": "[%key:ui::panel::config::automation::editor::conditions::type::time::after%]",
|
||||||
"mode_before": "[%key:ui::panel::config::automation::editor::conditions::type::time::before%]",
|
"mode_before": "[%key:ui::panel::config::automation::editor::conditions::type::time::before%]",
|
||||||
"weekdays": {
|
"weekdays": {
|
||||||
"mon": "Monday",
|
"mon": "[%key:ui::weekdays::monday%]",
|
||||||
"tue": "Tuesday",
|
"tue": "[%key:ui::weekdays::tuesday%]",
|
||||||
"wed": "Wednesday",
|
"wed": "[%key:ui::weekdays::wednesday%]",
|
||||||
"thu": "Thursday",
|
"thu": "[%key:ui::weekdays::thursday%]",
|
||||||
"fri": "Friday",
|
"fri": "[%key:ui::weekdays::friday%]",
|
||||||
"sat": "Saturday",
|
"sat": "[%key:ui::weekdays::saturday%]",
|
||||||
"sun": "Sunday"
|
"sun": "[%key:ui::weekdays::sunday%]"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"picker": "If the current time is before or after a specified time.",
|
"picker": "If the current time is before or after a specified time.",
|
||||||
@ -4443,14 +4500,14 @@
|
|||||||
"trace_no_longer_available": "Chosen trace is no longer available",
|
"trace_no_longer_available": "Chosen trace is no longer available",
|
||||||
"enter_downloaded_trace": "Enter downloaded trace",
|
"enter_downloaded_trace": "Enter downloaded trace",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"details": "Step Details",
|
"details": "Step details",
|
||||||
"timeline": "Trace Timeline",
|
"timeline": "Trace timeline",
|
||||||
"logbook": "Related logbook entries",
|
"logbook": "Related logbook entries",
|
||||||
"automation_config": "Automation Config",
|
"automation_config": "Automation config",
|
||||||
"step_config": "Step Config",
|
"step_config": "Step config",
|
||||||
"changed_variables": "Changed Variables",
|
"changed_variables": "Changed variables",
|
||||||
"blueprint_config": "Blueprint Config",
|
"blueprint_config": "Blueprint config",
|
||||||
"script_config": "Script Config"
|
"script_config": "Script config"
|
||||||
},
|
},
|
||||||
"path": {
|
"path": {
|
||||||
"choose": "Select a step on the left for more information.",
|
"choose": "Select a step on the left for more information.",
|
||||||
@ -4497,7 +4554,7 @@
|
|||||||
"caption": "Blueprints",
|
"caption": "Blueprints",
|
||||||
"description": "Manage blueprints",
|
"description": "Manage blueprints",
|
||||||
"overview": {
|
"overview": {
|
||||||
"header": "Blueprint Editor",
|
"header": "Blueprint editor",
|
||||||
"introduction": "The blueprint configuration allows you to import and manage your blueprints.",
|
"introduction": "The blueprint configuration allows you to import and manage your blueprints.",
|
||||||
"learn_more": "Learn more about using blueprints",
|
"learn_more": "Learn more about using blueprints",
|
||||||
"headers": {
|
"headers": {
|
||||||
@ -4554,14 +4611,14 @@
|
|||||||
"override_description": "Importing it will override the existing blueprint. If the updated blueprint is not compatible, it can break your automations. Automations will have to be adjusted manually.",
|
"override_description": "Importing it will override the existing blueprint. If the updated blueprint is not compatible, it can break your automations. Automations will have to be adjusted manually.",
|
||||||
"error_no_url": "Please enter the blueprint address.",
|
"error_no_url": "Please enter the blueprint address.",
|
||||||
"unsupported_blueprint": "This blueprint is not supported",
|
"unsupported_blueprint": "This blueprint is not supported",
|
||||||
"file_name": "Blueprint Path"
|
"file_name": "Blueprint path"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"script": {
|
"script": {
|
||||||
"caption": "Scripts",
|
"caption": "Scripts",
|
||||||
"description": "Execute a sequence of actions",
|
"description": "Execute a sequence of actions",
|
||||||
"picker": {
|
"picker": {
|
||||||
"header": "Script Editor",
|
"header": "Script editor",
|
||||||
"introduction": "The script editor allows you to create and edit scripts. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
|
"introduction": "The script editor allows you to create and edit scripts. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
|
||||||
"learn_more": "Learn more about scripts",
|
"learn_more": "Learn more about scripts",
|
||||||
"no_scripts": "We couldn't find any scripts",
|
"no_scripts": "We couldn't find any scripts",
|
||||||
@ -4637,7 +4694,7 @@
|
|||||||
"field_delete_confirm_title": "Delete field?",
|
"field_delete_confirm_title": "Delete field?",
|
||||||
"field_delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]",
|
"field_delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]",
|
||||||
"header": "Script: {name}",
|
"header": "Script: {name}",
|
||||||
"default_name": "New Script",
|
"default_name": "New script",
|
||||||
"modes": {
|
"modes": {
|
||||||
"label": "[%key:ui::panel::config::automation::editor::modes::label%]",
|
"label": "[%key:ui::panel::config::automation::editor::modes::label%]",
|
||||||
"learn_more": "[%key:ui::panel::config::automation::editor::modes::learn_more%]",
|
"learn_more": "[%key:ui::panel::config::automation::editor::modes::learn_more%]",
|
||||||
@ -4682,7 +4739,7 @@
|
|||||||
"description": "Capture device states and easily recall them later",
|
"description": "Capture device states and easily recall them later",
|
||||||
"activated": "Activated scene {name}.",
|
"activated": "Activated scene {name}.",
|
||||||
"picker": {
|
"picker": {
|
||||||
"header": "Scene Editor",
|
"header": "Scene editor",
|
||||||
"introduction": "The scene editor allows you to create and edit scenes. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
|
"introduction": "The scene editor allows you to create and edit scenes. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
|
||||||
"learn_more": "Learn more about scenes",
|
"learn_more": "Learn more about scenes",
|
||||||
"pick_scene": "Pick scene to edit",
|
"pick_scene": "Pick scene to edit",
|
||||||
@ -4946,7 +5003,7 @@
|
|||||||
"other_home_assistant": "Other Home Assistant",
|
"other_home_assistant": "Other Home Assistant",
|
||||||
"instance_name": "Instance name",
|
"instance_name": "Instance name",
|
||||||
"instance_version": "Instance version",
|
"instance_version": "Instance version",
|
||||||
"ip_address": "IP Address",
|
"ip_address": "IP address",
|
||||||
"connected_at": "Connected at",
|
"connected_at": "Connected at",
|
||||||
"obfuscated_ip": {
|
"obfuscated_ip": {
|
||||||
"show": "Show IP address",
|
"show": "Show IP address",
|
||||||
@ -5081,7 +5138,8 @@
|
|||||||
"disabled_entities": "+{count} disabled {count, plural,\n one {entity}\n other {entities}\n}",
|
"disabled_entities": "+{count} disabled {count, plural,\n one {entity}\n other {entities}\n}",
|
||||||
"hidden": "Hidden"
|
"hidden": "Hidden"
|
||||||
},
|
},
|
||||||
"confirm_disable_config_entry": "There are no more devices for the config entry {entry_name}, do you want to instead disable the config entry?",
|
"confirm_disable_config_entry_title": "Disable config entry?",
|
||||||
|
"confirm_disable_config_entry_message": "There are no more devices for the config entry {name}, do you want to disable the config entry instead?",
|
||||||
"update_device_error": "Updating the device failed",
|
"update_device_error": "Updating the device failed",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"data_table": {
|
"data_table": {
|
||||||
@ -5332,7 +5390,10 @@
|
|||||||
"device": {
|
"device": {
|
||||||
"enable": "Enable device",
|
"enable": "Enable device",
|
||||||
"disable": "Disable device",
|
"disable": "Disable device",
|
||||||
|
"confirm_disable_title": "Disable device?",
|
||||||
|
"confirm_disable_message": "Are you sure you want to disable {name} and all of its entities?",
|
||||||
"configure": "Configure device",
|
"configure": "Configure device",
|
||||||
|
"edit": "Edit device",
|
||||||
"delete": "Remove device"
|
"delete": "Remove device"
|
||||||
},
|
},
|
||||||
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
|
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
|
||||||
@ -5366,7 +5427,7 @@
|
|||||||
"via": "Connected via",
|
"via": "Connected via",
|
||||||
"firmware": "Firmware: {version}",
|
"firmware": "Firmware: {version}",
|
||||||
"hardware": "Hardware: {version}",
|
"hardware": "Hardware: {version}",
|
||||||
"version": "Version: {version}",
|
"version": "Version {version}",
|
||||||
"serial_number": "Serial number: {serial_number}",
|
"serial_number": "Serial number: {serial_number}",
|
||||||
"unnamed_entry": "Unnamed entry",
|
"unnamed_entry": "Unnamed entry",
|
||||||
"unknown_via_device": "Unknown device",
|
"unknown_via_device": "Unknown device",
|
||||||
@ -5385,7 +5446,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"custom_integration": "Custom integration",
|
"custom_integration": "Custom integration",
|
||||||
"internal_integration": "Internal integration",
|
|
||||||
"legacy_integration": "Legacy integration",
|
"legacy_integration": "Legacy integration",
|
||||||
"custom_overwrites_core": "Custom integration that replaces a core component",
|
"custom_overwrites_core": "Custom integration that replaces a core component",
|
||||||
"depends_on_cloud": "Requires Internet",
|
"depends_on_cloud": "Requires Internet",
|
||||||
@ -5612,9 +5672,9 @@
|
|||||||
},
|
},
|
||||||
"dhcp": {
|
"dhcp": {
|
||||||
"title": "DHCP discovery",
|
"title": "DHCP discovery",
|
||||||
"mac_address": "MAC Address",
|
"mac_address": "MAC address",
|
||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"ip_address": "IP Address",
|
"ip_address": "IP address",
|
||||||
"no_devices_found": "No recent DHCP requests found; no matching discoveries detected"
|
"no_devices_found": "No recent DHCP requests found; no matching discoveries detected"
|
||||||
},
|
},
|
||||||
"thread": {
|
"thread": {
|
||||||
@ -5673,7 +5733,7 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"ip_addresses": "IP Addresses",
|
"ip_addresses": "IP addresses",
|
||||||
"properties": "Properties",
|
"properties": "Properties",
|
||||||
"discovery_information": "Discovery information",
|
"discovery_information": "Discovery information",
|
||||||
"copy_to_clipboard": "Copy to clipboard",
|
"copy_to_clipboard": "Copy to clipboard",
|
||||||
@ -5840,7 +5900,7 @@
|
|||||||
"not_ready": "{count} not ready",
|
"not_ready": "{count} not ready",
|
||||||
"nvm_backup": {
|
"nvm_backup": {
|
||||||
"title": "Backup and restore",
|
"title": "Backup and restore",
|
||||||
"description": "Back up or restore your Z-Wave controller's non-volatile memory (NVM). The NVM contains your network information including paired devices. It's recommended to create a backup before making any major changes to your Z-Wave network.",
|
"description": "Back up or restore your Z-Wave adapter's non-volatile memory (NVM). The NVM contains your network information including paired devices. It's recommended to create a backup before making any major changes to your Z-Wave network.",
|
||||||
"download_backup": "Download backup",
|
"download_backup": "Download backup",
|
||||||
"restore_backup": "Restore from backup",
|
"restore_backup": "Restore from backup",
|
||||||
"backup_failed": "Failed to download backup",
|
"backup_failed": "Failed to download backup",
|
||||||
@ -5848,21 +5908,21 @@
|
|||||||
"restore_failed": "Failed to restore backup",
|
"restore_failed": "Failed to restore backup",
|
||||||
"creating": "Creating backup",
|
"creating": "Creating backup",
|
||||||
"restoring": "Restoring backup",
|
"restoring": "Restoring backup",
|
||||||
"migrate": "Migrate controller"
|
"migrate": "Migrate adapter"
|
||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"title": "Controller statistics",
|
"title": "Adapter statistics",
|
||||||
"messages_tx": {
|
"messages_tx": {
|
||||||
"label": "Messages TX",
|
"label": "Messages TX",
|
||||||
"tooltip": "Number of messages successfully sent to the controller"
|
"tooltip": "Number of messages successfully sent to the adapter"
|
||||||
},
|
},
|
||||||
"messages_rx": {
|
"messages_rx": {
|
||||||
"label": "Messages RX",
|
"label": "Messages RX",
|
||||||
"tooltip": "Number of messages successfully received by the controller"
|
"tooltip": "Number of messages successfully received by the adapter"
|
||||||
},
|
},
|
||||||
"messages_dropped_tx": {
|
"messages_dropped_tx": {
|
||||||
"label": "Dropped messages TX",
|
"label": "Dropped messages TX",
|
||||||
"tooltip": "Number of messages from the controller that were dropped by the host"
|
"tooltip": "Number of messages from the adapter that were dropped by the host"
|
||||||
},
|
},
|
||||||
"messages_dropped_rx": {
|
"messages_dropped_rx": {
|
||||||
"label": "Dropped messages RX",
|
"label": "Dropped messages RX",
|
||||||
@ -5870,23 +5930,23 @@
|
|||||||
},
|
},
|
||||||
"nak": {
|
"nak": {
|
||||||
"label": "NAK",
|
"label": "NAK",
|
||||||
"tooltip": "Number of messages that the controller did not accept"
|
"tooltip": "Number of messages that the adapter did not accept"
|
||||||
},
|
},
|
||||||
"can": {
|
"can": {
|
||||||
"label": "CAN",
|
"label": "CAN",
|
||||||
"tooltip": "Number of collisions while sending a message to the controller"
|
"tooltip": "Number of collisions while sending a message to the adapter"
|
||||||
},
|
},
|
||||||
"timeout_ack": {
|
"timeout_ack": {
|
||||||
"label": "Timeout ACK",
|
"label": "Timeout ACK",
|
||||||
"tooltip": "Number of transmission attempts where an ACK was missing from the controller"
|
"tooltip": "Number of transmission attempts where an ACK was missing from the adapter"
|
||||||
},
|
},
|
||||||
"timeout_response": {
|
"timeout_response": {
|
||||||
"label": "Timeout response",
|
"label": "Timeout response",
|
||||||
"tooltip": "Number of transmission attempts where the controller response did not come in time"
|
"tooltip": "Number of transmission attempts where the adapter response did not come in time"
|
||||||
},
|
},
|
||||||
"timeout_callback": {
|
"timeout_callback": {
|
||||||
"label": "Timeout callback",
|
"label": "Timeout callback",
|
||||||
"tooltip": "Number of transmission attempts where the controller callback did not come in time"
|
"tooltip": "Number of transmission attempts where the adapter callback did not come in time"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -5909,18 +5969,18 @@
|
|||||||
},
|
},
|
||||||
"hard_reset_controller": {
|
"hard_reset_controller": {
|
||||||
"NotStarted": {
|
"NotStarted": {
|
||||||
"title": "Reset controller to factory settings",
|
"title": "Reset adapter to factory settings",
|
||||||
"body": "If you decide to move forward, you will reset your controller to factory settings. As a result, the controller will forget all devices it is paired with and all Z-Wave devices for this network will be removed from Home Assistant. If there are any devices still paired with the controller when it is reset, they will have to go through the exclusion process before they can be re-paired. Would you like to continue?"
|
"body": "If you decide to move forward, you will reset your adapter to factory settings. As a result, the adapter will forget all devices it is paired with and all Z-Wave devices for this network will be removed from Home Assistant. If there are any devices still paired with the adapter when it is reset, they will have to go through the exclusion process before they can be re-paired. Would you like to continue?"
|
||||||
},
|
},
|
||||||
"InProgress": {
|
"InProgress": {
|
||||||
"title": "Resetting controller",
|
"title": "Resetting adapter",
|
||||||
"body": "Your controller is being reset and restarted. Wait until the process is complete before closing this dialog"
|
"body": "Your adapter is being reset and restarted. Wait until the process is complete before closing this dialog"
|
||||||
},
|
},
|
||||||
"Done": {
|
"Done": {
|
||||||
"title": "Controller reset complete",
|
"title": "Adapter reset complete",
|
||||||
"body": "Your controller has been reset to factory settings and has been restarted! You can now close this dialog."
|
"body": "Your adapter has been reset to factory settings and has been restarted! You can now close this dialog."
|
||||||
},
|
},
|
||||||
"confirmation": "This action cannot be undone unless you have an NVM backup from your controller."
|
"confirmation": "This action cannot be undone unless you have a backup from your adapter."
|
||||||
},
|
},
|
||||||
"node_statistics": {
|
"node_statistics": {
|
||||||
"title": "Device statistics",
|
"title": "Device statistics",
|
||||||
@ -5985,7 +6045,7 @@
|
|||||||
},
|
},
|
||||||
"rssi": {
|
"rssi": {
|
||||||
"label": "RSSI",
|
"label": "RSSI",
|
||||||
"tooltip": "The RSSI of the ACK frame received by the controller"
|
"tooltip": "The RSSI of the ACK frame received by the adapter"
|
||||||
},
|
},
|
||||||
"route_failed_between": {
|
"route_failed_between": {
|
||||||
"label": "Route failed between",
|
"label": "Route failed between",
|
||||||
@ -6204,7 +6264,7 @@
|
|||||||
},
|
},
|
||||||
"rebuild_node_routes": {
|
"rebuild_node_routes": {
|
||||||
"title": "Rebuild routes for a Z-Wave device",
|
"title": "Rebuild routes for a Z-Wave device",
|
||||||
"introduction": "Tell {device} to update its routes back to the controller. This can help with communication issues if you have recently moved the device or your controller.",
|
"introduction": "Assign new routes between {device} and the adapter. This can help with communication issues if you have recently moved the device or your adapter.",
|
||||||
"traffic_warning": "The route rebuilding process generates a large amount of traffic on the Z-Wave network. This may cause devices to respond slowly (or not at all) while the rebuilding is in progress.",
|
"traffic_warning": "The route rebuilding process generates a large amount of traffic on the Z-Wave network. This may cause devices to respond slowly (or not at all) while the rebuilding is in progress.",
|
||||||
"start_rebuilding_routes": "Rebuild Routes for Device",
|
"start_rebuilding_routes": "Rebuild Routes for Device",
|
||||||
"rebuilding_routes_failed": "{device} routes could not be rebuild.",
|
"rebuilding_routes_failed": "{device} routes could not be rebuild.",
|
||||||
@ -6216,7 +6276,7 @@
|
|||||||
"update_firmware": {
|
"update_firmware": {
|
||||||
"title": "Update device firmware",
|
"title": "Update device firmware",
|
||||||
"warning": "WARNING: Firmware updates can brick your device if you do not correctly follow the manufacturer's guidance. The Home Assistant and Z-Wave JS teams do not take any responsibility for any damages to your device as a result of the firmware update and will not be able to help you if you brick your device. Would you still like to continue?",
|
"warning": "WARNING: Firmware updates can brick your device if you do not correctly follow the manufacturer's guidance. The Home Assistant and Z-Wave JS teams do not take any responsibility for any damages to your device as a result of the firmware update and will not be able to help you if you brick your device. Would you still like to continue?",
|
||||||
"warning_controller": "WARNING: Firmware updates can brick your controller if you do not use the right firmware files, or if you attempt to stop the firmware update before it completes. The Home Assistant and Z-Wave JS teams do not take any responsibility for any damages to your controller as a result of the firmware update and will not be able to help you if you brick your controller. Would you still like to continue?",
|
"warning_controller": "WARNING: Firmware updates can brick your adapter if you do not use the right firmware files, or if you attempt to stop the firmware update before it completes. The Home Assistant and Z-Wave JS teams do not take any responsibility for any damages to your adapter as a result of the firmware update and will not be able to help you if you brick your adapter. Would you still like to continue?",
|
||||||
"introduction": "Select the firmware file you would like to use to update {device}.",
|
"introduction": "Select the firmware file you would like to use to update {device}.",
|
||||||
"introduction_controller": "Select the firmware file you would like to use to update {device}. Note that once you start a firmware update, you MUST wait for the update to complete.",
|
"introduction_controller": "Select the firmware file you would like to use to update {device}. Note that once you start a firmware update, you MUST wait for the update to complete.",
|
||||||
"firmware_target_intro": "Select the firmware target (0 for the Z-Wave chip, ≥1 for other chips if they exist) for this update.",
|
"firmware_target_intro": "Select the firmware target (0 for the Z-Wave chip, ≥1 for other chips if they exist) for this update.",
|
||||||
@ -6237,7 +6297,7 @@
|
|||||||
"error": "Unable to update firmware on {device}: {message}.",
|
"error": "Unable to update firmware on {device}: {message}.",
|
||||||
"try_again": "To attempt the firmware update again, select the new firmware file you would like to use.",
|
"try_again": "To attempt the firmware update again, select the new firmware file you would like to use.",
|
||||||
"done": "The firmware update is complete! If you want to attempt another firmware update on this device, please wait until it gets re-interviewed.",
|
"done": "The firmware update is complete! If you want to attempt another firmware update on this device, please wait until it gets re-interviewed.",
|
||||||
"done_controller": "The firmware update is complete! Your controller is being restarted and your network will temporarily be unavailable.",
|
"done_controller": "The firmware update is complete! Your adapter is being restarted and your network will temporarily be unavailable.",
|
||||||
"Error_Timeout": "Timed out",
|
"Error_Timeout": "Timed out",
|
||||||
"Error_Checksum": "Checksum error",
|
"Error_Checksum": "Checksum error",
|
||||||
"Error_TransmissionFailed": "Transmission failed",
|
"Error_TransmissionFailed": "Transmission failed",
|
||||||
@ -6305,7 +6365,7 @@
|
|||||||
"title": "Door lock",
|
"title": "Door lock",
|
||||||
"twist_assist": "Twist assist",
|
"twist_assist": "Twist assist",
|
||||||
"block_to_block": "Block to block",
|
"block_to_block": "Block to block",
|
||||||
"auto_relock_time": "Auto relock time",
|
"auto_relock_time": "Autorelock time",
|
||||||
"hold_release_time": "Hold and release time",
|
"hold_release_time": "Hold and release time",
|
||||||
"operation_type": "Operation type",
|
"operation_type": "Operation type",
|
||||||
"operation_types": {
|
"operation_types": {
|
||||||
@ -6476,13 +6536,13 @@
|
|||||||
"prefix": "Subnet prefix",
|
"prefix": "Subnet prefix",
|
||||||
"add_address": "Add address",
|
"add_address": "Add address",
|
||||||
"gateway": "Gateway address",
|
"gateway": "Gateway address",
|
||||||
"dns_server": "DNS Server",
|
"dns_server": "DNS server",
|
||||||
"add_dns_server": "Add DNS Server",
|
"add_dns_server": "Add DNS server",
|
||||||
"custom_dns": "Custom",
|
"custom_dns": "Custom",
|
||||||
"unsaved": "You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
|
"unsaved": "You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
|
||||||
"failed_to_change": "Failed to change network settings",
|
"failed_to_change": "Failed to change network settings",
|
||||||
"hostname": {
|
"hostname": {
|
||||||
"title": "Host name",
|
"title": "Hostname",
|
||||||
"description": "The name your instance will have on your network",
|
"description": "The name your instance will have on your network",
|
||||||
"failed_to_set_hostname": "Setting hostname failed"
|
"failed_to_set_hostname": "Setting hostname failed"
|
||||||
}
|
}
|
||||||
@ -6499,9 +6559,9 @@
|
|||||||
},
|
},
|
||||||
"network_adapter": "Network adapter",
|
"network_adapter": "Network adapter",
|
||||||
"network_adapter_info": "Configure which network adapters integrations will use. A restart is required for these settings to apply.",
|
"network_adapter_info": "Configure which network adapters integrations will use. A restart is required for these settings to apply.",
|
||||||
"ip_information": "IP Information",
|
"ip_information": "IP information",
|
||||||
"adapter": {
|
"adapter": {
|
||||||
"auto_configure": "Auto configure",
|
"auto_configure": "Autoconfigure",
|
||||||
"detected": "Detected",
|
"detected": "Detected",
|
||||||
"adapter": "Adapter"
|
"adapter": "Adapter"
|
||||||
}
|
}
|
||||||
@ -6510,7 +6570,7 @@
|
|||||||
"caption": "Storage",
|
"caption": "Storage",
|
||||||
"description": "{percent_used} used - {free_space} free",
|
"description": "{percent_used} used - {free_space} free",
|
||||||
"used_space": "Used space",
|
"used_space": "Used space",
|
||||||
"emmc_lifetime_used": "eMMC Lifetime Used",
|
"emmc_lifetime_used": "eMMC lifetime used",
|
||||||
"disk_metrics": "Disk metrics",
|
"disk_metrics": "Disk metrics",
|
||||||
"datadisk": {
|
"datadisk": {
|
||||||
"title": "Move data disk",
|
"title": "Move data disk",
|
||||||
@ -6624,8 +6684,8 @@
|
|||||||
},
|
},
|
||||||
"areas": {
|
"areas": {
|
||||||
"no_entities": "No entities in this area.",
|
"no_entities": "No entities in this area.",
|
||||||
"header": "Area badges",
|
"sensors": "Sensors",
|
||||||
"header_description": "To display temperature and humidity sensors in the overview and in the area view, add a sensor to that area and {edit_the_area} to configure related sensors.",
|
"sensors_description": "To display temperature and humidity sensors in the overview and in the area view, add a sensor to that area and {edit_the_area} to configure related sensors.",
|
||||||
"edit_the_area": "edit the area",
|
"edit_the_area": "edit the area",
|
||||||
"groups": {
|
"groups": {
|
||||||
"lights": "Lights",
|
"lights": "Lights",
|
||||||
@ -6636,7 +6696,8 @@
|
|||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"others": "Others"
|
"others": "Others"
|
||||||
},
|
},
|
||||||
"unassigned_areas": "[%key:ui::panel::config::areas::picker::unassigned_areas%]"
|
"other_areas": "Other areas",
|
||||||
|
"areas": "Areas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cards": {
|
"cards": {
|
||||||
@ -6860,7 +6921,7 @@
|
|||||||
},
|
},
|
||||||
"edit_view": {
|
"edit_view": {
|
||||||
"header": "View configuration",
|
"header": "View configuration",
|
||||||
"header_name": "{name} View Configuration",
|
"header_name": "{name} view configuration",
|
||||||
"add": "Add view",
|
"add": "Add view",
|
||||||
"background": {
|
"background": {
|
||||||
"settings": "Background settings",
|
"settings": "Background settings",
|
||||||
@ -6976,7 +7037,7 @@
|
|||||||
},
|
},
|
||||||
"edit_card": {
|
"edit_card": {
|
||||||
"header": "Card configuration",
|
"header": "Card configuration",
|
||||||
"typed_header": "{type} Card configuration",
|
"typed_header": "{type} card configuration",
|
||||||
"pick_card": "Add to dashboard",
|
"pick_card": "Add to dashboard",
|
||||||
"pick_card_title": "Which card would you like to add to {name}",
|
"pick_card_title": "Which card would you like to add to {name}",
|
||||||
"toggle_editor": "Toggle editor",
|
"toggle_editor": "Toggle editor",
|
||||||
@ -7046,7 +7107,7 @@
|
|||||||
"move_card": {
|
"move_card": {
|
||||||
"header": "Choose a view to move the card to",
|
"header": "Choose a view to move the card to",
|
||||||
"strategy_error_title": "Impossible to move the card",
|
"strategy_error_title": "Impossible to move the card",
|
||||||
"strategy_error_text_strategy": "Moving a card to an auto generated view is not supported.",
|
"strategy_error_text_strategy": "Moving a card to an auto-generated view is not supported.",
|
||||||
"success": "Card moved successfully",
|
"success": "Card moved successfully",
|
||||||
"error": "Error while moving card"
|
"error": "Error while moving card"
|
||||||
},
|
},
|
||||||
@ -7381,7 +7442,7 @@
|
|||||||
},
|
},
|
||||||
"light": {
|
"light": {
|
||||||
"name": "Light",
|
"name": "Light",
|
||||||
"description": "The Light card allows you to change the brightness of the light."
|
"description": "The Light card allows you to change the brightness of a light."
|
||||||
},
|
},
|
||||||
"generic": {
|
"generic": {
|
||||||
"alt_text": "Alternative text",
|
"alt_text": "Alternative text",
|
||||||
@ -7469,13 +7530,13 @@
|
|||||||
"geo_location_sources": "Geolocation sources",
|
"geo_location_sources": "Geolocation sources",
|
||||||
"no_geo_location_sources": "No geolocation sources available",
|
"no_geo_location_sources": "No geolocation sources available",
|
||||||
"appearance": "Appearance",
|
"appearance": "Appearance",
|
||||||
"theme_mode": "Theme Mode",
|
"theme_mode": "Theme mode",
|
||||||
"theme_modes": {
|
"theme_modes": {
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark"
|
"dark": "Dark"
|
||||||
},
|
},
|
||||||
"default_zoom": "Default Zoom",
|
"default_zoom": "Default zoom",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
"description": "The Map card that allows you to display entities on a map."
|
"description": "The Map card that allows you to display entities on a map."
|
||||||
},
|
},
|
||||||
@ -7523,7 +7584,7 @@
|
|||||||
"picture-elements": {
|
"picture-elements": {
|
||||||
"name": "Picture elements",
|
"name": "Picture elements",
|
||||||
"description": "The Picture elements card is one of the most versatile types of cards. The cards allow you to position icons or text and even actions! On an image based on coordinates.",
|
"description": "The Picture elements card is one of the most versatile types of cards. The cards allow you to position icons or text and even actions! On an image based on coordinates.",
|
||||||
"card_options": "Card Options",
|
"card_options": "Card options",
|
||||||
"elements": "Elements",
|
"elements": "Elements",
|
||||||
"new_element": "Add new element",
|
"new_element": "Add new element",
|
||||||
"confirm_delete_element": "Are you sure you want to delete the {type} element?",
|
"confirm_delete_element": "Are you sure you want to delete the {type} element?",
|
||||||
@ -7562,7 +7623,7 @@
|
|||||||
"none": "None",
|
"none": "None",
|
||||||
"line": "Line"
|
"line": "Line"
|
||||||
},
|
},
|
||||||
"description": "The Sensor card gives you a quick overview of your sensors state with an optional graph to visualize change over time.",
|
"description": "The Sensor card gives you a quick overview of a sensor's state with an optional graph to visualize change over time.",
|
||||||
"limit_min": "Minimum value",
|
"limit_min": "Minimum value",
|
||||||
"limit_max": "Maximum value"
|
"limit_max": "Maximum value"
|
||||||
},
|
},
|
||||||
@ -7572,14 +7633,14 @@
|
|||||||
"integration_not_loaded": "This card requires the `todo` integration to be set up.",
|
"integration_not_loaded": "This card requires the `todo` integration to be set up.",
|
||||||
"hide_completed": "Hide completed items",
|
"hide_completed": "Hide completed items",
|
||||||
"hide_create": "Hide 'Add item' field",
|
"hide_create": "Hide 'Add item' field",
|
||||||
"display_order": "Display Order",
|
"display_order": "Display order",
|
||||||
"sort_modes": {
|
"sort_modes": {
|
||||||
"none": "Default",
|
"none": "Default",
|
||||||
"manual": "Manual",
|
"manual": "Manual",
|
||||||
"alpha_asc": "Alphabetical (A-Z)",
|
"alpha_asc": "Alphabetical (A-Z)",
|
||||||
"alpha_desc": "Alphabetical (Z-A)",
|
"alpha_desc": "Alphabetical (Z-A)",
|
||||||
"duedate_asc": "Due Date (Soonest First)",
|
"duedate_asc": "Due date (Soonest first)",
|
||||||
"duedate_desc": "Due Date (Latest First)"
|
"duedate_desc": "Due date (Latest first)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
@ -7589,7 +7650,7 @@
|
|||||||
},
|
},
|
||||||
"tile": {
|
"tile": {
|
||||||
"name": "Tile",
|
"name": "Tile",
|
||||||
"description": "The tile card gives you a quick overview of your entity. The card allows you to toggle the entity, show the More info dialog or trigger custom actions.",
|
"description": "The Tile card gives you a quick overview of an entity. The card allows you to toggle the entity, show the More info dialog or trigger custom actions.",
|
||||||
"color": "Color",
|
"color": "Color",
|
||||||
"color_helper": "Inactive state (e.g. off, closed) will not be colored.",
|
"color_helper": "Inactive state (e.g. off, closed) will not be colored.",
|
||||||
"icon_tap_action": "Icon tap behavior",
|
"icon_tap_action": "Icon tap behavior",
|
||||||
@ -7643,7 +7704,7 @@
|
|||||||
"badge": {
|
"badge": {
|
||||||
"entity": {
|
"entity": {
|
||||||
"name": "Entity",
|
"name": "Entity",
|
||||||
"description": "The Entity badge gives you a quick overview of your entity.",
|
"description": "The Entity badge gives you a quick overview of an entity.",
|
||||||
"color": "[%key:ui::panel::lovelace::editor::card::tile::color%]",
|
"color": "[%key:ui::panel::lovelace::editor::card::tile::color%]",
|
||||||
"color_helper": "[%key:ui::panel::lovelace::editor::card::tile::color_helper%]",
|
"color_helper": "[%key:ui::panel::lovelace::editor::card::tile::color_helper%]",
|
||||||
"show_entity_picture": "Show entity picture",
|
"show_entity_picture": "Show entity picture",
|
||||||
@ -7867,7 +7928,17 @@
|
|||||||
"controls_options": {
|
"controls_options": {
|
||||||
"light": "Lights",
|
"light": "Lights",
|
||||||
"fan": "Fans",
|
"fan": "Fans",
|
||||||
"switch": "Switches"
|
"switch": "Switches",
|
||||||
|
"cover-awning": "Awnings",
|
||||||
|
"cover-blind": "Blinds",
|
||||||
|
"cover-curtain": "Curtains",
|
||||||
|
"cover-damper": "Dampers",
|
||||||
|
"cover-door": "Doors",
|
||||||
|
"cover-garage": "Garage doors",
|
||||||
|
"cover-gate": "Gates",
|
||||||
|
"cover-shade": "Shades",
|
||||||
|
"cover-shutter": "Shutters",
|
||||||
|
"cover-window": "Windows"
|
||||||
},
|
},
|
||||||
"no_compatible_controls": "No compatible controls available for this area"
|
"no_compatible_controls": "No compatible controls available for this area"
|
||||||
}
|
}
|
||||||
@ -8171,14 +8242,14 @@
|
|||||||
"confirm_delete_title": "Delete long-lived access token?",
|
"confirm_delete_title": "Delete long-lived access token?",
|
||||||
"confirm_delete_text": "Are you sure you want to delete the long-lived access token for {name}?",
|
"confirm_delete_text": "Are you sure you want to delete the long-lived access token for {name}?",
|
||||||
"delete_failed": "Failed to delete the access token.",
|
"delete_failed": "Failed to delete the access token.",
|
||||||
"create": "Create Token",
|
"create": "Create token",
|
||||||
"create_failed": "Failed to create the access token.",
|
"create_failed": "Failed to create the access token.",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"prompt_name": "Give the token a name",
|
"prompt_name": "Give the token a name",
|
||||||
"prompt_copy_token": "Copy your access token. It will not be shown again.",
|
"prompt_copy_token": "Copy your access token. It will not be shown again.",
|
||||||
"empty_state": "You have no long-lived access tokens yet.",
|
"empty_state": "You have no long-lived access tokens yet.",
|
||||||
"qr_code_image": "QR code for token {name}",
|
"qr_code_image": "QR code for token {name}",
|
||||||
"generate_qr_code": "Generate QR Code"
|
"generate_qr_code": "Generate QR code"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"todo": {
|
"todo": {
|
||||||
@ -8335,7 +8406,7 @@
|
|||||||
"hdmi_input": "HDMI input",
|
"hdmi_input": "HDMI input",
|
||||||
"hdmi_switcher": "HDMI switcher",
|
"hdmi_switcher": "HDMI switcher",
|
||||||
"volume": "Volume",
|
"volume": "Volume",
|
||||||
"total_tv_time": "Total TV Time",
|
"total_tv_time": "Total TV time",
|
||||||
"turn_tv_off": "Turn television off",
|
"turn_tv_off": "Turn television off",
|
||||||
"air": "Air"
|
"air": "Air"
|
||||||
},
|
},
|
||||||
@ -8605,7 +8676,7 @@
|
|||||||
"input_button": "Input buttons",
|
"input_button": "Input buttons",
|
||||||
"input_text": "Input texts",
|
"input_text": "Input texts",
|
||||||
"input_number": "Input numbers",
|
"input_number": "Input numbers",
|
||||||
"input_datetime": "Input date times",
|
"input_datetime": "Input datetimes",
|
||||||
"input_select": "Input selects",
|
"input_select": "Input selects",
|
||||||
"template": "Template entities",
|
"template": "Template entities",
|
||||||
"universal": "Universal media player entities",
|
"universal": "Universal media player entities",
|
||||||
@ -9016,7 +9087,7 @@
|
|||||||
},
|
},
|
||||||
"capability": {
|
"capability": {
|
||||||
"stage": {
|
"stage": {
|
||||||
"title": "Add-on Stage",
|
"title": "Add-on stage",
|
||||||
"description": "Add-ons can have one of three stages:\n\n{icon_stable} **Stable**: These are add-ons ready to be used in production.\n\n{icon_experimental} **Experimental**: These may contain bugs, and may be unfinished.\n\n{icon_deprecated} **Deprecated**: These add-ons will no longer receive any updates."
|
"description": "Add-ons can have one of three stages:\n\n{icon_stable} **Stable**: These are add-ons ready to be used in production.\n\n{icon_experimental} **Experimental**: These may contain bugs, and may be unfinished.\n\n{icon_deprecated} **Deprecated**: These add-ons will no longer receive any updates."
|
||||||
},
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
@ -9098,8 +9169,8 @@
|
|||||||
"description": "This will restart the add-on if it crashes"
|
"description": "This will restart the add-on if it crashes"
|
||||||
},
|
},
|
||||||
"auto_update": {
|
"auto_update": {
|
||||||
"title": "Auto update",
|
"title": "Autoupdate",
|
||||||
"description": "Auto update the add-on when there is a new version available"
|
"description": "Autoupdate the add-on when there is a new version available"
|
||||||
},
|
},
|
||||||
"ingress_panel": {
|
"ingress_panel": {
|
||||||
"title": "Show in sidebar",
|
"title": "Show in sidebar",
|
||||||
@ -9210,7 +9281,7 @@
|
|||||||
"addons": "Add-ons",
|
"addons": "Add-ons",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"backups": "Backups",
|
"backups": "Backups",
|
||||||
"store": "Add-on Store",
|
"store": "Add-on store",
|
||||||
"system": "System"
|
"system": "System"
|
||||||
},
|
},
|
||||||
"my": {
|
"my": {
|
||||||
@ -9300,7 +9371,7 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"change_hostname": "Change hostname",
|
"change_hostname": "Change hostname",
|
||||||
"new_hostname": "Please enter a new hostname:",
|
"new_hostname": "Please enter a new hostname:",
|
||||||
"ip_address": "IP Address",
|
"ip_address": "IP address",
|
||||||
"change": "Change",
|
"change": "Change",
|
||||||
"operating_system": "Operating system",
|
"operating_system": "Operating system",
|
||||||
"docker_version": "Docker version",
|
"docker_version": "Docker version",
|
||||||
@ -9352,7 +9423,7 @@
|
|||||||
"confirm_password": "Confirm encryption key",
|
"confirm_password": "Confirm encryption key",
|
||||||
"password_protection": "Password protection",
|
"password_protection": "Password protection",
|
||||||
"enter_password": "Please enter a password.",
|
"enter_password": "Please enter a password.",
|
||||||
"passwords_not_matching": "The passwords does not match",
|
"passwords_not_matching": "The passwords do not match",
|
||||||
"backup_already_running": "A backup or restore is already running. Creating a new backup is currently not possible, try again later.",
|
"backup_already_running": "A backup or restore is already running. Creating a new backup is currently not possible, try again later.",
|
||||||
"confirm_restore_partial_backup_title": "Restore partial backup",
|
"confirm_restore_partial_backup_title": "Restore partial backup",
|
||||||
"confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again.",
|
"confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again.",
|
||||||
|
51
test/common/string/sort.test.ts
Normal file
51
test/common/string/sort.test.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { assert, describe, it } from "vitest";
|
||||||
|
|
||||||
|
import { stringCompare } from "../../../src/common/string/compare";
|
||||||
|
|
||||||
|
describe("stringCompare", () => {
|
||||||
|
// Node only ships with English support for `Intl`, so we cannot test for other language collators.
|
||||||
|
it("Ensure natural order reutrned when numeric value is included", () => {
|
||||||
|
assert.strictEqual(stringCompare("Helper 2", "Helper 10"), -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ensure prefixed numeric value is sorted naturally", () => {
|
||||||
|
assert.strictEqual(stringCompare("2 Helper", "10 Helper"), -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ensure order has reversed alphabet is sorted", () => {
|
||||||
|
const reverseAlphabet = [
|
||||||
|
"z",
|
||||||
|
"y",
|
||||||
|
"x",
|
||||||
|
"w",
|
||||||
|
"v",
|
||||||
|
"u",
|
||||||
|
"t",
|
||||||
|
"d",
|
||||||
|
"c",
|
||||||
|
"b",
|
||||||
|
"a",
|
||||||
|
];
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
[...reverseAlphabet].sort(stringCompare),
|
||||||
|
[...reverseAlphabet].reverse()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ensure natural order when using numbers", () => {
|
||||||
|
const testArray = [
|
||||||
|
"Helper 1",
|
||||||
|
"Helper 10",
|
||||||
|
"Helper 2",
|
||||||
|
"Helper 3",
|
||||||
|
"Helper 4",
|
||||||
|
];
|
||||||
|
assert.deepStrictEqual([...testArray].sort(stringCompare), [
|
||||||
|
"Helper 1",
|
||||||
|
"Helper 2",
|
||||||
|
"Helper 3",
|
||||||
|
"Helper 4",
|
||||||
|
"Helper 10",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -68,15 +68,18 @@ describe("Energy Short Format Test", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hass = { locale: defaultLocale } as HomeAssistant;
|
const hass = { locale: defaultLocale } as HomeAssistant;
|
||||||
it("Formats", () => {
|
it("No Unit conversion", () => {
|
||||||
|
assert.strictEqual(formatConsumptionShort(hass, 0, "Wh"), "0 Wh");
|
||||||
assert.strictEqual(formatConsumptionShort(hass, 0, "kWh"), "0 kWh");
|
assert.strictEqual(formatConsumptionShort(hass, 0, "kWh"), "0 kWh");
|
||||||
assert.strictEqual(formatConsumptionShort(hass, 0, "GWh"), "0 GWh");
|
assert.strictEqual(formatConsumptionShort(hass, 0, "GWh"), "0 GWh");
|
||||||
assert.strictEqual(formatConsumptionShort(hass, 0, "gal"), "0 gal");
|
assert.strictEqual(formatConsumptionShort(hass, 0, "gal"), "0 gal");
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
formatConsumptionShort(hass, 0.12345, "kWh"),
|
formatConsumptionShort(hass, 10000.12345, "gal"),
|
||||||
"0.12 kWh"
|
"10,000 gal"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(formatConsumptionShort(hass, 1.2345, "kWh"), "1.23 kWh");
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
formatConsumptionShort(hass, 10.12345, "kWh"),
|
formatConsumptionShort(hass, 10.12345, "kWh"),
|
||||||
"10.1 kWh"
|
"10.1 kWh"
|
||||||
@ -85,6 +88,10 @@ describe("Energy Short Format Test", () => {
|
|||||||
formatConsumptionShort(hass, 500.12345, "kWh"),
|
formatConsumptionShort(hass, 500.12345, "kWh"),
|
||||||
"500 kWh"
|
"500 kWh"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(formatConsumptionShort(hass, 10.01, "kWh"), "10 kWh");
|
||||||
|
});
|
||||||
|
it("Upward Unit conversion", () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
formatConsumptionShort(hass, 1512.34567, "kWh"),
|
formatConsumptionShort(hass, 1512.34567, "kWh"),
|
||||||
"1.51 MWh"
|
"1.51 MWh"
|
||||||
@ -105,23 +112,31 @@ describe("Energy Short Format Test", () => {
|
|||||||
formatConsumptionShort(hass, 15123456789.9, "kWh"),
|
formatConsumptionShort(hass, 15123456789.9, "kWh"),
|
||||||
"15.1 TWh"
|
"15.1 TWh"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
formatConsumptionShort(hass, 15123456789000.9, "kWh"),
|
formatConsumptionShort(hass, 15123456789000.9, "kWh"),
|
||||||
"15,123 TWh"
|
"15,123 TWh"
|
||||||
);
|
);
|
||||||
|
});
|
||||||
assert.strictEqual(formatConsumptionShort(hass, 1000.1, "GWh"), "1 TWh");
|
it("Downward Unit conversion", () => {
|
||||||
|
assert.strictEqual(formatConsumptionShort(hass, 0.00012, "kWh"), "0.12 Wh");
|
||||||
|
assert.strictEqual(formatConsumptionShort(hass, 0.12345, "kWh"), "123 Wh");
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
formatConsumptionShort(hass, 10000.12345, "gal"),
|
formatConsumptionShort(hass, 0.00001234, "TWh"),
|
||||||
"10,000 gal"
|
"12.3 MWh"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("Negativ Consumption", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
formatConsumptionShort(hass, -500.123, "kWh"),
|
||||||
|
"-500 kWh"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Don't really modify negative numbers, but make sure it's something sane.
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
formatConsumptionShort(hass, -1234.56, "kWh"),
|
formatConsumptionShort(hass, -1234.56, "kWh"),
|
||||||
"-1,234.56 kWh"
|
"-1.23 MWh"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
formatConsumptionShort(hass, -0.001234, "kWh"),
|
||||||
|
"-1.23 Wh"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user