Merge pull request #7875 from home-assistant/dev

This commit is contained in:
Bram Kragten 2020-12-02 19:51:36 +01:00 committed by GitHub
commit b7ccf3e0e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
117 changed files with 3929 additions and 965 deletions

View File

@ -7,8 +7,8 @@ export const createMediaPlayerEntities = () => [
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead", media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media // Select Source + Stop + Clear + Play + Shuffle Set
supported_features: 195135, supported_features: 64063,
entity_picture: "/images/album_cover_2.jpg", entity_picture: "/images/album_cover_2.jpg",
media_duration: 300, media_duration: 300,
media_position: 50, media_position: 50,
@ -24,8 +24,8 @@ export const createMediaPlayerEntities = () => [
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead", media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
supported_features: 64063, supported_features: 195135,
entity_picture: "/images/album_cover.jpg", entity_picture: "/images/album_cover.jpg",
media_duration: 300, media_duration: 300,
media_position: 0, media_position: 0,

View File

@ -146,6 +146,16 @@ const CONFIGS = [
entity: media_player.receiver_off entity: media_player.receiver_off
`, `,
}, },
{
heading: "Grid Full Size",
config: `
- type: grid
columns: 1
cards:
- type: media-control
entity: media_player.music_paused
`,
},
]; ];
class DemoHuiMediControlCard extends PolymerElement { class DemoHuiMediControlCard extends PolymerElement {

View File

@ -74,9 +74,7 @@ export class HassioUpdate extends LitElement {
"Supervisor", "Supervisor",
this.supervisor.supervisor, this.supervisor.supervisor,
"hassio/supervisor/update", "hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${ `https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
this.supervisor.supervisor.version_latest
}`
)} )}
${this.supervisor.host.features.includes("hassos") ${this.supervisor.host.features.includes("hassos")
? this._renderUpdateCard( ? this._renderUpdateCard(

View File

@ -137,8 +137,7 @@ export class DialogHassioNetwork extends LitElement
)} )}
${this._interface?.type === "wireless" ${this._interface?.type === "wireless"
? html` ? html`
<ha-expansion-panel outlined> <ha-expansion-panel header="Wi-Fi" outlined>
<span slot="title">Wi-Fi</span>
${this._interface?.wifi?.ssid ${this._interface?.wifi?.ssid
? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>` ? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>`
: ""} : ""}
@ -281,8 +280,10 @@ export class DialogHassioNetwork extends LitElement
private _renderIPConfiguration(version: string) { private _renderIPConfiguration(version: string) {
return html` return html`
<ha-expansion-panel outlined> <ha-expansion-panel
<span slot="title">IPv${version.charAt(version.length - 1)}</span> .header=${`IPv${version.charAt(version.length - 1)}`}
outlined
>
<div class="radio-row"> <div class="radio-row">
<ha-formfield label="DHCP"> <ha-formfield label="DHCP">
<ha-radio <ha-radio
@ -591,6 +592,7 @@ export class DialogHassioNetwork extends LitElement
} }
ha-expansion-panel { ha-expansion-panel {
--expansion-panel-summary-padding: 0 16px;
margin: 4px 0; margin: 4px 0;
} }
paper-input { paper-input {

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20201126.0", version="20201202.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer", url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors", author="The Home Assistant Authors",

View File

@ -0,0 +1,6 @@
export const ensureArray = (value?: any) => {
if (!value || Array.isArray(value)) {
return value;
}
return [value];
};

View File

@ -67,6 +67,10 @@ export const computeStateDisplay = (
} }
} }
if (domain === "counter") {
return formatNumber(compareState, language);
}
return ( return (
// Return device class translation // Return device class translation
(stateObj.attributes.device_class && (stateObj.attributes.device_class &&

View File

@ -139,7 +139,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
private _filteredDevices: DeviceRegistryEntry[] = []; private _filteredDevices: DeviceRegistryEntry[] = [];
private _getDevices = memoizeOne( private _getAreasWithDevices = memoizeOne(
( (
devices: DeviceRegistryEntry[], devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[], areas: AreaRegistryEntry[],
@ -277,7 +277,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
if (!this._devices || !this._areas || !this._entities) { if (!this._devices || !this._areas || !this._entities) {
return html``; return html``;
} }
const areas = this._getDevices( const areas = this._getAreasWithDevices(
this._devices, this._devices,
this._areas, this._areas,
this._entities, this._entities,

View File

@ -111,6 +111,18 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) @property({ type: Boolean })
private _opened?: boolean; private _opened?: boolean;
public open() {
this.updateComplete.then(() => {
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
});
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
});
}
private _getDevices = memoizeOne( private _getDevices = memoizeOne(
( (
devices: DeviceRegistryEntry[], devices: DeviceRegistryEntry[],
@ -126,14 +138,17 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
} }
const deviceEntityLookup: DeviceEntityLookup = {}; const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) { if (includeDomains || excludeDomains || includeDeviceClasses) {
continue; for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
} }
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
} }
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
@ -141,7 +156,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
areaLookup[area.area_id] = area; areaLookup[area.area_id] = area;
} }
let inputDevices = [...devices]; let inputDevices = devices.filter(
(device) => device.id === this.value || !device.disabled_by
);
if (includeDomains) { if (includeDomains) {
inputDevices = inputDevices.filter((device) => { inputDevices = inputDevices.filter((device) => {

View File

@ -101,6 +101,18 @@ export class HaEntityPicker extends LitElement {
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
public open() {
this.updateComplete.then(() => {
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
});
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
});
}
private _initedStates = false; private _initedStates = false;
private _states: HassEntity[] = []; private _states: HassEntity[] = [];

View File

@ -28,6 +28,18 @@ import {
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import memoizeOne from "memoize-one";
import {
DeviceEntityLookup,
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../data/entity_registry";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
const rowRenderer = ( const rowRenderer = (
root: HTMLElement, root: HTMLElement,
@ -68,39 +80,227 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public value?: string; @property() public value?: string;
@property() public _areas?: AreaRegistryEntry[]; @property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" }) @property({ type: Boolean, attribute: "no-add" })
public noAdd?: boolean; public noAdd?: boolean;
/**
* Show only areas with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no areas with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only areas with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@internalProperty() private _areas?: AreaRegistryEntry[];
@internalProperty() private _devices?: DeviceRegistryEntry[];
@internalProperty() private _entities?: EntityRegistryEntry[];
@internalProperty() private _opened?: boolean; @internalProperty() private _opened?: boolean;
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeAreaRegistry(this.hass.connection!, (areas) => { subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = this.noAdd this._areas = areas;
? areas }),
: [ subscribeDeviceRegistry(this.hass.connection!, (devices) => {
...areas, this._devices = devices;
{ }),
area_id: "add_new", subscribeEntityRegistry(this.hass.connection!, (entities) => {
name: this.hass.localize("ui.components.area-picker.add_new"), this._entities = entities;
},
];
}), }),
]; ];
} }
public open() {
this.updateComplete.then(() => {
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
});
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
});
}
private _getAreas = memoizeOne(
(
areas: AreaRegistryEntry[],
devices: DeviceRegistryEntry[],
entities: EntityRegistryEntry[],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
noAdd: this["noAdd"]
): AreaRegistryEntry[] => {
const deviceEntityLookup: DeviceEntityLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryEntry[] | undefined;
if (includeDomains || excludeDomains || includeDeviceClasses) {
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
} else if (deviceFilter) {
inputDevices = devices;
} else if (entityFilter) {
inputEntities = entities.filter((entity) => entity.area_id);
}
if (includeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
}
if (excludeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter(
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
}
if (deviceFilter) {
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
}
if (entityFilter) {
entities = entities.filter((entity) => entityFilter!(entity));
}
let outputAreas = areas;
let areaIds: string[] | undefined;
if (inputDevices) {
areaIds = inputDevices
.filter((device) => device.area_id)
.map((device) => device.area_id!);
}
if (inputEntities) {
areaIds = (areaIds ?? []).concat(
inputEntities
.filter((entity) => entity.area_id)
.map((entity) => entity.area_id!)
);
}
if (areaIds) {
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
}
return noAdd
? outputAreas
: [
...outputAreas,
{
area_id: "add_new",
name: this.hass.localize("ui.components.area-picker.add_new"),
},
];
}
);
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._areas) { if (!this._devices || !this._areas || !this._entities) {
return html``; return html``;
} }
const areas = this._getAreas(
this._areas,
this._devices,
this._entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.noAdd
);
return html` return html`
<vaadin-combo-box-light <vaadin-combo-box-light
item-value-path="area_id" item-value-path="area_id"
item-id-path="area_id" item-id-path="area_id"
item-label-path="name" item-label-path="name"
.items=${this._areas} .items=${areas}
.value=${this._value} .value=${this._value}
.renderer=${rowRenderer} .renderer=${rowRenderer}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@ -110,6 +310,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
.label=${this.label === undefined && this.hass .label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area") ? this.hass.localize("ui.components.area-picker.area")
: this.label} : this.label}
.placeholder=${this.placeholder
? this._area(this.placeholder)?.name
: undefined}
class="input" class="input"
autocapitalize="none" autocapitalize="none"
autocomplete="off" autocomplete="off"
@ -132,7 +335,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
</ha-icon-button> </ha-icon-button>
` `
: ""} : ""}
${this._areas.length > 0 ${areas.length > 0
? html` ? html`
<ha-icon-button <ha-icon-button
aria-label=${this.hass.localize( aria-label=${this.hass.localize(
@ -151,6 +354,12 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
`; `;
} }
private _area = memoizeOne((areaId: string):
| AreaRegistryEntry
| undefined => {
return this._areas?.find((area) => area.area_id === areaId);
});
private _clearValue(ev: Event) { private _clearValue(ev: Event) {
ev.stopPropagation(); ev.stopPropagation();
this._setValue(""); this._setValue("");

View File

@ -11,6 +11,7 @@ import {
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { ToggleButton } from "../types"; import type { ToggleButton } from "../types";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "@material/mwc-button/mwc-button";
@customElement("ha-button-toggle-group") @customElement("ha-button-toggle-group")
export class HaButtonToggleGroup extends LitElement { export class HaButtonToggleGroup extends LitElement {
@ -21,17 +22,22 @@ export class HaButtonToggleGroup extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div> <div>
${this.buttons.map( ${this.buttons.map((button) =>
(button) => html` button.iconPath
<mwc-icon-button ? html`<mwc-icon-button
.label=${button.label} .label=${button.label}
.value=${button.value} .value=${button.value}
?active=${this.active === button.value} ?active=${this.active === button.value}
@click=${this._handleClick} @click=${this._handleClick}
> >
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon> <ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>`
` : html`<mwc-button
.value=${button.value}
?active=${this.active === button.value}
@click=${this._handleClick}
>${button.label}</mwc-button
>`
)} )}
</div> </div>
`; `;
@ -49,13 +55,15 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-icon-button-size: var(--button-toggle-size, 36px); --mdc-icon-button-size: var(--button-toggle-size, 36px);
--mdc-icon-size: var(--button-toggle-icon-size, 20px); --mdc-icon-size: var(--button-toggle-icon-size, 20px);
} }
mwc-icon-button { mwc-icon-button,
mwc-button {
border: 1px solid var(--primary-color); border: 1px solid var(--primary-color);
border-right-width: 0px; border-right-width: 0px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
mwc-icon-button::before { mwc-icon-button::before,
mwc-button::before {
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
@ -67,17 +75,21 @@ export class HaButtonToggleGroup extends LitElement {
content: ""; content: "";
transition: opacity 15ms linear, background-color 15ms linear; transition: opacity 15ms linear, background-color 15ms linear;
} }
mwc-icon-button[active]::before { mwc-icon-button[active]::before,
mwc-button[active]::before {
opacity: var(--mdc-icon-button-ripple-opacity, 0.12); opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
} }
mwc-icon-button:first-child { mwc-icon-button:first-child,
mwc-button:first-child {
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
} }
mwc-icon-button:last-child { mwc-icon-button:last-child,
mwc-button:last-child {
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
border-right-width: 1px; border-right-width: 1px;
} }
mwc-icon-button:only-child { mwc-icon-button:only-child,
mwc-button:only-child {
border-radius: 4px; border-radius: 4px;
border-right-width: 1px; border-right-width: 1px;
} }

View File

@ -19,12 +19,14 @@ class HaExpansionPanel extends LitElement {
@property({ type: Boolean, reflect: true }) outlined = false; @property({ type: Boolean, reflect: true }) outlined = false;
@property() header?: string;
@query(".container") private _container!: HTMLDivElement; @query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="summary" @click=${this._toggleContainer}> <div class="summary" @click=${this._toggleContainer}>
<slot name="title"></slot> <slot name="header">${this.header}</slot>
<ha-svg-icon <ha-svg-icon
.path=${mdiChevronDown} .path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}" class="summary-icon ${classMap({ expanded: this.expanded })}"
@ -76,7 +78,7 @@ class HaExpansionPanel extends LitElement {
.summary { .summary {
display: flex; display: flex;
padding: var(--expansion-panel-summary-padding, 0px 16px); padding: var(--expansion-panel-summary-padding, 0);
min-height: 48px; min-height: 48px;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;

20
src/components/ha-fab.ts Normal file
View File

@ -0,0 +1,20 @@
import type { Fab } from "@material/mwc-fab";
import "@material/mwc-fab";
import { customElement } from "lit-element";
import { Constructor } from "../types";
const MwcFab = customElements.get("mwc-fab") as Constructor<Fab>;
@customElement("ha-fab")
export class HaFab extends MwcFab {
protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-fab": HaFab;
}
}

View File

@ -60,8 +60,9 @@ export class HaIconInput extends LitElement {
static get styles() { static get styles() {
return css` return css`
ha-icon { ha-icon {
position: relative; position: absolute;
bottom: 4px; bottom: 2px;
right: 0;
} }
`; `;
} }

View File

@ -0,0 +1,45 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { ActionSelector } from "../../data/selector";
import { Action } from "../../data/script";
import "../../panels/config/automation/action/ha-automation-action";
@customElement("ha-selector-action")
export class HaActionSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: ActionSelector;
@property() public value?: Action;
@property() public label?: string;
protected render() {
return html`<ha-automation-action
.actions=${this.value || []}
.hass=${this.hass}
></ha-automation-action>`;
}
static get styles(): CSSResult {
return css`
ha-automation-action {
display: block;
margin-bottom: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-action": HaActionSelector;
}
}

View File

@ -1,7 +1,16 @@
import { customElement, html, LitElement, property } from "lit-element"; import {
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { AreaSelector } from "../../data/selector"; import { AreaSelector } from "../../data/selector";
import "../ha-area-picker"; import "../ha-area-picker";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceRegistryEntry } from "../../data/device_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
@customElement("ha-selector-area") @customElement("ha-selector-area")
export class HaAreaSelector extends LitElement { export class HaAreaSelector extends LitElement {
@ -13,14 +22,76 @@ export class HaAreaSelector extends LitElement {
@property() public label?: string; @property() public label?: string;
@internalProperty() public _configEntries?: ConfigEntry[];
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (
oldSelector !== this.selector &&
this.selector.area.device?.integration
) {
this._loadConfigEntries();
}
}
}
protected render() { protected render() {
return html`<ha-area-picker return html`<ha-area-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this.value} .value=${this.value}
.label=${this.label} .label=${this.label}
no-add no-add
.deviceFilter=${(device) => this._filterDevices(device)}
.entityFilter=${(entity) => this._filterEntities(entity)}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
></ha-area-picker>`; ></ha-area-picker>`;
} }
private _filterEntities(entity: EntityRegistryEntry): boolean {
if (this.selector.area.entity?.integration) {
if (entity.platform !== this.selector.area.entity.integration) {
return false;
}
}
return true;
}
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.area.device?.manufacturer &&
device.manufacturer !== this.selector.area.device.manufacturer
) {
return false;
}
if (
this.selector.area.device?.model &&
device.model !== this.selector.area.device.model
) {
return false;
}
if (this.selector.area.device?.integration) {
if (
!this._configEntries?.some((entry) =>
device.config_entries.includes(entry.entry_id)
)
) {
return false;
}
}
return true;
}
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.area.device?.integration
);
}
} }
declare global { declare global {

View File

@ -19,7 +19,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property() public selector!: EntitySelector; @property() public selector!: EntitySelector;
@internalProperty() private _entities?: Record<string, string>; @internalProperty() private _entityPlaformLookup?: Record<string, string>;
@property() public value?: any; @property() public value?: any;
@ -45,7 +45,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
} }
entityLookup[confEnt.entity_id] = confEnt.platform; entityLookup[confEnt.entity_id] = confEnt.platform;
} }
this._entities = entityLookup; this._entityPlaformLookup = entityLookup;
}), }),
]; ];
} }
@ -66,8 +66,9 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
} }
if (this.selector.entity.integration) { if (this.selector.entity.integration) {
if ( if (
!this._entities || !this._entityPlaformLookup ||
this._entities[entity.entity_id] !== this.selector.entity.integration this._entityPlaformLookup[entity.entity_id] !==
this.selector.entity.integration
) { ) {
return false; return false;
} }

View File

@ -0,0 +1,153 @@
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { TargetSelector } from "../../data/selector";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceRegistryEntry } from "../../data/device_registry";
import "../ha-target-picker";
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-input/paper-input";
import "@material/mwc-list/mwc-list";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { Target } from "../../data/target";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
@customElement("ha-selector-target")
export class HaTargetSelector extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public selector!: TargetSelector;
@property() public value?: Target;
@property() public label?: string;
@internalProperty() private _entityPlaformLookup?: Record<string, string>;
@internalProperty() private _configEntries?: ConfigEntry[];
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
const entityLookup = {};
for (const confEnt of entities) {
if (!confEnt.platform) {
continue;
}
entityLookup[confEnt.entity_id] = confEnt.platform;
}
this._entityPlaformLookup = entityLookup;
}),
];
}
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (
oldSelector !== this.selector &&
this.selector.target.device?.integration
) {
this._loadConfigEntries();
}
}
}
protected render() {
return html`<ha-target-picker
.hass=${this.hass}
.value=${this.value}
.deviceFilter=${(device) => this._filterDevices(device)}
.entityRegFilter=${(entity: EntityRegistryEntry) =>
this._filterRegEntities(entity)}
.entityFilter=${(entity: HassEntity) => this._filterEntities(entity)}
.includeDeviceClasses=${this.selector.target.entity?.device_class
? [this.selector.target.entity.device_class]
: undefined}
.includeDomains=${this.selector.target.entity?.domain
? [this.selector.target.entity.domain]
: undefined}
></ha-target-picker>`;
}
private _filterEntities(entity: HassEntity): boolean {
if (this.selector.target.entity?.integration) {
if (
!this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !==
this.selector.target.entity.integration
) {
return false;
}
}
return true;
}
private _filterRegEntities(entity: EntityRegistryEntry): boolean {
if (this.selector.target.entity?.integration) {
if (entity.platform !== this.selector.target.entity.integration) {
return false;
}
}
return true;
}
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.target.device?.manufacturer &&
device.manufacturer !== this.selector.target.device.manufacturer
) {
return false;
}
if (
this.selector.target.device?.model &&
device.model !== this.selector.target.device.model
) {
return false;
}
if (this.selector.target.device?.integration) {
if (
!this._configEntries?.some((entry) =>
device.config_entries.includes(entry.entry_id)
)
) {
return false;
}
}
return true;
}
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.target.device?.integration
);
}
static get styles(): CSSResult {
return css`
ha-target-picker {
margin: 0 -8px;
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-target": HaTargetSelector;
}
}

View File

@ -5,9 +5,11 @@ import { HomeAssistant } from "../../types";
import "./ha-selector-entity"; import "./ha-selector-entity";
import "./ha-selector-device"; import "./ha-selector-device";
import "./ha-selector-area"; import "./ha-selector-area";
import "./ha-selector-target";
import "./ha-selector-number"; import "./ha-selector-number";
import "./ha-selector-boolean"; import "./ha-selector-boolean";
import "./ha-selector-time"; import "./ha-selector-time";
import "./ha-selector-action";
import { Selector } from "../../data/selector"; import { Selector } from "../../data/selector";
@customElement("ha-selector") @customElement("ha-selector")

View File

@ -0,0 +1,595 @@
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
query,
unsafeCSS,
} from "lit-element";
import { HomeAssistant } from "../types";
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import {
mdiSofa,
mdiDevices,
mdiClose,
mdiPlus,
mdiUnfoldMoreVertical,
} from "@mdi/js";
import "./ha-svg-icon";
import "./ha-icon";
import "@material/mwc-icon-button/mwc-icon-button";
import { classMap } from "lit-html/directives/class-map";
import "@material/mwc-button/mwc-button";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../data/area_registry";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { computeStateName } from "../common/entity/compute_state_name";
import { stateIcon } from "../common/entity/state_icon";
import { fireEvent } from "../common/dom/fire_event";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import { computeDomain } from "../common/entity/compute_domain";
import { Target } from "../data/target";
import { ensureArray } from "../common/ensure-array";
import "./entity/ha-entity-picker";
import "./device/ha-device-picker";
import "./ha-area-picker";
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
import "@polymer/paper-tooltip/paper-tooltip";
@customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public value?: Target;
@property() public label?: string;
/**
* Show only targets with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show only targets with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityRegFilter?: (entity: EntityRegistryEntry) => boolean;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry };
@internalProperty() private _devices?: {
[deviceId: string]: DeviceRegistryEntry;
};
@internalProperty() private _entities?: EntityRegistryEntry[];
@internalProperty() private _addMode?: "area_id" | "entity_id" | "device_id";
@query("#input") private _inputElement?;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeAreaRegistry(this.hass.connection!, (areas) => {
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
this._areas = areaLookup;
}),
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
for (const device of devices) {
deviceLookup[device.id] = device;
}
this._devices = deviceLookup;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
}),
];
}
protected render() {
if (!this._areas || !this._devices || !this._entities) {
return html``;
}
return html`<div class="mdc-chip-set items">
${ensureArray(this.value?.area_id)?.map((area_id) => {
const area = this._areas![area_id];
return this._renderChip(
"area_id",
area_id,
area?.name || area_id,
undefined,
mdiSofa
);
})}
${ensureArray(this.value?.device_id)?.map((device_id) => {
const device = this._devices![device_id];
return this._renderChip(
"device_id",
device_id,
device?.name || device_id,
undefined,
mdiDevices
);
})}
${ensureArray(this.value?.entity_id)?.map((entity_id) => {
const entity = this.hass.states[entity_id];
return this._renderChip(
"entity_id",
entity_id,
entity ? computeStateName(entity) : entity_id,
entity ? stateIcon(entity) : undefined
);
})}
</div>
${this._renderPicker()}
<div class="mdc-chip-set">
<div
class="mdc-chip area_id add"
.type=${"area_id"}
@click=${this._showPicker}
>
<div class="mdc-chip__ripple"></div>
<ha-svg-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.path=${mdiPlus}
></ha-svg-icon>
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__primary-action">
<span class="mdc-chip__text"
>${this.hass.localize(
"ui.components.target-picker.add_area_id"
)}</span
>
</span>
</span>
</div>
<div
class="mdc-chip device_id add"
.type=${"device_id"}
@click=${this._showPicker}
>
<div class="mdc-chip__ripple"></div>
<ha-svg-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.path=${mdiPlus}
></ha-svg-icon>
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__primary-action">
<span class="mdc-chip__text"
>${this.hass.localize(
"ui.components.target-picker.add_device_id"
)}</span
>
</span>
</span>
</div>
<div
class="mdc-chip entity_id add"
.type=${"entity_id"}
@click=${this._showPicker}
>
<div class="mdc-chip__ripple"></div>
<ha-svg-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.path=${mdiPlus}
></ha-svg-icon>
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__primary-action">
<span class="mdc-chip__text"
>${this.hass.localize(
"ui.components.target-picker.add_entity_id"
)}</span
>
</span>
</span>
</div>
</div>`;
}
private async _showPicker(ev) {
this._addMode = ev.currentTarget.type;
await this.updateComplete;
setTimeout(() => {
this._inputElement?.open();
this._inputElement?.focus();
}, 0);
}
private _renderChip(
type: string,
id: string,
name: string,
icon?: string,
iconPath?: string
) {
return html`
<div
class="mdc-chip ${classMap({
[type]: true,
})}"
>
${iconPath
? html`<ha-svg-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.path=${iconPath}
></ha-svg-icon>`
: ""}
${icon
? html`<ha-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.icon=${icon}
></ha-icon>`
: ""}
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__primary-action">
<span class="mdc-chip__text">${name}</span>
</span>
</span>
${type === "entity_id"
? ""
: html` <span role="gridcell">
<mwc-icon-button
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
tabindex="-1"
role="button"
.label=${"Expand"}
.id=${id}
.type=${type}
@click=${this._handleExpand}
>
<ha-svg-icon .path=${mdiUnfoldMoreVertical}></ha-svg-icon>
</mwc-icon-button>
<paper-tooltip class="expand" animation-delay="0"
>${this.hass.localize(
`ui.components.target-picker.expand_${type}`
)}</paper-tooltip
>
</span>`}
<span role="gridcell">
<mwc-icon-button
class="mdc-chip__icon mdc-chip__icon--trailing"
tabindex="-1"
role="button"
.label=${"Remove"}
.id=${id}
.type=${type}
@click=${this._handleRemove}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
<paper-tooltip animation-delay="0"
>${this.hass.localize(
`ui.components.target-picker.remove_${type}`
)}</paper-tooltip
>
</span>
</div>
`;
}
private _renderPicker() {
switch (this._addMode) {
case "area_id":
return html`<ha-area-picker
.hass=${this.hass}
id="input"
.type=${"area_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_area_id"
)}
no-add
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityRegFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
@value-changed=${this._targetPicked}
></ha-area-picker>`;
case "device_id":
return html`<ha-device-picker
.hass=${this.hass}
id="input"
.type=${"device_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_device_id"
)}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityRegFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
@value-changed=${this._targetPicked}
></ha-device-picker>`;
case "entity_id":
return html`<ha-entity-picker
.hass=${this.hass}
id="input"
.type=${"entity_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_entity_id"
)}
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
@value-changed=${this._targetPicked}
></ha-entity-picker>`;
}
return html``;
}
private _targetPicked(ev) {
ev.stopPropagation();
if (!ev.detail.value) {
return;
}
const value = ev.detail.value;
const target = ev.currentTarget;
target.value = "";
this._addMode = undefined;
fireEvent(this, "value-changed", {
value: this.value
? {
...this.value,
[target.type]: this.value[target.type]
? [...ensureArray(this.value[target.type]), value]
: value,
}
: { [target.type]: value },
});
}
private _handleExpand(ev) {
const target = ev.currentTarget as any;
const newDevices: string[] = [];
const newEntities: string[] = [];
if (target.type === "area_id") {
Object.values(this._devices!).forEach((device) => {
if (
device.area_id === target.id &&
!this.value!.device_id?.includes(device.id) &&
this._deviceMeetsFilter(device)
) {
newDevices.push(device.id);
}
});
this._entities!.forEach((entity) => {
if (
entity.area_id === target.id &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity)
) {
newEntities.push(entity.entity_id);
}
});
} else if (target.type === "device_id") {
this._entities!.forEach((entity) => {
if (
entity.device_id === target.id &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity)
) {
newEntities.push(entity.entity_id);
}
});
} else {
return;
}
let value = this.value;
if (newEntities.length) {
value = this._addItems(value, "entity_id", newEntities);
}
if (newDevices.length) {
value = this._addItems(value, "device_id", newDevices);
}
value = this._removeItem(value, target.type, target.id);
fireEvent(this, "value-changed", { value });
}
private _handleRemove(ev) {
const target = ev.currentTarget as any;
fireEvent(this, "value-changed", {
value: this._removeItem(this.value, target.type, target.id),
});
}
private _addItems(
value: this["value"],
type: string,
ids: string[]
): this["value"] {
return {
...value,
[type]: value![type] ? ensureArray(value![type])!.concat(ids) : ids,
};
}
private _removeItem(
value: this["value"],
type: string,
id: string
): this["value"] {
return {
...value,
[type]: ensureArray(value![type])!.filter((val) => val !== id),
};
}
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
const devEntities = this._entities?.filter(
(entity) => entity.device_id === device.id
);
if (this.includeDomains) {
if (!devEntities || !devEntities.length) {
return false;
}
if (
!devEntities.some((entity) =>
this.includeDomains!.includes(computeDomain(entity.entity_id))
)
) {
return false;
}
}
if (this.includeDeviceClasses) {
if (!devEntities || !devEntities.length) {
return false;
}
if (
!devEntities.some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
this.includeDeviceClasses!.includes(
stateObj.attributes.device_class
)
);
})
) {
return false;
}
}
if (this.deviceFilter) {
return this.deviceFilter(device);
}
return true;
}
private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean {
if (
this.includeDomains &&
!this.includeDomains.includes(computeDomain(entity.entity_id))
) {
return false;
}
if (this.includeDeviceClasses) {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
if (
!stateObj.attributes.device_class ||
!this.includeDeviceClasses!.includes(stateObj.attributes.device_class)
) {
return false;
}
}
if (this.entityRegFilter) {
return this.entityRegFilter(entity);
}
return true;
}
static get styles(): CSSResult {
return css`
${unsafeCSS(chipStyles)}
.mdc-chip {
color: var(--primary-text-color);
}
.items {
z-index: 2;
}
.mdc-chip.add {
color: rgba(0, 0, 0, 0.87);
}
.mdc-chip:not(.add) {
cursor: default;
}
.mdc-chip mwc-icon-button {
--mdc-icon-button-size: 24px;
display: flex;
align-items: center;
outline: none;
}
.mdc-chip mwc-icon-button ha-svg-icon {
border-radius: 50%;
background: var(--secondary-text-color);
}
.mdc-chip__icon.mdc-chip__icon--trailing {
width: 16px;
height: 16px;
--mdc-icon-size: 14px;
color: var(--card-background-color);
}
.mdc-chip__icon--leading {
display: flex;
align-items: center;
justify-content: center;
--mdc-icon-size: 20px;
border-radius: 50%;
padding: 6px;
margin-left: -14px !important;
}
.expand-btn {
margin-right: 0;
}
.mdc-chip.area_id:not(.add) {
border: 2px solid #fed6a4;
background: var(--card-background-color);
}
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.area_id.add {
background: #fed6a4;
}
.mdc-chip.device_id:not(.add) {
border: 2px solid #a8e1fb;
background: var(--card-background-color);
}
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.device_id.add {
background: #a8e1fb;
}
.mdc-chip.entity_id:not(.add) {
border: 2px solid #d2e7b9;
background: var(--card-background-color);
}
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
.mdc-chip.entity_id.add {
background: #d2e7b9;
}
.mdc-chip:hover {
z-index: 5;
}
paper-tooltip.expand {
min-width: 200px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-target-picker": HaTargetPicker;
}
}

View File

@ -1,5 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-fab/mwc-fab"; import "../ha-fab";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiArrowLeft, mdiClose, mdiPlay, mdiPlus } from "@mdi/js"; import { mdiArrowLeft, mdiClose, mdiPlay, mdiPlus } from "@mdi/js";
@ -170,7 +170,7 @@ export class HaMediaPlayerBrowse extends LitElement {
> >
${this._narrow && currentItem?.can_play ${this._narrow && currentItem?.can_play
? html` ? html`
<mwc-fab <ha-fab
mini mini
.item=${currentItem} .item=${currentItem}
@click=${this._actionClicked} @click=${this._actionClicked}
@ -185,7 +185,7 @@ export class HaMediaPlayerBrowse extends LitElement {
${this.hass.localize( ${this.hass.localize(
`ui.components.media-browser.${this.action}` `ui.components.media-browser.${this.action}`
)} )}
</mwc-fab> </ha-fab>
` `
: ""} : ""}
</div> </div>
@ -927,7 +927,7 @@ export class HaMediaPlayerBrowse extends LitElement {
transition: width 0.4s, height 0.4s, padding-bottom 0.4s; transition: width 0.4s, height 0.4s, padding-bottom 0.4s;
} }
mwc-fab { ha-fab {
position: absolute; position: absolute;
--mdc-theme-secondary: var(--primary-color); --mdc-theme-secondary: var(--primary-color);
bottom: -20px; bottom: -20px;
@ -1011,7 +1011,7 @@ export class HaMediaPlayerBrowse extends LitElement {
margin-bottom: 0; margin-bottom: 0;
} }
:host([scroll]) mwc-fab { :host([scroll]) ha-fab {
bottom: 4px; bottom: 4px;
right: 4px; right: 4px;
--mdc-fab-box-shadow: none; --mdc-fab-box-shadow: none;

View File

@ -6,7 +6,7 @@ import { navigate } from "../common/navigate";
import { Context, HomeAssistant } from "../types"; import { Context, HomeAssistant } from "../types";
import { BlueprintInput } from "./blueprint"; import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation"; import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action } from "./script"; import { Action, MODES } from "./script";
export interface AutomationEntity extends HassEntityBase { export interface AutomationEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & { attributes: HassEntityAttributeBase & {
@ -26,7 +26,7 @@ export interface ManualAutomationConfig {
trigger: Trigger[]; trigger: Trigger[];
condition?: Condition[]; condition?: Condition[];
action: Action[]; action: Action[];
mode?: "single" | "restart" | "queued" | "parallel"; mode?: typeof MODES[number];
max?: number; max?: number;
} }

View File

@ -17,6 +17,7 @@ export interface DeviceRegistryEntry {
area_id?: string; area_id?: string;
name_by_user?: string; name_by_user?: string;
entry_type: "service" | null; entry_type: "service" | null;
disabled_by: string | null;
} }
export interface DeviceEntityLookup { export interface DeviceEntityLookup {
@ -26,6 +27,7 @@ export interface DeviceEntityLookup {
export interface DeviceRegistryEntryMutableParams { export interface DeviceRegistryEntryMutableParams {
area_id?: string | null; area_id?: string | null;
name_by_user?: string | null; name_by_user?: string | null;
disabled_by?: string | null;
} }
export const fallbackDeviceName = ( export const fallbackDeviceName = (

View File

@ -10,6 +10,7 @@ export interface EntityRegistryEntry {
platform: string; platform: string;
config_entry_id?: string; config_entry_id?: string;
device_id?: string; device_id?: string;
area_id?: string;
disabled_by: string | null; disabled_by: string | null;
} }
@ -29,6 +30,7 @@ export interface UpdateEntityRegistryEntryResult {
export interface EntityRegistryEntryUpdateParams { export interface EntityRegistryEntryUpdateParams {
name?: string | null; name?: string | null;
icon?: string | null; icon?: string | null;
area_id?: string | null;
disabled_by?: string | null; disabled_by?: string | null;
new_entity_id?: string; new_entity_id?: string;
} }

View File

@ -7,13 +7,13 @@ import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { Condition, Trigger } from "./automation"; import { Condition, Trigger } from "./automation";
export const MODES = ["single", "restart", "queued", "parallel"]; export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"]; export const MODES_MAX = ["queued", "parallel"];
export interface ScriptEntity extends HassEntityBase { export interface ScriptEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & { attributes: HassEntityAttributeBase & {
last_triggered: string; last_triggered: string;
mode: "single" | "restart" | "queued" | "parallel"; mode: typeof MODES[number];
current?: number; current?: number;
max?: number; max?: number;
}; };
@ -23,7 +23,7 @@ export interface ScriptConfig {
alias: string; alias: string;
sequence: Action[]; sequence: Action[];
icon?: string; icon?: string;
mode?: "single" | "restart" | "queued" | "parallel"; mode?: typeof MODES[number];
max?: number; max?: number;
} }

View File

@ -2,9 +2,11 @@ export type Selector =
| EntitySelector | EntitySelector
| DeviceSelector | DeviceSelector
| AreaSelector | AreaSelector
| TargetSelector
| NumberSelector | NumberSelector
| BooleanSelector | BooleanSelector
| TimeSelector; | TimeSelector
| ActionSelector;
export interface EntitySelector { export interface EntitySelector {
entity: { entity: {
@ -19,13 +21,41 @@ export interface DeviceSelector {
integration?: string; integration?: string;
manufacturer?: string; manufacturer?: string;
model?: string; model?: string;
entity?: EntitySelector["entity"]; entity?: {
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
}; };
} }
export interface AreaSelector { export interface AreaSelector {
// eslint-disable-next-line @typescript-eslint/ban-types area: {
area: {}; entity?: {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
};
}
export interface TargetSelector {
target: {
entity?: {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
};
} }
export interface NumberSelector { export interface NumberSelector {
@ -47,3 +77,8 @@ export interface TimeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
time: {}; time: {};
} }
export interface ActionSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
action: {};
}

5
src/data/target.ts Normal file
View File

@ -0,0 +1,5 @@
export interface Target {
entity_id?: string[];
device_id?: string[];
area_id?: string[];
}

View File

@ -9,6 +9,7 @@ export const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN];
export interface User { export interface User {
id: string; id: string;
username: string | null;
name: string; name: string;
is_owner: boolean; is_owner: boolean;
is_active: boolean; is_active: boolean;
@ -19,6 +20,7 @@ export interface User {
export interface UpdateUserParams { export interface UpdateUserParams {
name?: User["name"]; name?: User["name"];
is_active?: User["is_active"];
group_ids?: User["group_ids"]; group_ids?: User["group_ids"];
} }

View File

@ -27,6 +27,7 @@ export interface ZHADevice {
device_type: string; device_type: string;
signature: any; signature: any;
neighbors: Neighbor[]; neighbors: Neighbor[];
pairing_status?: string;
} }
export interface Neighbor { export interface Neighbor {
@ -270,3 +271,23 @@ export const addGroup = (
group_name: groupName, group_name: groupName,
members: membersToAdd, members: membersToAdd,
}); });
export const INITIALIZED = "INITIALIZED";
export const INTERVIEW_COMPLETE = "INTERVIEW_COMPLETE";
export const CONFIGURED = "CONFIGURED";
export const PAIRED = "PAIRED";
export const INCOMPLETE_PAIRING_STATUSES = [
PAIRED,
CONFIGURED,
INTERVIEW_COMPLETE,
];
export const DEVICE_JOINED = "device_joined";
export const RAW_DEVICE_INITIALIZED = "raw_device_initialized";
export const DEVICE_FULLY_INITIALIZED = "device_fully_initialized";
export const DEVICE_MESSAGE_TYPES = [
DEVICE_JOINED,
RAW_DEVICE_INITIALIZED,
DEVICE_FULLY_INITIALIZED,
];
export const LOG_OUTPUT = "log_output";

View File

@ -46,7 +46,7 @@ export class HuiNotificationItemTemplate extends LitElement {
} }
.actions { .actions {
border-top: 1px solid #e8e8e8; border-top: 1px solid var(--divider-color, #e8e8e8);
padding: 5px 16px; padding: 5px 16px;
} }

View File

@ -42,7 +42,7 @@
#ha-init-skeleton::before { #ha-init-skeleton::before {
display: block; display: block;
content: ""; content: "";
height: 112px; height: 56px;
background-color: #THEMEC; background-color: #THEMEC;
} }
html { html {

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import { mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@ -124,7 +124,7 @@ export class HaConfigAreasDashboard extends LitElement {
icon="hass:help-circle" icon="hass:help-circle"
@click=${this._showHelp} @click=${this._showHelp}
></ha-icon-button> ></ha-icon-button>
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.areas.picker.create_area" "ui.panel.config.areas.picker.create_area"
@ -133,7 +133,7 @@ export class HaConfigAreasDashboard extends LitElement {
@click=${this._createArea} @click=${this._createArea}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }

View File

@ -39,7 +39,7 @@ export class HaWaitForTriggerAction extends LitElement
)} )}
> >
<ha-switch <ha-switch
.checked=${continue_on_timeout} .checked=${continue_on_timeout ?? true}
@change=${this._continueChanged} @change=${this._continueChanged}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>

View File

@ -63,7 +63,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
protected render() { protected render() {
const blueprint = this._blueprint; const blueprint = this._blueprint;
return html`<ha-config-section .isWide=${this.isWide}> return html`<ha-config-section vertical .isWide=${this.isWide}>
${!this.narrow ${!this.narrow
? html` <span slot="header">${this.config.alias}</span> ` ? html` <span slot="header">${this.config.alias}</span> `
: ""} : ""}
@ -119,7 +119,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
</ha-card> </ha-card>
</ha-config-section> </ha-config-section>
<ha-config-section .isWide=${this.isWide}> <ha-config-section vertical .isWide=${this.isWide}>
<span slot="header" <span slot="header"
>${this.hass.localize( >${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.header" "ui.panel.config.automation.editor.blueprint.header"
@ -185,6 +185,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
></ha-selector>` ></ha-selector>`
: html`<paper-input : html`<paper-input
.key=${key} .key=${key}
required
.value=${this.config.use_blueprint.input && .value=${this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]} this.config.use_blueprint.input[key]}
@value-changed=${this._inputChanged} @value-changed=${this._inputChanged}
@ -275,20 +276,9 @@ export class HaBlueprintAutomationEditor extends LitElement {
return [ return [
haStyle, haStyle,
css` css`
ha-card {
overflow: hidden;
}
.errors {
padding: 20px;
font-weight: bold;
color: var(--error-color);
}
.padding { .padding {
padding: 16px; padding: 16px;
} }
.content {
padding-bottom: 20px;
}
.blueprint-picker-container { .blueprint-picker-container {
padding: 16px; padding: 16px;
display: flex; display: flex;
@ -312,24 +302,10 @@ export class HaBlueprintAutomationEditor extends LitElement {
border-top: 1px solid var(--divider-color); border-top: 1px solid var(--divider-color);
} }
:host(:not([narrow])) ha-settings-row paper-input { :host(:not([narrow])) ha-settings-row paper-input {
width: 50%; width: 60%;
} }
:host(:not([narrow])) ha-settings-row ha-selector { :host(:not([narrow])) ha-settings-row ha-selector {
width: 50%; width: 60%;
}
mwc-fab {
position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s;
}
mwc-fab.dirty {
bottom: 0;
}
.selected_menu_item {
color: var(--primary-color);
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
} }
`, `,
]; ];

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import { import {
mdiCheck, mdiCheck,
mdiContentDuplicate, mdiContentDuplicate,
@ -206,6 +206,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
? html`<blueprint-automation-editor ? html`<blueprint-automation-editor
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj} .stateObj=${stateObj}
.config=${this._config} .config=${this._config}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@ -213,6 +214,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
: html`<manual-automation-editor : html`<manual-automation-editor
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj} .stateObj=${stateObj}
.config=${this._config} .config=${this._config}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@ -271,7 +273,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
</div> </div>
` `
: ""} : ""}
<mwc-fab <ha-fab
slot="fab" slot="fab"
class=${classMap({ dirty: this._dirty })} class=${classMap({ dirty: this._dirty })}
.label=${this.hass.localize("ui.panel.config.automation.editor.save")} .label=${this.hass.localize("ui.panel.config.automation.editor.save")}
@ -279,7 +281,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
@click=${this._saveAutomation} @click=${this._saveAutomation}
> >
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
} }
@ -524,21 +526,18 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
.content { .content {
padding-bottom: 20px; padding-bottom: 20px;
} }
span[slot="introduction"] a {
color: var(--primary-color);
}
p { p {
margin-bottom: 0; margin-bottom: 0;
} }
ha-entity-toggle { ha-entity-toggle {
margin-right: 8px; margin-right: 8px;
} }
mwc-fab { ha-fab {
position: relative; position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom)); bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s; transition: bottom 0.3s;
} }
mwc-fab.dirty { ha-fab.dirty {
bottom: 0; bottom: 0;
} }
.selected_menu_item { .selected_menu_item {

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import { mdiPlus, mdiHelpCircle } from "@mdi/js"; import { mdiPlus, mdiHelpCircle } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
@ -170,7 +170,7 @@ class HaAutomationPicker extends LitElement {
<mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}> <mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}>
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.picker.add_automation" "ui.panel.config.automation.picker.add_automation"
@ -179,7 +179,7 @@ class HaAutomationPicker extends LitElement {
@click=${this._createNew} @click=${this._createNew}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }

View File

@ -309,14 +309,6 @@ export class HaManualAutomationEditor extends LitElement {
ha-card { ha-card {
overflow: hidden; overflow: hidden;
} }
.errors {
padding: 20px;
font-weight: bold;
color: var(--error-color);
}
.content {
padding-bottom: 20px;
}
span[slot="introduction"] a { span[slot="introduction"] a {
color: var(--primary-color); color: var(--primary-color);
} }
@ -326,20 +318,6 @@ export class HaManualAutomationEditor extends LitElement {
ha-entity-toggle { ha-entity-toggle {
margin-right: 8px; margin-right: 8px;
} }
mwc-fab {
position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s;
}
mwc-fab.dirty {
bottom: 0;
}
.selected_menu_item {
color: var(--primary-color);
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`, `,
]; ];
} }

View File

@ -4,7 +4,6 @@ import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import { import {
css,
CSSResult, CSSResult,
customElement, customElement,
html, html,
@ -97,12 +96,11 @@ class DialogImportBlueprint extends LitElement {
)} )}
></paper-input> ></paper-input>
`} `}
<ha-expansion-panel> <ha-expansion-panel
<span slot="title" .header=${this.hass.localize(
>${this.hass.localize( "ui.panel.config.blueprint.add.raw_blueprint"
"ui.panel.config.blueprint.add.raw_blueprint" )}
)}</span >
>
<pre>${this._result.raw_data}</pre> <pre>${this._result.raw_data}</pre>
</ha-expansion-panel>` </ha-expansion-panel>`
: html`${this.hass.localize( : html`${this.hass.localize(
@ -201,15 +199,8 @@ class DialogImportBlueprint extends LitElement {
} }
} }
static get styles(): CSSResult[] { static get styles(): CSSResult {
return [ return haStyleDialog;
haStyleDialog,
css`
ha-expansion-panel {
--expansion-panel-summary-padding: 0;
}
`,
];
} }
} }

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import { mdiPlus, mdiHelpCircle, mdiDelete, mdiRobot } from "@mdi/js"; import { mdiPlus, mdiHelpCircle, mdiDelete, mdiRobot } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
@ -174,7 +174,7 @@ class HaBlueprintOverview extends LitElement {
<mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}> <mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}>
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.blueprint.overview.add_blueprint" "ui.panel.config.blueprint.overview.add_blueprint"
@ -183,7 +183,7 @@ class HaBlueprintOverview extends LitElement {
@click=${this._addBlueprint} @click=${this._addBlueprint}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }

View File

@ -8,7 +8,6 @@ import {
html, html,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
@ -31,7 +30,7 @@ export class HaDeviceEntitiesCard extends LitElement {
@property() public entities!: EntityRegistryStateEntry[]; @property() public entities!: EntityRegistryStateEntry[];
@internalProperty() private _showDisabled = false; @property() public showDisabled = false;
private _entityRows: Array<LovelaceRow | HuiErrorCard> = []; private _entityRows: Array<LovelaceRow | HuiErrorCard> = [];
@ -68,7 +67,7 @@ export class HaDeviceEntitiesCard extends LitElement {
})} })}
</div> </div>
${disabledEntities.length ${disabledEntities.length
? !this._showDisabled ? !this.showDisabled
? html` ? html`
<button <button
class="show-more" class="show-more"
@ -119,7 +118,7 @@ export class HaDeviceEntitiesCard extends LitElement {
} }
private _toggleShowDisabled() { private _toggleShowDisabled() {
this._showDisabled = !this._showDisabled; this.showDisabled = !this.showDisabled;
} }
private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult { private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult {
@ -227,3 +226,9 @@ export class HaDeviceEntitiesCard extends LitElement {
`; `;
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ha-device-entities-card": HaDeviceEntitiesCard;
}
}

View File

@ -12,7 +12,7 @@ import {
computeDeviceName, computeDeviceName,
DeviceRegistryEntry, DeviceRegistryEntry,
} from "../../../../data/device_registry"; } from "../../../../data/device_registry";
import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; import { loadDeviceRegistryDetailDialog } from "../device-registry-detail/show-dialog-device-registry-detail";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@customElement("ha-device-info-card") @customElement("ha-device-info-card")

View File

@ -3,8 +3,8 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import "../../components/ha-dialog"; import "../../../../components/ha-dialog";
import "../../components/ha-area-picker"; import "../../../../components/ha-area-picker";
import { import {
CSSResult, CSSResult,
@ -18,11 +18,12 @@ import {
} from "lit-element"; } from "lit-element";
import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail"; import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../../../types";
import { PolymerChangedEvent } from "../../polymer-types"; import type { HaSwitch } from "../../../../components/ha-switch";
import { computeDeviceName } from "../../data/device_registry"; import { PolymerChangedEvent } from "../../../../polymer-types";
import { fireEvent } from "../../common/dom/fire_event"; import { computeDeviceName } from "../../../../data/device_registry";
import { haStyleDialog } from "../../resources/styles"; import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
@customElement("dialog-device-registry-detail") @customElement("dialog-device-registry-detail")
class DialogDeviceRegistryDetail extends LitElement { class DialogDeviceRegistryDetail extends LitElement {
@ -36,6 +37,8 @@ class DialogDeviceRegistryDetail extends LitElement {
@internalProperty() private _areaId?: string; @internalProperty() private _areaId?: string;
@internalProperty() private _disabledBy!: string | null;
@internalProperty() private _submitting?: boolean; @internalProperty() private _submitting?: boolean;
public async showDialog( public async showDialog(
@ -45,6 +48,7 @@ class DialogDeviceRegistryDetail extends LitElement {
this._error = undefined; this._error = undefined;
this._nameByUser = this._params.device.name_by_user || ""; this._nameByUser = this._params.device.name_by_user || "";
this._areaId = this._params.device.area_id; this._areaId = this._params.device.area_id;
this._disabledBy = this._params.device.disabled_by;
await this.updateComplete; await this.updateComplete;
} }
@ -80,6 +84,32 @@ class DialogDeviceRegistryDetail extends LitElement {
.value=${this._areaId} .value=${this._areaId}
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
></ha-area-picker> ></ha-area-picker>
<div class="row">
<ha-switch
.checked=${!this._disabledBy}
@change=${this._disabledByChanged}
>
</ha-switch>
<div>
<div>
${this.hass.localize("ui.panel.config.devices.enabled_label")}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.panel.config.devices.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
${this.hass.localize(
"ui.panel.config.devices.enabled_description"
)}
</div>
</div>
</div>
</div> </div>
</div> </div>
<mwc-button <mwc-button
@ -109,12 +139,17 @@ class DialogDeviceRegistryDetail extends LitElement {
this._areaId = event.detail.value; this._areaId = event.detail.value;
} }
private _disabledByChanged(ev: Event): void {
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
}
private async _updateEntry(): Promise<void> { private async _updateEntry(): Promise<void> {
this._submitting = true; this._submitting = true;
try { try {
await this._params!.updateEntry({ await this._params!.updateEntry({
name_by_user: this._nameByUser.trim() || null, name_by_user: this._nameByUser.trim() || null,
area_id: this._areaId || null, area_id: this._areaId || null,
disabled_by: this._disabledBy || null,
}); });
this._params = undefined; this._params = undefined;
} catch (err) { } catch (err) {
@ -128,6 +163,7 @@ class DialogDeviceRegistryDetail extends LitElement {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyle,
haStyleDialog, haStyleDialog,
css` css`
.form { .form {
@ -139,6 +175,15 @@ class DialogDeviceRegistryDetail extends LitElement {
.error { .error {
color: var(--error-color); color: var(--error-color);
} }
ha-switch {
margin-right: 16px;
}
.row {
margin-top: 8px;
color: var(--primary-text-color);
display: flex;
align-items: center;
}
`, `,
]; ];
} }

View File

@ -1,8 +1,8 @@
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { import {
DeviceRegistryEntry, DeviceRegistryEntry,
DeviceRegistryEntryMutableParams, DeviceRegistryEntryMutableParams,
} from "../../data/device_registry"; } from "../../../../data/device_registry";
export interface DeviceRegistryDetailDialogParams { export interface DeviceRegistryDetailDialogParams {
device: DeviceRegistryEntry; device: DeviceRegistryEntry;

View File

@ -35,7 +35,7 @@ import { findRelated, RelatedResult } from "../../../data/search";
import { import {
loadDeviceRegistryDetailDialog, loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog,
} from "../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; } from "./device-registry-detail/show-dialog-device-registry-detail";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-tabs-subpage";
@ -46,6 +46,7 @@ import "./device-detail/ha-device-entities-card";
import "./device-detail/ha-device-info-card"; import "./device-detail/ha-device-info-card";
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation"; import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
import { haStyle } from "../../../resources/styles";
export interface EntityRegistryStateEntry extends EntityRegistryEntry { export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null; stateName?: string | null;
@ -246,6 +247,28 @@ export class HaConfigDevicePage extends LitElement {
.devices=${this.devices} .devices=${this.devices}
.device=${device} .device=${device}
> >
${
device.disabled_by
? html`
<div>
<p class="warning">
${this.hass.localize(
"ui.panel.config.devices.enabled_cause",
"cause",
this.hass.localize(
`ui.panel.config.devices.disabled_by.${device.disabled_by}`
)
)}
</p>
</div>
<div class="card-actions" slot="actions">
<mwc-button unelevated @click=${this._enableDevice}>
${this.hass.localize("ui.common.enable")}
</mwc-button>
</div>
`
: html``
}
${this._renderIntegrationInfo(device, integrations)} ${this._renderIntegrationInfo(device, integrations)}
</ha-device-info-card> </ha-device-info-card>
@ -255,6 +278,7 @@ export class HaConfigDevicePage extends LitElement {
<ha-device-entities-card <ha-device-entities-card
.hass=${this.hass} .hass=${this.hass}
.entities=${entities} .entities=${entities}
.showDisabled=${device.disabled_by !== null}
> >
</ha-device-entities-card> </ha-device-entities-card>
` `
@ -272,9 +296,14 @@ export class HaConfigDevicePage extends LitElement {
)} )}
<ha-icon-button <ha-icon-button
@click=${this._showAutomationDialog} @click=${this._showAutomationDialog}
title=${this.hass.localize( .disabled=${device.disabled_by}
"ui.panel.config.devices.automation.create" title=${device.disabled_by
)} ? this.hass.localize(
"ui.panel.config.devices.automation.create_disabled"
)
: this.hass.localize(
"ui.panel.config.devices.automation.create"
)}
icon="hass:plus-circle" icon="hass:plus-circle"
></ha-icon-button> ></ha-icon-button>
</h1> </h1>
@ -342,9 +371,16 @@ export class HaConfigDevicePage extends LitElement {
<ha-icon-button <ha-icon-button
@click=${this._createScene} @click=${this._createScene}
title=${this.hass.localize( .disabled=${device.disabled_by}
"ui.panel.config.devices.scene.create" title=${
)} device.disabled_by
? this.hass.localize(
"ui.panel.config.devices.scene.create_disabled"
)
: this.hass.localize(
"ui.panel.config.devices.scene.create"
)
}
icon="hass:plus-circle" icon="hass:plus-circle"
></ha-icon-button> ></ha-icon-button>
</h1> </h1>
@ -415,9 +451,14 @@ export class HaConfigDevicePage extends LitElement {
)} )}
<ha-icon-button <ha-icon-button
@click=${this._showScriptDialog} @click=${this._showScriptDialog}
title=${this.hass.localize( .disabled=${device.disabled_by}
"ui.panel.config.devices.script.create" title=${device.disabled_by
)} ? this.hass.localize(
"ui.panel.config.devices.script.create_disabled"
)
: this.hass.localize(
"ui.panel.config.devices.script.create"
)}
icon="hass:plus-circle" icon="hass:plus-circle"
></ha-icon-button> ></ha-icon-button>
</h1> </h1>
@ -632,128 +673,137 @@ export class HaConfigDevicePage extends LitElement {
}); });
} }
static get styles(): CSSResult { private async _enableDevice(): Promise<void> {
return css` await updateDeviceRegistryEntry(this.hass, this.deviceId, {
.container { disabled_by: null,
display: flex; });
flex-wrap: wrap; }
margin: auto;
max-width: 1000px;
margin-top: 32px;
margin-bottom: 32px;
}
.card-header { static get styles(): CSSResult[] {
display: flex; return [
align-items: center; haStyle,
justify-content: space-between; css`
} .container {
display: flex;
flex-wrap: wrap;
margin: auto;
max-width: 1000px;
margin-top: 32px;
margin-bottom: 32px;
}
.card-header ha-icon-button { .card-header {
margin-right: -8px; display: flex;
color: var(--primary-color); align-items: center;
height: auto; justify-content: space-between;
} }
.device-info { .card-header ha-icon-button {
padding: 16px; margin-right: -8px;
} color: var(--primary-color);
height: auto;
}
.show-more { .device-info {
} padding: 16px;
}
h1 { .show-more {
margin: 0; }
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
opacity: var(--dark-primary-opacity);
}
.header { h1 {
display: flex; margin: 0;
justify-content: space-between; font-family: var(--paper-font-headline_-_font-family);
} -webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
opacity: var(--dark-primary-opacity);
}
.column, .header {
.fullwidth { display: flex;
padding: 8px; justify-content: space-between;
box-sizing: border-box; }
}
.column {
width: 33%;
flex-grow: 1;
}
.fullwidth {
width: 100%;
flex-grow: 1;
}
.header-right { .column,
align-self: center; .fullwidth {
} padding: 8px;
box-sizing: border-box;
}
.column {
width: 33%;
flex-grow: 1;
}
.fullwidth {
width: 100%;
flex-grow: 1;
}
.header-right img { .header-right {
height: 30px; align-self: center;
} }
.header-right { .header-right img {
display: flex; height: 30px;
} }
.header-right:first-child { .header-right {
width: 100%; display: flex;
justify-content: flex-end; }
}
.header-right > *:not(:first-child) { .header-right:first-child {
margin-left: 16px; width: 100%;
} justify-content: flex-end;
}
.battery { .header-right > *:not(:first-child) {
align-self: center; margin-left: 16px;
align-items: center; }
display: flex;
}
.column > *:not(:first-child) { .battery {
margin-top: 16px; align-self: center;
} align-items: center;
display: flex;
}
:host([narrow]) .column { .column > *:not(:first-child) {
width: 100%; margin-top: 16px;
} }
:host([narrow]) .container { :host([narrow]) .column {
margin-top: 0; width: 100%;
} }
paper-item { :host([narrow]) .container {
cursor: pointer; margin-top: 0;
font-size: var(--paper-font-body1_-_font-size); }
}
paper-item.no-link { paper-item {
cursor: default; cursor: pointer;
} font-size: var(--paper-font-body1_-_font-size);
}
a { paper-item.no-link {
text-decoration: none; cursor: default;
color: var(--primary-color); }
}
ha-card { a {
padding-bottom: 8px; text-decoration: none;
} color: var(--primary-color);
}
ha-card a { ha-card {
color: var(--primary-text-color); padding-bottom: 8px;
} }
`;
ha-card a {
color: var(--primary-text-color);
}
`,
];
} }
} }

View File

@ -1,4 +1,9 @@
import { mdiPlus, mdiFilterVariant, mdiCancel } from "@mdi/js";
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-tooltip/paper-tooltip";
import { import {
css,
CSSResult,
customElement, customElement,
html, html,
internalProperty, internalProperty,
@ -6,16 +11,20 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { HASSDomEvent } from "../../../common/dom/fire_event"; import { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize"; import { LocalizeFunc } from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl";
import { import {
DataTableColumnContainer, DataTableColumnContainer,
DataTableRowData, DataTableRowData,
RowClickedEvent, RowClickedEvent,
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-battery-icon"; import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-button-menu";
import { AreaRegistryEntry } from "../../../data/area_registry"; import { AreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry } from "../../../data/config_entries"; import { ConfigEntry } from "../../../data/config_entries";
import { import {
@ -32,6 +41,7 @@ import { domainToName } from "../../../data/integration";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { haStyle } from "../../../resources/styles";
interface DeviceRowData extends DeviceRegistryEntry { interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData; device?: DeviceRowData;
@ -62,6 +72,12 @@ export class HaConfigDeviceDashboard extends LitElement {
window.location.search window.location.search
); );
@internalProperty() private _showDisabled = false;
@internalProperty() private _filter = "";
@internalProperty() private _numHiddenDevices = 0;
private _activeFilters = memoizeOne( private _activeFilters = memoizeOne(
( (
entries: ConfigEntry[], entries: ConfigEntry[],
@ -72,6 +88,10 @@ export class HaConfigDeviceDashboard extends LitElement {
filters.forEach((value, key) => { filters.forEach((value, key) => {
switch (key) { switch (key) {
case "config_entry": { case "config_entry": {
// If we are requested to show the devices for a given config entry,
// also show the disabled ones by default.
this._showDisabled = true;
const configEntry = entries.find( const configEntry = entries.find(
(entry) => entry.entry_id === value (entry) => entry.entry_id === value
); );
@ -96,13 +116,14 @@ export class HaConfigDeviceDashboard extends LitElement {
} }
); );
private _devices = memoizeOne( private _devicesAndFilterDomains = memoizeOne(
( (
devices: DeviceRegistryEntry[], devices: DeviceRegistryEntry[],
entries: ConfigEntry[], entries: ConfigEntry[],
entities: EntityRegistryEntry[], entities: EntityRegistryEntry[],
areas: AreaRegistryEntry[], areas: AreaRegistryEntry[],
filters: URLSearchParams, filters: URLSearchParams,
showDisabled: boolean,
localize: LocalizeFunc localize: LocalizeFunc
) => { ) => {
// Some older installations might have devices pointing at invalid entryIDs // Some older installations might have devices pointing at invalid entryIDs
@ -115,6 +136,9 @@ export class HaConfigDeviceDashboard extends LitElement {
deviceLookup[device.id] = device; deviceLookup[device.id] = device;
} }
// If nothing gets filtered, this is our correct count of devices
let startLength = outputDevices.length;
const deviceEntityLookup: DeviceEntityLookup = {}; const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) { for (const entity of entities) {
if (!entity.device_id) { if (!entity.device_id) {
@ -136,16 +160,25 @@ export class HaConfigDeviceDashboard extends LitElement {
areaLookup[area.area_id] = area; areaLookup[area.area_id] = area;
} }
const filterDomains: string[] = [];
filters.forEach((value, key) => { filters.forEach((value, key) => {
switch (key) { if (key === "config_entry") {
case "config_entry": outputDevices = outputDevices.filter((device) =>
outputDevices = outputDevices.filter((device) => device.config_entries.includes(value)
device.config_entries.includes(value) );
); startLength = outputDevices.length;
break; const configEntry = entries.find((entry) => entry.entry_id === value);
if (configEntry) {
filterDomains.push(configEntry.domain);
}
} }
}); });
if (!showDisabled) {
outputDevices = outputDevices.filter((device) => !device.disabled_by);
}
outputDevices = outputDevices.map((device) => { outputDevices = outputDevices.map((device) => {
return { return {
...device, ...device,
@ -176,16 +209,19 @@ export class HaConfigDeviceDashboard extends LitElement {
}; };
}); });
return outputDevices; this._numHiddenDevices = startLength - outputDevices.length;
return { devicesOutput: outputDevices, filteredDomains: filterDomains };
} }
); );
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer => { (narrow: boolean, showDisabled: boolean): DataTableColumnContainer => {
const columns: DataTableColumnContainer = narrow const columns: DataTableColumnContainer = narrow
? { ? {
name: { name: {
title: "Device", title: this.hass.localize(
"ui.panel.config.devices.data_table.device"
),
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
@ -271,6 +307,24 @@ export class HaConfigDeviceDashboard extends LitElement {
: html` - `; : html` - `;
}, },
}; };
if (showDisabled) {
columns.disabled_by = {
title: "",
type: "icon",
template: (disabled_by) =>
disabled_by
? html`<div
tabindex="0"
style="display:inline-block; position: relative;"
>
<ha-svg-icon .path=${mdiCancel}></ha-svg-icon>
<paper-tooltip animation-delay="0" position="left">
${this.hass.localize("ui.panel.config.devices.disabled")}
</paper-tooltip>
</div>`
: "",
};
}
return columns; return columns;
} }
); );
@ -286,6 +340,126 @@ export class HaConfigDeviceDashboard extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const { devicesOutput, filteredDomains } = this._devicesAndFilterDomains(
this.devices,
this.entries,
this.entities,
this.areas,
this._searchParms,
this._showDisabled,
this.hass.localize
);
const includeZHAFab = filteredDomains.includes("zha");
const activeFilters = this._activeFilters(
this.entries,
this._searchParms,
this.hass.localize
);
const headerToolbar = html`
<search-input
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.devices.picker.search")}
></search-input
>${activeFilters
? html`<div class="active-filters">
${this.narrow
? html` <div>
<ha-icon icon="hass:filter-variant"></ha-icon>
<paper-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)}
${activeFilters.join(", ")}
${this._numHiddenDevices
? "(" +
this.hass.localize(
"ui.panel.config.devices.picker.filter.hidden_devices",
"number",
this._numHiddenDevices
) +
")"
: ""}
</paper-tooltip>
</div>`
: `${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)} ${activeFilters.join(", ")}
${
this._numHiddenDevices
? "(" +
this.hass.localize(
"ui.panel.config.devices.picker.filter.hidden_devices",
"number",
this._numHiddenDevices
) +
")"
: ""
}
`}
<mwc-button @click=${this._clearFilter}
>${this.hass.localize(
"ui.panel.config.filtering.clear"
)}</mwc-button
>
</div>`
: ""}
${this._numHiddenDevices && !activeFilters
? html`<div class="active-filters">
${this.narrow
? html` <div>
<ha-icon icon="hass:filter-variant"></ha-icon>
<paper-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.devices.picker.filter.hidden_devices",
"number",
this._numHiddenDevices
)}
</paper-tooltip>
</div>`
: `${this.hass.localize(
"ui.panel.config.devices.picker.filter.hidden_devices",
"number",
this._numHiddenDevices
)}`}
<mwc-button @click=${this._showAll}
>${this.hass.localize(
"ui.panel.config.devices.picker.filter.show_all"
)}</mwc-button
>
</div>`
: ""}
<ha-button-menu corner="BOTTOM_START" multi>
<mwc-icon-button
slot="trigger"
.label=${this.hass!.localize(
"ui.panel.config.devices.picker.filter.filter"
)}
.title=${this.hass!.localize(
"ui.panel.config.devices.picker.filter.filter"
)}
>
<ha-svg-icon .path=${mdiFilterVariant}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item
@request-selected="${this._showDisabledChanged}"
graphic="control"
.selected=${this._showDisabled}
>
<ha-checkbox
slot="graphic"
.checked=${this._showDisabled}
></ha-checkbox>
${this.hass!.localize(
"ui.panel.config.devices.picker.filter.show_disabled"
)}
</mwc-list-item>
</ha-button-menu>
`;
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
@ -295,23 +469,33 @@ export class HaConfigDeviceDashboard extends LitElement {
: "/config"} : "/config"}
.tabs=${configSections.integrations} .tabs=${configSections.integrations}
.route=${this.route} .route=${this.route}
.columns=${this._columns(this.narrow)} .columns=${this._columns(this.narrow, this._showDisabled)}
.data=${this._devices( .data=${devicesOutput}
this.devices, .filter=${this._filter}
this.entries,
this.entities,
this.areas,
this._searchParms,
this.hass.localize
)}
.activeFilters=${this._activeFilters(
this.entries,
this._searchParms,
this.hass.localize
)}
@row-click=${this._handleRowClicked} @row-click=${this._handleRowClicked}
clickable clickable
.hasFab=${includeZHAFab}
> >
${includeZHAFab
? html`<a href="/config/zha/add" slot="fab">
<ha-fab
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
extended
?rtl=${computeRTL(this.hass)}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</a>`
: html``}
<div
class=${classMap({
"search-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
${headerToolbar}
</div>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }
@ -342,6 +526,136 @@ export class HaConfigDeviceDashboard extends LitElement {
const deviceId = ev.detail.id; const deviceId = ev.detail.id;
navigate(this, `/config/devices/device/${deviceId}`); navigate(this, `/config/devices/device/${deviceId}`);
} }
private _showDisabledChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
}
this._showDisabled = ev.detail.selected;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
private _clearFilter() {
navigate(this, window.location.pathname, true);
}
private _showAll() {
this._showDisabled = true;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
hass-loading-screen {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
}
a {
color: var(--primary-color);
}
h2 {
margin-top: 0;
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
opacity: var(--dark-primary-opacity);
}
p {
font-family: var(--paper-font-subhead_-_font-family);
-webkit-font-smoothing: var(
--paper-font-subhead_-_-webkit-font-smoothing
);
font-weight: var(--paper-font-subhead_-_font-weight);
line-height: var(--paper-font-subhead_-_line-height);
}
ha-data-table {
width: 100%;
--data-table-border-width: 0;
}
:host(:not([narrow])) ha-data-table {
height: calc(100vh - 1px - var(--header-height));
display: block;
}
ha-button-menu {
margin-right: 8px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
search-input {
margin-left: 16px;
flex-grow: 1;
position: relative;
top: 2px;
}
.search-toolbar search-input {
margin-left: 8px;
top: 1px;
}
.search-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--secondary-text-color);
}
.search-toolbar ha-button-menu {
position: static;
}
.selected-txt {
font-weight: bold;
padding-left: 16px;
}
.table-header .selected-txt {
margin-top: 20px;
}
.search-toolbar .selected-txt {
font-size: 16px;
}
.header-btns > mwc-button,
.header-btns > ha-icon-button {
margin: 8px;
}
.active-filters {
color: var(--primary-text-color);
position: relative;
display: flex;
align-items: center;
padding: 2px 2px 2px 8px;
margin-left: 4px;
font-size: 14px;
}
.active-filters ha-icon {
color: var(--primary-color);
}
.active-filters mwc-button {
margin-left: 8px;
}
.active-filters::before {
background-color: var(--primary-color);
opacity: 0.12;
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: "";
}
`,
];
}
} }
declare global { declare global {

View File

@ -20,9 +20,16 @@ import {
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import type { PolymerChangedEvent } from "../../../polymer-types"; import type { PolymerChangedEvent } from "../../../polymer-types";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../../../components/ha-area-picker";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
@customElement("ha-registry-basic-editor") @customElement("ha-registry-basic-editor")
export class HaEntityRegistryBasicEditor extends LitElement { export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public entry!: ExtEntityRegistryEntry; @property() public entry!: ExtEntityRegistryEntry;
@ -31,16 +38,26 @@ export class HaEntityRegistryBasicEditor extends LitElement {
@internalProperty() private _entityId!: string; @internalProperty() private _entityId!: string;
@internalProperty() private _areaId?: string;
@internalProperty() private _disabledBy!: string | null; @internalProperty() private _disabledBy!: string | null;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
@internalProperty() private _device?: DeviceRegistryEntry;
@internalProperty() private _submitting?: boolean; @internalProperty() private _submitting?: boolean;
public async updateEntry(): Promise<void> { public async updateEntry(): Promise<void> {
this._submitting = true; this._submitting = true;
const params: Partial<EntityRegistryEntryUpdateParams> = { const params: Partial<EntityRegistryEntryUpdateParams> = {
new_entity_id: this._entityId.trim(), new_entity_id: this._entityId.trim(),
area_id: this._areaId || null,
}; };
if (this._disabledBy === null || this._disabledBy === "user") { if (
this.entry.disabled_by !== this._disabledBy &&
(this._disabledBy === null || this._disabledBy === "user")
) {
params.disabled_by = this._disabledBy; params.disabled_by = this._disabledBy;
} }
try { try {
@ -70,6 +87,20 @@ export class HaEntityRegistryBasicEditor extends LitElement {
} }
} }
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._deviceLookup = {};
for (const device of devices) {
this._deviceLookup[device.id] = device;
}
if (!this._device && this.entry.device_id) {
this._device = this._deviceLookup[this.entry.device_id];
}
}),
];
}
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties); super.updated(changedProperties);
if (!changedProperties.has("entry")) { if (!changedProperties.has("entry")) {
@ -79,6 +110,11 @@ export class HaEntityRegistryBasicEditor extends LitElement {
this._origEntityId = this.entry.entity_id; this._origEntityId = this.entry.entity_id;
this._entityId = this.entry.entity_id; this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by; this._disabledBy = this.entry.disabled_by;
this._areaId = this.entry.area_id;
this._device =
this.entry.device_id && this._deviceLookup
? this._deviceLookup[this.entry.device_id]
: undefined;
} }
} }
@ -105,6 +141,12 @@ export class HaEntityRegistryBasicEditor extends LitElement {
.invalid=${invalidDomainUpdate} .invalid=${invalidDomainUpdate}
.disabled=${this._submitting} .disabled=${this._submitting}
></paper-input> ></paper-input>
<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
.placeholder=${this._device?.area_id}
@value-changed=${this._areaPicked}
></ha-area-picker>
<div class="row"> <div class="row">
<ha-switch <ha-switch
.checked=${!this._disabledBy} .checked=${!this._disabledBy}
@ -139,6 +181,10 @@ export class HaEntityRegistryBasicEditor extends LitElement {
`; `;
} }
private _areaPicked(ev: CustomEvent) {
this._areaId = ev.detail.value;
}
private _entityIdChanged(ev: PolymerChangedEvent<string>): void { private _entityIdChanged(ev: PolymerChangedEvent<string>): void {
this._entityId = ev.detail.value; this._entityId = ev.detail.value;
} }

View File

@ -1,6 +1,6 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResult, CSSResult,
@ -31,9 +31,18 @@ import type { PolymerChangedEvent } from "../../../polymer-types";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { domainIcon } from "../../../common/entity/domain_icon"; import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-area-picker";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
updateDeviceRegistryEntry,
} from "../../../data/device_registry";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import "../../../components/ha-expansion-panel";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
@customElement("entity-registry-settings") @customElement("entity-registry-settings")
export class EntityRegistrySettings extends LitElement { export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public entry!: ExtEntityRegistryEntry; @property() public entry!: ExtEntityRegistryEntry;
@ -44,14 +53,34 @@ export class EntityRegistrySettings extends LitElement {
@internalProperty() private _entityId!: string; @internalProperty() private _entityId!: string;
@internalProperty() private _areaId?: string | null;
@internalProperty() private _disabledBy!: string | null; @internalProperty() private _disabledBy!: string | null;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
@internalProperty() private _device?: DeviceRegistryEntry;
@internalProperty() private _error?: string; @internalProperty() private _error?: string;
@internalProperty() private _submitting?: boolean; @internalProperty() private _submitting?: boolean;
private _origEntityId!: string; private _origEntityId!: string;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._deviceLookup = {};
for (const device of devices) {
this._deviceLookup[device.id] = device;
}
if (this.entry.device_id) {
this._device = this._deviceLookup[this.entry.device_id];
}
}),
];
}
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties); super.updated(changedProperties);
if (changedProperties.has("entry")) { if (changedProperties.has("entry")) {
@ -59,8 +88,13 @@ export class EntityRegistrySettings extends LitElement {
this._name = this.entry.name || ""; this._name = this.entry.name || "";
this._icon = this.entry.icon || ""; this._icon = this.entry.icon || "";
this._origEntityId = this.entry.entity_id; this._origEntityId = this.entry.entity_id;
this._areaId = this.entry.area_id;
this._entityId = this.entry.entity_id; this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by; this._disabledBy = this.entry.disabled_by;
this._device =
this.entry.device_id && this._deviceLookup
? this._deviceLookup[this.entry.device_id]
: undefined;
} }
} }
@ -77,10 +111,19 @@ export class EntityRegistrySettings extends LitElement {
return html` return html`
${!stateObj ${!stateObj
? html` ? html`
<div class="container"> <div class="container warning">
${this.hass!.localize( ${this.hass!.localize(
"ui.dialogs.entity_registry.editor.unavailable" "ui.dialogs.entity_registry.editor.unavailable"
)} )}
${this._device?.disabled_by
? html`<br />${this.hass!.localize(
"ui.dialogs.entity_registry.editor.device_disabled"
)}<br /><mwc-button @click=${this._openDeviceSettings}>
${this.hass!.localize(
"ui.dialogs.entity_registry.editor.open_device_settings"
)}
</mwc-button>`
: ""}
</div> </div>
` `
: ""} : ""}
@ -117,9 +160,17 @@ export class EntityRegistrySettings extends LitElement {
.invalid=${invalidDomainUpdate} .invalid=${invalidDomainUpdate}
.disabled=${this._submitting} .disabled=${this._submitting}
></paper-input> ></paper-input>
${!this.entry.device_id
? html`<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
@value-changed=${this._areaPicked}
></ha-area-picker>`
: ""}
<div class="row"> <div class="row">
<ha-switch <ha-switch
.checked=${!this._disabledBy} .checked=${!this._disabledBy}
.disabled=${this._device?.disabled_by}
@change=${this._disabledByChanged} @change=${this._disabledByChanged}
> >
</ha-switch> </ha-switch>
@ -148,6 +199,31 @@ export class EntityRegistrySettings extends LitElement {
</div> </div>
</div> </div>
</div> </div>
${this.entry.device_id
? html`<ha-expansion-panel .header=${"Advanced"}>
<p>
By default the entities of a device are in the same area as the
device. If you change the area of this entity, it will no longer
follow the area of the device.
</p>
${this._areaId
? html`<mwc-button @click=${this._clearArea}
>Follow device area</mwc-button
>`
: this._device
? html`<mwc-button @click=${this._openDeviceSettings}
>Change device area</mwc-button
>`
: ""}
<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
.placeholder=${this._device?.area_id}
@value-changed=${this._areaPicked}
></ha-area-picker
></ha-expansion-panel>`
: ""}
</div> </div>
<div class="buttons"> <div class="buttons">
<mwc-button <mwc-button
@ -183,14 +259,37 @@ export class EntityRegistrySettings extends LitElement {
this._entityId = ev.detail.value; this._entityId = ev.detail.value;
} }
private _areaPicked(ev: CustomEvent) {
this._error = undefined;
this._areaId = ev.detail.value;
}
private _clearArea() {
this._error = undefined;
this._areaId = null;
}
private _openDeviceSettings() {
showDeviceRegistryDetailDialog(this, {
device: this._device!,
updateEntry: async (updates) => {
await updateDeviceRegistryEntry(this.hass, this._device!.id, updates);
},
});
}
private async _updateEntry(): Promise<void> { private async _updateEntry(): Promise<void> {
this._submitting = true; this._submitting = true;
const params: Partial<EntityRegistryEntryUpdateParams> = { const params: Partial<EntityRegistryEntryUpdateParams> = {
name: this._name.trim() || null, name: this._name.trim() || null,
icon: this._icon.trim() || null, icon: this._icon.trim() || null,
area_id: this._areaId || null,
new_entity_id: this._entityId.trim(), new_entity_id: this._entityId.trim(),
}; };
if (this._disabledBy === null || this._disabledBy === "user") { if (
this.entry.disabled_by !== this._disabledBy &&
(this._disabledBy === null || this._disabledBy === "user")
) {
params.disabled_by = this._disabledBy; params.disabled_by = this._disabledBy;
} }
try { try {

View File

@ -1,6 +1,6 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiFilterVariant } from "@mdi/js"; import { mdiFilterVariant, mdiPlus } from "@mdi/js";
import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
@ -62,6 +62,15 @@ import {
} from "./show-dialog-entity-editor"; } from "./show-dialog-entity-editor";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../../data/area_registry";
import { computeRTL } from "../../../common/util/compute_rtl";
export interface StateEntity extends EntityRegistryEntry { export interface StateEntity extends EntityRegistryEntry {
readonly?: boolean; readonly?: boolean;
@ -73,6 +82,7 @@ export interface EntityRow extends StateEntity {
unavailable: boolean; unavailable: boolean;
restored: boolean; restored: boolean;
status: string; status: string;
area?: string;
} }
@customElement("ha-config-entities") @customElement("ha-config-entities")
@ -87,6 +97,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@internalProperty() private _entities?: EntityRegistryEntry[]; @internalProperty() private _entities?: EntityRegistryEntry[];
@internalProperty() private _devices?: DeviceRegistryEntry[];
@internalProperty() private _areas: AreaRegistryEntry[] = [];
@internalProperty() private _stateEntities: StateEntity[] = []; @internalProperty() private _stateEntities: StateEntity[] = [];
@property() public _entries?: ConfigEntry[]; @property() public _entries?: ConfigEntry[];
@ -175,9 +189,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
? (name, entity: any) => ? (name, entity: any) =>
html` html`
${name}<br /> ${name}<br />
${entity.entity_id} | <div class="secondary">
${this.hass.localize(`component.${entity.platform}.title`) || ${entity.entity_id} |
entity.platform} ${this.hass.localize(`component.${entity.platform}.title`) ||
entity.platform}
</div>
` `
: undefined, : undefined,
}, },
@ -201,6 +217,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
template: (platform) => template: (platform) =>
this.hass.localize(`component.${platform}.title`) || platform, this.hass.localize(`component.${platform}.title`) || platform,
}, },
area: {
title: this.hass.localize(
"ui.panel.config.entities.picker.headers.area"
),
sortable: true,
hidden: narrow,
filterable: true,
width: "15%",
},
status: { status: {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.entities.picker.headers.status" "ui.panel.config.entities.picker.headers.status"
@ -252,48 +277,87 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
}) })
); );
private _filteredEntities = memoize( private _filteredEntitiesAndDomains = memoize(
( (
entities: EntityRegistryEntry[], entities: EntityRegistryEntry[],
devices: DeviceRegistryEntry[] | undefined,
areas: AreaRegistryEntry[] | undefined,
stateEntities: StateEntity[], stateEntities: StateEntity[],
filters: URLSearchParams, filters: URLSearchParams,
showDisabled: boolean, showDisabled: boolean,
showUnavailable: boolean, showUnavailable: boolean,
showReadOnly: boolean showReadOnly: boolean,
): EntityRow[] => { entries?: ConfigEntry[]
) => {
const result: EntityRow[] = []; const result: EntityRow[] = [];
// If nothing gets filtered, this is our correct count of entities // If nothing gets filtered, this is our correct count of entities
let startLength = entities.length + stateEntities.length; let startLength = entities.length + stateEntities.length;
entities = showReadOnly ? entities.concat(stateEntities) : entities; const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
if (areas) {
for (const area of areas) {
areaLookup[area.area_id] = area;
}
if (devices) {
for (const device of devices) {
deviceLookup[device.id] = device;
}
}
}
entities.forEach((entity) => {
return entity;
});
let filteredEntities = showReadOnly
? entities.concat(stateEntities)
: entities;
const filteredDomains: string[] = [];
filters.forEach((value, key) => { filters.forEach((value, key) => {
switch (key) { if (key === "config_entry") {
case "config_entry": filteredEntities = filteredEntities.filter(
entities = entities.filter( (entity) => entity.config_entry_id === value
);
// If we have an active filter and `showReadOnly` is true, the length of `entities` is correct.
// If however, the read-only entities were not added before, we need to check how many would
// have matched the active filter and add that number to the count.
startLength = filteredEntities.length;
if (!showReadOnly) {
startLength += stateEntities.filter(
(entity) => entity.config_entry_id === value (entity) => entity.config_entry_id === value
); ).length;
// If we have an active filter and `showReadOnly` is true, the length of `entities` is correct. }
// If however, the read-only entities were not added before, we need to check how many would
// have matched the active filter and add that number to the count. if (!entries) {
startLength = entities.length; this._loadConfigEntries();
if (!showReadOnly) { return;
startLength += stateEntities.filter( }
(entity) => entity.config_entry_id === value
).length; const configEntry = entries.find((entry) => entry.entry_id === value);
}
break; if (configEntry) {
filteredDomains.push(configEntry.domain);
}
} }
}); });
if (!showDisabled) { if (!showDisabled) {
entities = entities.filter((entity) => !entity.disabled_by); filteredEntities = filteredEntities.filter(
(entity) => !entity.disabled_by
);
} }
for (const entry of entities) { for (const entry of filteredEntities) {
const entity = this.hass.states[entry.entity_id]; const entity = this.hass.states[entry.entity_id];
const unavailable = entity?.state === UNAVAILABLE; const unavailable = entity?.state === UNAVAILABLE;
const restored = entity?.attributes.restored; const restored = entity?.attributes.restored;
const areaId = entry.area_id ?? deviceLookup[entry.device_id!]?.area_id;
const area = areaId ? areaLookup[areaId] : undefined;
if (!showUnavailable && unavailable) { if (!showUnavailable && unavailable) {
continue; continue;
@ -309,6 +373,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this.hass.localize("state.default.unavailable"), this.hass.localize("state.default.unavailable"),
unavailable, unavailable,
restored, restored,
area: area ? area.name : undefined,
status: restored status: restored
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.entities.picker.status.restored" "ui.panel.config.entities.picker.status.restored"
@ -326,7 +391,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
} }
this._numHiddenEntities = startLength - result.length; this._numHiddenEntities = startLength - result.length;
return result; return { filteredEntities: result, filteredDomains: filteredDomains };
} }
); );
@ -345,6 +410,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
subscribeEntityRegistry(this.hass.connection!, (entities) => { subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities; this._entities = entities;
}), }),
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
}),
subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
}),
]; ];
} }
@ -370,15 +441,22 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._entries this._entries
); );
const entityData = this._filteredEntities( const {
filteredEntities,
filteredDomains,
} = this._filteredEntitiesAndDomains(
this._entities, this._entities,
this._devices,
this._areas,
this._stateEntities, this._stateEntities,
this._searchParms, this._searchParms,
this._showDisabled, this._showDisabled,
this._showUnavailable, this._showUnavailable,
this._showReadOnly this._showReadOnly,
this._entries
); );
const includeZHAFab = filteredDomains.includes("zha");
const headerToolbar = this._selectedEntities.length const headerToolbar = this._selectedEntities.length
? html` ? html`
<p class="selected-txt"> <p class="selected-txt">
@ -584,13 +662,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.route=${this.route} .route=${this.route}
.tabs=${configSections.integrations} .tabs=${configSections.integrations}
.columns=${this._columns(this.narrow, this.hass.language)} .columns=${this._columns(this.narrow, this.hass.language)}
.data=${entityData} .data=${filteredEntities}
.filter=${this._filter} .filter=${this._filter}
selectable selectable
clickable clickable
@selection-changed=${this._handleSelectionChanged} @selection-changed=${this._handleSelectionChanged}
@row-click=${this._openEditEntry} @row-click=${this._openEditEntry}
id="entity_id" id="entity_id"
.hasFab=${includeZHAFab}
> >
<div <div
class=${classMap({ class=${classMap({
@ -601,6 +680,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
> >
${headerToolbar} ${headerToolbar}
</div> </div>
${includeZHAFab
? html`<a href="/config/zha/add" slot="fab">
<ha-fab
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
extended
?rtl=${computeRTL(this.hass)}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</a>`
: html``}
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }

View File

@ -5,6 +5,8 @@ import { classMap } from "lit-html/directives/class-map";
export class HaConfigSection extends LitElement { export class HaConfigSection extends LitElement {
@property() public isWide = false; @property() public isWide = false;
@property({ type: Boolean }) public vertical = false;
protected render() { protected render() {
return html` return html`
<div <div
@ -16,8 +18,8 @@ export class HaConfigSection extends LitElement {
<div <div
class="together layout ${classMap({ class="together layout ${classMap({
narrow: !this.isWide, narrow: !this.isWide,
vertical: !this.isWide, vertical: this.vertical || !this.isWide,
horizontal: this.isWide, horizontal: !this.vertical && this.isWide,
})}" })}"
> >
<div class="intro"><slot name="introduction"></slot></div> <div class="intro"><slot name="introduction"></slot></div>

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import { mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
@ -158,7 +158,7 @@ export class HaConfigHelpers extends LitElement {
"ui.panel.config.helpers.picker.no_helpers" "ui.panel.config.helpers.picker.no_helpers"
)} )}
> >
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.helpers.picker.add_helper" "ui.panel.config.helpers.picker.add_helper"
@ -167,7 +167,7 @@ export class HaConfigHelpers extends LitElement {
@click=${this._createHelpler} @click=${this._createHelpler}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiPlus } from "@mdi/js"; import { mdiDotsVertical, mdiPlus } from "@mdi/js";
@ -474,7 +474,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
` `
: ""} : ""}
</div> </div>
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.integrations.add_integration" "ui.panel.config.integrations.add_integration"
@ -483,7 +483,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@click=${this._createFlow} @click=${this._createFlow}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
} }

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-fab";
import { mdiCheckCircle, mdiCircle, mdiCloseCircle, mdiZWave } from "@mdi/js"; import { mdiCheckCircle, mdiCircle, mdiCloseCircle, mdiZWave } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-fab";
import { mdiCheckCircle, mdiCircle, mdiCloseCircle } from "@mdi/js"; import { mdiCheckCircle, mdiCircle, mdiCloseCircle } from "@mdi/js";
import { import {
css, css,

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-fab";
import { mdiAlert, mdiCheck } from "@mdi/js"; import { mdiAlert, mdiCheck } from "@mdi/js";
import { import {
CSSResult, CSSResult,

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-fab";
import { import {
css, css,
CSSResultArray, CSSResultArray,

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-fab";
import { import {
css, css,
CSSResultArray, CSSResultArray,

View File

@ -14,13 +14,17 @@ import {
} from "lit-element"; } from "lit-element";
import "../../../../../components/ha-service-description"; import "../../../../../components/ha-service-description";
import "@polymer/paper-input/paper-textarea"; import "@polymer/paper-input/paper-textarea";
import { ZHADevice } from "../../../../../data/zha";
import "../../../../../layouts/hass-tabs-subpage"; import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../../types"; import { HomeAssistant, Route } from "../../../../../types";
import "./zha-device-card"; import "./zha-device-pairing-status-card";
import { zhaTabs } from "./zha-config-dashboard"; import { zhaTabs } from "./zha-config-dashboard";
import { IronAutogrowTextareaElement } from "@polymer/iron-autogrow-textarea"; import { IronAutogrowTextareaElement } from "@polymer/iron-autogrow-textarea";
import {
DEVICE_MESSAGE_TYPES,
LOG_OUTPUT,
ZHADevice,
} from "../../../../../data/zha";
@customElement("zha-add-devices-page") @customElement("zha-add-devices-page")
class ZHAAddDevicesPage extends LitElement { class ZHAAddDevicesPage extends LitElement {
@ -34,7 +38,10 @@ class ZHAAddDevicesPage extends LitElement {
@internalProperty() private _error?: string; @internalProperty() private _error?: string;
@internalProperty() private _discoveredDevices: ZHADevice[] = []; @internalProperty() private _discoveredDevices: Record<
string,
ZHADevice
> = {};
@internalProperty() private _formattedEvents = ""; @internalProperty() private _formattedEvents = "";
@ -64,7 +71,7 @@ class ZHAAddDevicesPage extends LitElement {
super.disconnectedCallback(); super.disconnectedCallback();
this._unsubscribe(); this._unsubscribe();
this._error = undefined; this._error = undefined;
this._discoveredDevices = []; this._discoveredDevices = {};
this._formattedEvents = ""; this._formattedEvents = "";
} }
@ -115,7 +122,7 @@ class ZHAAddDevicesPage extends LitElement {
</div> </div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""} ${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="content"> <div class="content">
${this._discoveredDevices.length < 1 ${Object.keys(this._discoveredDevices).length < 1
? html` ? html`
<div class="discovery-text"> <div class="discovery-text">
<h4> <h4>
@ -133,15 +140,15 @@ class ZHAAddDevicesPage extends LitElement {
</div> </div>
` `
: html` : html`
${this._discoveredDevices.map( ${Object.values(this._discoveredDevices).map(
(device) => html` (device) => html`
<zha-device-card <zha-device-pairing-status-card
class="card" class="card"
.hass=${this.hass} .hass=${this.hass}
.device=${device} .device=${device}
.narrow=${this.narrow} .narrow=${this.narrow}
.showHelp=${this._showHelp} .showHelp=${this._showHelp}
></zha-device-card> ></zha-device-pairing-status-card>
` `
)} )}
`} `}
@ -164,7 +171,7 @@ class ZHAAddDevicesPage extends LitElement {
} }
private _handleMessage(message: any): void { private _handleMessage(message: any): void {
if (message.type === "log_output") { if (message.type === LOG_OUTPUT) {
this._formattedEvents += message.log_entry.message + "\n"; this._formattedEvents += message.log_entry.message + "\n";
if (this.shadowRoot) { if (this.shadowRoot) {
const paperTextArea = this.shadowRoot.querySelector("paper-textarea"); const paperTextArea = this.shadowRoot.querySelector("paper-textarea");
@ -175,8 +182,8 @@ class ZHAAddDevicesPage extends LitElement {
} }
} }
} }
if (message.type && message.type === "device_fully_initialized") { if (message.type && DEVICE_MESSAGE_TYPES.includes(message.type)) {
this._discoveredDevices.push(message.device_info); this._discoveredDevices[message.device_info.ieee] = message.device_info;
} }
} }

View File

@ -1,6 +1,6 @@
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import "@material/mwc-fab"; import "../../../../../components/ha-fab";
import { import {
css, css,
CSSResultArray, CSSResultArray,
@ -88,13 +88,13 @@ class ZHAConfigDashboard extends LitElement {
: ""} : ""}
</ha-card> </ha-card>
<a href="/config/zha/add" slot="fab"> <a href="/config/zha/add" slot="fab">
<mwc-fab <ha-fab
.label=${this.hass.localize("ui.panel.config.zha.add_device")} .label=${this.hass.localize("ui.panel.config.zha.add_device")}
extended extended
?rtl=${computeRTL(this.hass)} ?rtl=${computeRTL(this.hass)}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</a> </a>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;

View File

@ -0,0 +1,147 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/entity/state-badge";
import "../../../../../components/ha-card";
import "../../../../../components/ha-service-description";
import {
CONFIGURED,
INCOMPLETE_PAIRING_STATUSES,
INITIALIZED,
INTERVIEW_COMPLETE,
ZHADevice,
} from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-area-picker";
import { formatAsPaddedHex } from "./functions";
import "./zha-device-card";
import { classMap } from "lit-html/directives/class-map";
@customElement("zha-device-pairing-status-card")
class ZHADevicePairingStatusCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device?: ZHADevice;
@property({ type: Boolean }) public narrow?: boolean;
@internalProperty() private _showHelp = false;
protected render(): TemplateResult {
if (!this.hass || !this.device) {
return html``;
}
return html`
<ha-card
outlined
class="discovered ${classMap({
initialized: this.device.pairing_status === INITIALIZED,
})}"
><div
class="header"
>
<h1>
${this.hass!.localize(
`ui.panel.config.zha.device_pairing_card.${this.device.pairing_status}`
)}
</h1>
<h4>
${this.hass!.localize(
`ui.panel.config.zha.device_pairing_card.${this.device.pairing_status}_status_text`
)}
</h4>
</div>
<div class="card-content">
${[INTERVIEW_COMPLETE, CONFIGURED].includes(
this.device.pairing_status!
)
? html`
<div class="model">${this.device.model}</div>
<div class="manuf">
${this.hass.localize(
"ui.dialogs.zha_device_info.manuf",
"manufacturer",
this.device.manufacturer
)}
</div>
`
: html``}
<div class="info">
${INCOMPLETE_PAIRING_STATUSES.includes(this.device.pairing_status!)
? html`
<div class="text">IEEE: ${this.device.ieee}</div>
<div class="text">
NWK: ${formatAsPaddedHex(this.device.nwk)}
</div>
`
: html``}
</div>
${this.device.pairing_status === INITIALIZED
? html`
<zha-device-card
class="card"
.hass=${this.hass}
.device=${this.device}
.narrow=${this.narrow}
.showHelp=${this._showHelp}
></zha-device-card>
`
: html``}
</div>
</ha-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.discovered {
--ha-card-border-color: var(--primary-color);
}
.discovered.initialized {
--ha-card-border-color: var(--success-color);
}
.discovered .header {
background: var(--primary-color);
color: var(--text-primary-color);
padding: 8px;
text-align: center;
margin-bottom: 20px;
}
.discovered.initialized .header {
background: var(--success-color);
}
h1 {
margin: 0;
}
h4 {
margin: 0;
}
.text,
.manuf,
.model {
color: var(--secondary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-device-pairing-status-card": ZHADevicePairingStatusCard;
}
}

View File

@ -1,5 +1,5 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-fab"; import "../../../../../components/ha-fab";
import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-icon-button";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { import {
@ -127,14 +127,14 @@ export class ZHAGroupsDashboard extends LitElement {
clickable clickable
> >
<a href="/config/zha/group-add" slot="fab"> <a href="/config/zha/group-add" slot="fab">
<mwc-fab <ha-fab
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.config.zha.groups.add_group" "ui.panel.config.zha.groups.add_group"
)} )}
extended extended
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</a> </a>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../../components/ha-fab";
import { mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { import {
@ -223,7 +223,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
hasFab hasFab
clickable clickable
> >
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.add_dashboard" "ui.panel.config.lovelace.dashboards.picker.add_dashboard"
@ -232,7 +232,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
@click=${this._addDashboard} @click=${this._addDashboard}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../../components/ha-fab";
import { mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
@ -103,7 +103,7 @@ export class HaConfigLovelaceRescources extends LitElement {
hasFab hasFab
clickable clickable
> >
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.lovelace.resources.picker.add_resource" "ui.panel.config.lovelace.resources.picker.add_resource"
@ -112,7 +112,7 @@ export class HaConfigLovelaceRescources extends LitElement {
@click=${this._addResource} @click=${this._addResource}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }

View File

@ -68,6 +68,8 @@ class DialogPersonDetail extends LitElement {
@internalProperty() private _submitting = false; @internalProperty() private _submitting = false;
@internalProperty() private _personExists = false;
private _deviceTrackersAvailable = memoizeOne((hass) => { private _deviceTrackersAvailable = memoizeOne((hass) => {
return Object.keys(hass.states).some( return Object.keys(hass.states).some(
(entityId) => (entityId) =>
@ -79,6 +81,7 @@ class DialogPersonDetail extends LitElement {
this._params = params; this._params = params;
this._error = undefined; this._error = undefined;
if (this._params.entry) { if (this._params.entry) {
this._personExists = true;
this._name = this._params.entry.name || ""; this._name = this._params.entry.name || "";
this._userId = this._params.entry.user_id || undefined; this._userId = this._params.entry.user_id || undefined;
this._deviceTrackers = this._params.entry.device_trackers || []; this._deviceTrackers = this._params.entry.device_trackers || [];
@ -88,6 +91,7 @@ class DialogPersonDetail extends LitElement {
: undefined; : undefined;
this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
} else { } else {
this._personExists = false;
this._name = ""; this._name = "";
this._userId = undefined; this._userId = undefined;
this._user = undefined; this._user = undefined;
@ -398,6 +402,7 @@ class DialogPersonDetail extends LitElement {
await this._params!.updateEntry(values); await this._params!.updateEntry(values);
} else { } else {
await this._params!.createEntry(values); await this._params!.createEntry(values);
this._personExists = true;
} }
this._params = undefined; this._params = undefined;
} catch (err) { } catch (err) {
@ -422,6 +427,14 @@ class DialogPersonDetail extends LitElement {
} }
private _close(): void { private _close(): void {
// If we do not have a person ID yet (= person creation dialog was just cancelled), but
// we already created a user ID for it, delete it now to not have it "free floating".
if (!this._personExists && this._userId) {
deleteUser(this.hass, this._userId);
this._params?.refreshUsers();
this._userId = undefined;
}
this._params = undefined; this._params = undefined;
} }

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import { mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@ -146,14 +146,14 @@ class HaConfigPerson extends LitElement {
` `
: ""} : ""}
</ha-config-section> </ha-config-section>
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${hass.localize("ui.panel.config.person.add_person")} .label=${hass.localize("ui.panel.config.person.add_person")}
extended extended
@click=${this._createPerson} @click=${this._createPerson}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
} }

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import { mdiPlus, mdiHelpCircle } from "@mdi/js"; import { mdiPlus, mdiHelpCircle } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
@ -152,14 +152,14 @@ class HaSceneDashboard extends LitElement {
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
<a href="/config/scene/edit/new" slot="fab"> <a href="/config/scene/edit/new" slot="fab">
<mwc-fab <ha-fab
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.scene.picker.add_scene" "ui.panel.config.scene.picker.add_scene"
)} )}
extended extended
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</a> </a>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;

View File

@ -25,7 +25,7 @@ import "../../../components/device/ha-device-picker";
import "../../../components/entity/ha-entities-picker"; import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-input"; import "../../../components/ha-icon-input";
import "@material/mwc-fab"; import "../../../components/ha-fab";
import { import {
computeDeviceName, computeDeviceName,
DeviceRegistryEntry, DeviceRegistryEntry,
@ -403,7 +403,7 @@ export class HaSceneEditor extends SubscribeMixin(
` `
: ""} : ""}
</div> </div>
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize("ui.panel.config.scene.editor.save")} .label=${this.hass.localize("ui.panel.config.scene.editor.save")}
extended extended
@ -411,7 +411,7 @@ export class HaSceneEditor extends SubscribeMixin(
class=${classMap({ dirty: this._dirty })} class=${classMap({ dirty: this._dirty })}
> >
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
} }
@ -786,12 +786,12 @@ export class HaSceneEditor extends SubscribeMixin(
span[slot="introduction"] a { span[slot="introduction"] a {
color: var(--primary-color); color: var(--primary-color);
} }
mwc-fab { ha-fab {
position: relative; position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom)); bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s; transition: bottom 0.3s;
} }
mwc-fab.dirty { ha-fab.dirty {
bottom: 0; bottom: 0;
} }
`, `,

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import { import {
mdiCheck, mdiCheck,
mdiContentSave, mdiContentSave,
@ -388,7 +388,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
` `
: ``} : ``}
</div> </div>
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.script.editor.save_script" "ui.panel.config.script.editor.save_script"
@ -400,7 +400,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
})} })}
> >
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
} }
@ -690,12 +690,12 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
span[slot="introduction"] a { span[slot="introduction"] a {
color: var(--primary-color); color: var(--primary-color);
} }
mwc-fab { ha-fab {
position: relative; position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom)); bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s; transition: bottom 0.3s;
} }
mwc-fab.dirty { ha-fab.dirty {
bottom: 0; bottom: 0;
} }
.selected_menu_item { .selected_menu_item {

View File

@ -16,7 +16,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeRTL } from "../../../common/util/compute_rtl"; import { computeRTL } from "../../../common/util/compute_rtl";
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import "@material/mwc-fab"; import "../../../components/ha-fab";
import { triggerScript } from "../../../data/script"; import { triggerScript } from "../../../data/script";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
@ -147,7 +147,7 @@ class HaScriptPicker extends LitElement {
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
<a href="/config/script/edit/new" slot="fab"> <a href="/config/script/edit/new" slot="fab">
<mwc-fab <ha-fab
?is-wide=${this.isWide} ?is-wide=${this.isWide}
?narrow=${this.narrow} ?narrow=${this.narrow}
.label=${this.hass.localize( .label=${this.hass.localize(
@ -157,7 +157,7 @@ class HaScriptPicker extends LitElement {
?rtl=${computeRTL(this.hass)} ?rtl=${computeRTL(this.hass)}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</a> </a>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import { import {
mdiCog, mdiCog,
@ -207,14 +207,14 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
<mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}> <mwc-icon-button slot="toolbar-icon" @click=${this._showHelp}>
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon> <ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize("ui.panel.config.tags.add_tag")} .label=${this.hass.localize("ui.panel.config.tags.add_tag")}
extended extended
@click=${this._addTag} @click=${this._addTag}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }

View File

@ -98,7 +98,7 @@ export class DialogAddUser extends LitElement {
class="name" class="name"
name="name" name="name"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.users.add_user.name" "ui.panel.config.users.editor.name"
)} )}
.value=${this._name} .value=${this._name}
required required
@ -113,7 +113,7 @@ export class DialogAddUser extends LitElement {
class="username" class="username"
name="username" name="username"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.users.add_user.username" "ui.panel.config.users.editor.username"
)} )}
.value=${this._username} .value=${this._username}
required required
@ -241,7 +241,7 @@ export class DialogAddUser extends LitElement {
user = userResponse.user; user = userResponse.user;
} catch (err) { } catch (err) {
this._loading = false; this._loading = false;
this._error = err.code; this._error = err.message;
return; return;
} }
@ -255,10 +255,11 @@ export class DialogAddUser extends LitElement {
} catch (err) { } catch (err) {
await deleteUser(this.hass, user.id); await deleteUser(this.hass, user.id);
this._loading = false; this._loading = false;
this._error = err.code; this._error = err.message;
return; return;
} }
user.username = this._username;
this._params!.userAddedCallback(user); this._params!.userAddedCallback(user);
this._close(); this._close();
} }

View File

@ -13,6 +13,7 @@ import {
} from "lit-element"; } from "lit-element";
import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-help-tooltip";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import { adminChangePassword } from "../../../data/auth"; import { adminChangePassword } from "../../../data/auth";
@ -37,6 +38,8 @@ class DialogUserDetail extends LitElement {
@internalProperty() private _isAdmin?: boolean; @internalProperty() private _isAdmin?: boolean;
@internalProperty() private _isActive?: boolean;
@internalProperty() private _error?: string; @internalProperty() private _error?: string;
@internalProperty() private _params?: UserDetailDialogParams; @internalProperty() private _params?: UserDetailDialogParams;
@ -48,6 +51,7 @@ class DialogUserDetail extends LitElement {
this._error = undefined; this._error = undefined;
this._name = params.entry.name || ""; this._name = params.entry.name || "";
this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._isActive = params.entry.is_active;
await this.updateComplete; await this.updateComplete;
} }
@ -67,7 +71,10 @@ class DialogUserDetail extends LitElement {
<div> <div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""} ${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="secondary"> <div class="secondary">
${this.hass.localize("ui.panel.config.users.editor.id")}: ${user.id} ${this.hass.localize("ui.panel.config.users.editor.id")}:
${user.id}<br />
${this.hass.localize("ui.panel.config.users.editor.username")}:
${user.username}
</div> </div>
<div> <div>
${user.is_owner ${user.is_owner
@ -88,15 +95,6 @@ class DialogUserDetail extends LitElement {
</span> </span>
` `
: ""} : ""}
${user.is_active
? html`
<span class="state"
>${this.hass.localize(
"ui.panel.config.users.editor.active"
)}</span
>
`
: ""}
</div> </div>
<div class="form"> <div class="form">
<paper-input <paper-input
@ -107,17 +105,21 @@ class DialogUserDetail extends LitElement {
"ui.panel.config.users.editor.name" "ui.panel.config.users.editor.name"
)}" )}"
></paper-input> ></paper-input>
<ha-formfield <div class="row">
.label=${this.hass.localize("ui.panel.config.users.editor.admin")} <ha-formfield
.dir=${computeRTLDirection(this.hass)} .label=${this.hass.localize(
> "ui.panel.config.users.editor.admin"
<ha-switch )}
.disabled=${user.system_generated || user.is_owner} .dir=${computeRTLDirection(this.hass)}
.checked=${this._isAdmin}
@change=${this._adminChanged}
> >
</ha-switch> <ha-switch
</ha-formfield> .disabled=${user.system_generated || user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-formfield>
</div>
${!this._isAdmin ${!this._isAdmin
? html` ? html`
<br /> <br />
@ -126,6 +128,27 @@ class DialogUserDetail extends LitElement {
)} )}
` `
: ""} : ""}
<div class="row">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.users.editor.active"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.disabled=${user.system_generated || user.is_owner}
.checked=${this._isActive}
@change=${this._activeChanged}
>
</ha-switch>
</ha-formfield>
<ha-help-tooltip
.label=${this.hass.localize(
"ui.panel.config.users.editor.active_tooltip"
)}
>
</ha-help-tooltip>
</div>
</div> </div>
</div> </div>
@ -189,11 +212,16 @@ class DialogUserDetail extends LitElement {
this._isAdmin = ev.target.checked; this._isAdmin = ev.target.checked;
} }
private async _activeChanged(ev): Promise<void> {
this._isActive = ev.target.checked;
}
private async _updateEntry() { private async _updateEntry() {
this._submitting = true; this._submitting = true;
try { try {
await this._params!.updateEntry({ await this._params!.updateEntry({
name: this._name.trim(), name: this._name.trim(),
is_active: this._isActive,
group_ids: [ group_ids: [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
], ],
@ -290,8 +318,13 @@ class DialogUserDetail extends LitElement {
.state:not(:first-child) { .state:not(:first-child) {
margin-left: 8px; margin-left: 8px;
} }
ha-switch { .row {
margin-top: 8px; display: flex;
padding: 8px 0;
}
ha-help-tooltip {
margin-left: 4px;
position: relative;
} }
`, `,
]; ];

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import { mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import { import {
customElement, customElement,
@ -35,18 +35,40 @@ export class HaConfigUsers extends LitElement {
@property() public route!: Route; @property() public route!: Route;
private _columns = memoizeOne( private _columns = memoizeOne(
(_language): DataTableColumnContainer => { (narrow: boolean, _language): DataTableColumnContainer => {
return { const columns: DataTableColumnContainer = {
name: { name: {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.users.picker.headers.name" "ui.panel.config.users.picker.headers.name"
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "25%",
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (name) => html` template: (name, user: any) =>
${name || narrow
? html` ${name}<br />
<div class="secondary">
${user.username} |
${this.hass.localize(`groups.${user.group_ids[0]}`)}
</div>`
: html` ${name ||
this.hass!.localize(
"ui.panel.config.users.editor.unnamed_user"
)}`,
},
username: {
title: this.hass.localize(
"ui.panel.config.users.picker.headers.username"
),
sortable: true,
filterable: true,
width: "20%",
direction: "asc",
hidden: narrow,
template: (username) => html`
${username ||
this.hass!.localize("ui.panel.config.users.editor.unnamed_user")} this.hass!.localize("ui.panel.config.users.editor.unnamed_user")}
`, `,
}, },
@ -56,26 +78,38 @@ export class HaConfigUsers extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "30%", width: "20%",
direction: "asc",
hidden: narrow,
template: (groupIds) => html` template: (groupIds) => html`
${this.hass.localize(`groups.${groupIds[0]}`)} ${this.hass.localize(`groups.${groupIds[0]}`)}
`, `,
}, },
is_active: {
title: this.hass.localize(
"ui.panel.config.users.picker.headers.is_active"
),
type: "icon",
sortable: true,
filterable: true,
width: "80px",
template: (is_active) =>
is_active ? html`<ha-icon icon="hass:check"> </ha-icon>` : "",
},
system_generated: { system_generated: {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.users.picker.headers.system" "ui.panel.config.users.picker.headers.system"
), ),
type: "icon", type: "icon",
width: "80px",
sortable: true, sortable: true,
filterable: true, filterable: true,
template: (generated) => html` width: "160px",
${generated template: (generated) =>
? html` <ha-icon icon="hass:check-circle-outline"></ha-icon> ` generated ? html`<ha-icon icon="hass:check"> </ha-icon>` : "",
: ""}
`,
}, },
}; };
return columns;
} }
); );
@ -92,26 +126,32 @@ export class HaConfigUsers extends LitElement {
.route=${this.route} .route=${this.route}
backPath="/config" backPath="/config"
.tabs=${configSections.persons} .tabs=${configSections.persons}
.columns=${this._columns(this.hass.language)} .columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._users} .data=${this._users}
@row-click=${this._editUser} @row-click=${this._editUser}
hasFab hasFab
clickable clickable
> >
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize("ui.panel.config.users.picker.add_user")} .label=${this.hass.localize("ui.panel.config.users.picker.add_user")}
extended extended
@click=${this._addUser} @click=${this._addUser}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }
private async _fetchUsers() { private async _fetchUsers() {
this._users = await fetchUsers(this.hass); this._users = await fetchUsers(this.hass);
this._users.forEach(function (user) {
if (user.is_owner) {
user.group_ids.unshift("owner");
}
});
} }
private _editUser(ev: HASSDomEvent<RowClickedEvent>) { private _editUser(ev: HASSDomEvent<RowClickedEvent>) {

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../components/ha-fab";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import { mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js"; import { mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
@ -255,14 +255,14 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
</div> </div>
` `
: ""} : ""}
<mwc-fab <ha-fab
slot="fab" slot="fab"
.label=${hass.localize("ui.panel.config.zone.add_zone")} .label=${hass.localize("ui.panel.config.zone.add_zone")}
extended extended
@click=${this._createZone} @click=${this._createZone}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
} }

View File

@ -109,7 +109,7 @@ class PanelDeveloperTools extends LitElement {
ha-tabs { ha-tabs {
margin-left: max(env(safe-area-inset-left), 24px); margin-left: max(env(safe-area-inset-left), 24px);
margin-right: max(env(safe-area-inset-right), 24px); margin-right: max(env(safe-area-inset-right), 24px);
--paper-tabs-selection-bar-color: #fff; --paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
text-transform: uppercase; text-transform: uppercase;
} }
`, `,

View File

@ -109,7 +109,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
} }
public setConfig(config: EntitiesCardConfig): void { public setConfig(config: EntitiesCardConfig): void {
if (!config || !config.entities.length) { if (!config.entities || !Array.isArray(config.entities)) {
throw new Error("Entities must be specified"); throw new Error("Entities must be specified");
} }

View File

@ -16,7 +16,6 @@ import "../../../components/state-history-charts";
import { CacheConfig, getRecentWithCache } from "../../../data/cached-history"; import { CacheConfig, getRecentWithCache } from "../../../data/cached-history";
import { HistoryResult } from "../../../data/history"; import { HistoryResult } from "../../../data/history";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entites";
import { hasConfigOrEntitiesChanged } from "../common/has-changed"; import { hasConfigOrEntitiesChanged } from "../common/has-changed";
import { processConfigEntities } from "../common/process-config-entities"; import { processConfigEntities } from "../common/process-config-entities";
import { EntityConfig } from "../entity-rows/types"; import { EntityConfig } from "../entity-rows/types";
@ -30,22 +29,9 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
return document.createElement("hui-history-graph-card-editor"); return document.createElement("hui-history-graph-card-editor");
} }
public static getStubConfig( public static getStubConfig(): HistoryGraphCardConfig {
hass: HomeAssistant, // Hard coded to sun.sun to prevent high server load when it would pick an entity with a lot of state changes
entities: string[], return { type: "history-graph", entities: ["sun.sun"] };
entitiesFallback: string[]
): HistoryGraphCardConfig {
const includeDomains = ["sensor"];
const maxEntities = 1;
const foundEntities = findEntities(
hass,
maxEntities,
entities,
entitiesFallback,
includeDomains
);
return { type: "history-graph", entities: foundEntities };
} }
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@ -71,12 +57,12 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
} }
public setConfig(config: HistoryGraphCardConfig): void { public setConfig(config: HistoryGraphCardConfig): void {
if (!config.entities.length) { if (!config.entities || !Array.isArray(config.entities)) {
throw new Error("Entities must be specified"); throw new Error("Entities need to be an array");
} }
if (config.entities && !Array.isArray(config.entities)) { if (!config.entities.length) {
throw new Error("Entities need to be an array"); throw new Error("You must include at least one entity");
} }
this._config = config; this._config = config;

View File

@ -29,7 +29,7 @@ class HuiHorizontalStackCard extends HuiStackCard {
} }
#root > * { #root > * {
flex: 1 1 0; flex: 1 1 0;
margin: 0 4px; margin: var(--horizontal-stack-card-margin, var(--stack-card-margin, 0 4px));
min-width: 0; min-width: 0;
} }
#root > *:first-child { #root > *:first-child {

View File

@ -246,78 +246,73 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
${!isUnavailable && ${!isUnavailable &&
(mediaDescription || stateObj.attributes.media_title || showControls) (mediaDescription || stateObj.attributes.media_title || showControls)
? html` ? html`
<div <div>
class="title-controls" <div class="title-controls">
style=${styleMap({ ${!mediaDescription && !stateObj.attributes.media_title
paddingRight: isOffState ? ""
? "0" : html`
: `${this._cardHeight - 40}px`, <div class="media-info">
})} <hui-marquee
> .text=${stateObj.attributes.media_title ||
${!mediaDescription && !stateObj.attributes.media_title mediaDescription}
? "" .active=${this._marqueeActive}
: html` @mouseover=${this._marqueeMouseOver}
<div class="media-info"> @mouseleave=${this._marqueeMouseLeave}
<hui-marquee ></hui-marquee>
.text=${stateObj.attributes.media_title || ${!stateObj.attributes.media_title
mediaDescription} ? ""
.active=${this._marqueeActive} : mediaDescription}
@mouseover=${this._marqueeMouseOver} </div>
@mouseleave=${this._marqueeMouseLeave} `}
></hui-marquee> ${!showControls
${!stateObj.attributes.media_title ? ""
? "" : html`
: mediaDescription} <div class="controls">
</div> ${controls!.map(
`} (control) => html`
${!showControls <ha-icon-button
? ""
: html`
<div class="controls">
${controls!.map(
(control) => html`
<ha-icon-button
.title=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
.icon=${control.icon}
action=${control.action}
@click=${this._handleClick}
></ha-icon-button>
`
)}
${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA)
? html`
<mwc-icon-button
class="browse-media"
.title=${this.hass.localize( .title=${this.hass.localize(
"ui.card.media_player.browse_media" `ui.card.media_player.${control.action}`
)} )}
@click=${this._handleBrowseMedia} .icon=${control.icon}
><ha-svg-icon action=${control.action}
.path=${mdiPlayBoxMultiple} @click=${this._handleClick}
></ha-svg-icon ></ha-icon-button>
></mwc-icon-button>
` `
: ""} )}
</div> ${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA)
? html`
<mwc-icon-button
class="browse-media"
.title=${this.hass.localize(
"ui.card.media_player.browse_media"
)}
@click=${this._handleBrowseMedia}
><ha-svg-icon
.path=${mdiPlayBoxMultiple}
></ha-svg-icon
></mwc-icon-button>
`
: ""}
</div>
`}
</div>
${!this._showProgressBar
? ""
: html`
<paper-progress
.max=${stateObj.attributes.media_duration}
style=${styleMap({
"--paper-progress-active-color":
this._foregroundColor || "var(--accent-color)",
cursor: supportsFeature(stateObj, SUPPORT_SEEK)
? "pointer"
: "initial",
})}
@click=${this._handleSeek}
></paper-progress>
`} `}
</div> </div>
${!this._showProgressBar
? ""
: html`
<paper-progress
.max=${stateObj.attributes.media_duration}
style=${styleMap({
"--paper-progress-active-color":
this._foregroundColor || "var(--accent-color)",
cursor: supportsFeature(stateObj, SUPPORT_SEEK)
? "pointer"
: "initial",
})}
@click=${this._handleSeek}
></paper-progress>
`}
` `
: ""} : ""}
</div> </div>
@ -635,6 +630,11 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
.player { .player {
position: relative; position: relative;
padding: 16px; padding: 16px;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
color: var(--text-primary-color); color: var(--text-primary-color);
transition-property: color, padding; transition-property: color, padding;
transition-duration: 0.4s; transition-duration: 0.4s;
@ -671,7 +671,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
mwc-icon-button.browse-media { mwc-icon-button.browse-media {
position: absolute; position: absolute;
right: 0; right: 4px;
--mdc-icon-size: 24px; --mdc-icon-size: 24px;
} }
@ -693,7 +693,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
.more-info { .more-info {
position: absolute; position: absolute;
top: 4px; top: 4px;
right: 0px; right: 4px;
} }
.media-info { .media-info {

View File

@ -29,7 +29,7 @@ class HuiVerticalStackCard extends HuiStackCard {
height: 100%; height: 100%;
} }
#root > * { #root > * {
margin: 4px 0 4px 0; margin: var(--vertical-stack-card-margin, var(--stack-card-margin, 4px 0));
} }
#root > *:first-child { #root > *:first-child {
margin-top: 0; margin-top: 0;

View File

@ -72,7 +72,7 @@ class HuiMarquee extends LitElement {
display: flex; display: flex;
position: relative; position: relative;
align-items: center; align-items: center;
height: 1em; height: 1.2em;
contain: strict; contain: strict;
} }

View File

@ -353,11 +353,9 @@ export class HuiCardPicker extends LitElement {
max-width: 500px; max-width: 500px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-radius: 4px; border-radius: var(--ha-card-border-radius, 4px);
border: 1px solid var(--divider-color);
background: var(--primary-background-color, #fafafa); background: var(--primary-background-color, #fafafa);
cursor: pointer; cursor: pointer;
box-sizing: border-box;
position: relative; position: relative;
} }
@ -375,7 +373,6 @@ export class HuiCardPicker extends LitElement {
--ha-card-background, --ha-card-background,
var(--card-background-color, white) var(--card-background-color, white)
); );
border-radius: 0 0 4px 4px;
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid var(--divider-color);
} }
@ -408,6 +405,10 @@ export class HuiCardPicker extends LitElement {
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 1; z-index: 1;
box-sizing: border-box;
border: var(--ha-card-border-width, 1px) solid
var(--ha-card-border-color, var(--divider-color));
border-radius: var(--ha-card-border-radius, 4px);
} }
.manual { .manual {

View File

@ -450,6 +450,10 @@ export class HuiDialogEditCard extends LitElement
} }
.element-preview { .element-preview {
position: relative; position: relative;
height: max-content;
background: var(--primary-background-color);
padding: 4px;
border-radius: 4px;
} }
.element-preview ha-circular-progress { .element-preview ha-circular-progress {
top: 50%; top: 50%;

View File

@ -1,4 +1,4 @@
import "@material/mwc-fab"; import "../../../../components/ha-fab";
import { import {
css, css,
CSSResult, CSSResult,
@ -108,13 +108,13 @@ export class HuiUnusedEntities extends LitElement {
selected: this._selectedEntities.length, selected: this._selectedEntities.length,
})}" })}"
> >
<mwc-fab <ha-fab
.label=${this.hass.localize("ui.panel.lovelace.editor.edit_card.add")} .label=${this.hass.localize("ui.panel.lovelace.editor.edit_card.add")}
extended extended
@click=${this._addToLovelaceView} @click=${this._addToLovelaceView}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
</div> </div>
`; `;
} }
@ -189,12 +189,12 @@ export class HuiUnusedEntities extends LitElement {
padding-right: 16px; padding-right: 16px;
padding-left: calc(16px + env(safe-area-inset-left)); padding-left: calc(16px + env(safe-area-inset-left));
} }
mwc-fab { ha-fab {
position: relative; position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom)); bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s; transition: bottom 0.3s;
} }
.fab.selected mwc-fab { .fab.selected ha-fab {
bottom: 0; bottom: 0;
} }
`; `;

View File

@ -25,7 +25,7 @@ export class HuiPictureHeaderFooter extends LitElement
public static getStubConfig(): Record<string, unknown> { public static getStubConfig(): Record<string, unknown> {
return { return {
image: image:
"https://www.home-assistant.io/images/merchandise/shirt-frontpage.png", "https://www.home-assistant.io/images/lovelace/header-footer/balloons-header.png",
tap_action: { action: "none" }, tap_action: { action: "none" },
hold_action: { action: "none" }, hold_action: { action: "none" },
}; };

View File

@ -793,10 +793,6 @@ class HUIRoot extends LitElement {
ha-app-layout { ha-app-layout {
min-height: 100%; min-height: 100%;
background: var(
--lovelace-background,
var(--primary-background-color)
);
} }
ha-tabs { ha-tabs {
width: 100%; width: 100%;
@ -884,6 +880,12 @@ class HUIRoot extends LitElement {
.menu-link { .menu-link {
text-decoration: none; text-decoration: none;
} }
hui-view {
background: var(
--lovelace-background,
var(--primary-background-color)
);
}
`, `,
]; ];
} }

View File

@ -57,7 +57,8 @@ class HuiAttributeRow extends LitElement implements LovelaceRow {
return html` return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}> <hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<div> <div>
${this._config.prefix} ${attribute || "-"} ${this._config.suffix} ${this._config.prefix} ${attribute ?? "-"}
${this._config.suffix}
</div> </div>
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;

View File

@ -1,3 +1,3 @@
// hui-view dependencies for when in edit mode. // hui-view dependencies for when in edit mode.
import "@material/mwc-fab"; import "../../../components/ha-fab";
import "../components/hui-card-options"; import "../components/hui-card-options";

View File

@ -83,7 +83,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
<div id="columns"></div> <div id="columns"></div>
${this.lovelace?.editMode ${this.lovelace?.editMode
? html` ? html`
<mwc-fab <ha-fab
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.add" "ui.panel.lovelace.editor.edit_card.add"
)} )}
@ -94,7 +94,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
})} })}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
` `
: ""} : ""}
`; `;
@ -293,10 +293,10 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
.column > * { .column > * {
display: block; display: block;
margin: 4px 4px 8px; margin: var(--masonry-view-card-margin, 4px 4px 8px);
} }
mwc-fab { ha-fab {
position: sticky; position: sticky;
float: right; float: right;
right: calc(16px + env(safe-area-inset-right)); right: calc(16px + env(safe-area-inset-right));
@ -304,7 +304,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
z-index: 1; z-index: 1;
} }
mwc-fab.rtl { ha-fab.rtl {
float: left; float: left;
right: auto; right: auto;
left: calc(16px + env(safe-area-inset-left)); left: calc(16px + env(safe-area-inset-left));

View File

@ -75,7 +75,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
${this._card} ${this._card}
${this.lovelace?.editMode && this.cards.length === 0 ${this.lovelace?.editMode && this.cards.length === 0
? html` ? html`
<mwc-fab <ha-fab
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.add" "ui.panel.lovelace.editor.edit_card.add"
)} )}
@ -86,7 +86,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
})} })}
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </ha-fab>
` `
: ""} : ""}
`; `;
@ -137,7 +137,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
height: 100%; height: 100%;
} }
mwc-fab { ha-fab {
position: sticky; position: sticky;
float: right; float: right;
right: calc(16px + env(safe-area-inset-right)); right: calc(16px + env(safe-area-inset-right));
@ -145,7 +145,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
z-index: 1; z-index: 1;
} }
mwc-fab.rtl { ha-fab.rtl {
float: left; float: left;
right: auto; right: auto;
left: calc(16px + env(safe-area-inset-left)); left: calc(16px + env(safe-area-inset-left));

View File

@ -1,154 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-dialog/paper-dialog";
import "../../components/ha-circular-progress";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/ha-card";
import LocalizeMixin from "../../mixins/localize-mixin";
import "../../styles/polymer-ha-style";
/*
* @appliesMixin LocalizeMixin
*/
class HaChangePasswordCard extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
.error {
color: red;
}
.status {
color: var(--primary-color);
}
.error,
.status {
position: absolute;
top: -4px;
}
.currentPassword {
margin-top: -4px;
}
</style>
<div>
<ha-card
header="[[localize('ui.panel.profile.change_password.header')]]"
>
<div class="card-content">
<template is="dom-if" if="[[_errorMsg]]">
<div class="error">[[_errorMsg]]</div>
</template>
<template is="dom-if" if="[[_statusMsg]]">
<div class="status">[[_statusMsg]]</div>
</template>
<paper-input
class="currentPassword"
label="[[localize('ui.panel.profile.change_password.current_password')]]"
type="password"
value="{{_currentPassword}}"
required
auto-validate
error-message="[[localize('ui.panel.profile.change_password.error_required')]]"
></paper-input>
<template is="dom-if" if="[[_currentPassword]]">
<paper-input
label="[[localize('ui.panel.profile.change_password.new_password')]]"
type="password"
value="{{_password1}}"
required
auto-validate
error-message="[[localize('ui.panel.profile.change_password.error_required')]]"
></paper-input>
<paper-input
label="[[localize('ui.panel.profile.change_password.confirm_new_password')]]"
type="password"
value="{{_password2}}"
required
auto-validate
error-message="[[localize('ui.panel.profile.change_password.error_required')]]"
></paper-input>
</template>
</div>
<div class="card-actions">
<template is="dom-if" if="[[_loading]]">
<div><ha-circular-progress active></ha-circular-progress></div>
</template>
<template is="dom-if" if="[[!_loading]]">
<mwc-button on-click="_changePassword"
>[[localize('ui.panel.profile.change_password.submit')]]</mwc-button
>
</template>
</div>
</ha-card>
</div>
`;
}
static get properties() {
return {
hass: Object,
_loading: {
type: Boolean,
value: false,
},
// Error message when can't talk to server etc
_statusMsg: String,
_errorMsg: String,
_currentPassword: String,
_password1: String,
_password2: String,
};
}
ready() {
super.ready();
this.addEventListener("keypress", (ev) => {
this._statusMsg = null;
if (ev.keyCode === 13) {
this._changePassword();
}
});
}
async _changePassword() {
this._statusMsg = null;
if (!this._currentPassword || !this._password1 || !this._password2) return;
if (this._password1 !== this._password2) {
this._errorMsg = "New password confirmation doesn't match";
return;
}
if (this._currentPassword === this._password1) {
this._errorMsg = "New password must be different than current password";
return;
}
this._loading = true;
this._errorMsg = null;
try {
await this.hass.callWS({
type: "config/auth_provider/homeassistant/change_password",
current_password: this._currentPassword,
new_password: this._password1,
});
this.setProperties({
_statusMsg: "Password changed successfully",
_currentPassword: null,
_password1: null,
_password2: null,
});
} catch (err) {
this._errorMsg = err.message;
}
this._loading = false;
}
}
customElements.define("ha-change-password-card", HaChangePasswordCard);

View File

@ -0,0 +1,195 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-dialog/paper-dialog";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import "@material/mwc-button";
import "../../components/ha-circular-progress";
import "../../components/ha-card";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
@customElement("ha-change-password-card")
class HaChangePasswordCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _loading = false;
@internalProperty() private _statusMsg?: string;
@internalProperty() private _errorMsg?: string;
@internalProperty() private _currentPassword?: string;
@internalProperty() private _password?: string;
@internalProperty() private _passwordConfirm?: string;
protected render(): TemplateResult {
return html`
<div>
<ha-card
.header=${this.hass.localize(
"ui.panel.profile.change_password.header"
)}
>
<div class="card-content">
${this._errorMsg
? html` <div class="error">${this._errorMsg}</div> `
: ""}
${this._statusMsg
? html` <div class="status">${this._statusMsg}</div> `
: ""}
<paper-input
id="currentPassword"
.label=${this.hass.localize(
"ui.panel.profile.change_password.current_password"
)}
type="password"
.value=${this._currentPassword}
@value-changed=${this._currentPasswordChanged}
required
></paper-input>
${this._currentPassword
? html` <paper-input
.label=${this.hass.localize(
"ui.panel.profile.change_password.new_password"
)}
name="password"
type="password"
.value=${this._password}
@value-changed=${this._newPasswordChanged}
required
auto-validate
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.profile.change_password.confirm_new_password"
)}
name="passwordConfirm"
type="password"
.value=${this._passwordConfirm}
@value-changed=${this._newPasswordConfirmChanged}
required
auto-validate
></paper-input>`
: ""}
</div>
<div class="card-actions">
${this._loading
? html`<div>
<ha-circular-progress active></ha-circular-progress>
</div>`
: html`<mwc-button
@click=${this._changePassword}
.disabled=${!this._passwordConfirm}
>${this.hass.localize(
"ui.panel.profile.change_password.submit"
)}</mwc-button
>`}
</div>
</ha-card>
</div>
`;
}
private _currentPasswordChanged(ev: CustomEvent) {
this._currentPassword = ev.detail.value;
}
private _newPasswordChanged(ev: CustomEvent) {
this._password = ev.detail.value;
}
private _newPasswordConfirmChanged(ev: CustomEvent) {
this._passwordConfirm = ev.detail.value;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("keypress", (ev) => {
this._statusMsg = undefined;
if (ev.keyCode === 13) {
this._changePassword();
}
});
}
private async _changePassword() {
this._statusMsg = undefined;
if (!this._currentPassword || !this._password || !this._passwordConfirm) {
return;
}
if (this._password !== this._passwordConfirm) {
this._errorMsg = this.hass.localize(
"ui.panel.profile.change_password.error_new_mismatch"
);
return;
}
if (this._currentPassword === this._password) {
this._errorMsg = this.hass.localize(
"ui.panel.profile.change_password.error_new_is_old"
);
return;
}
this._loading = true;
this._errorMsg = undefined;
try {
await this.hass.callWS({
type: "config/auth_provider/homeassistant/change_password",
current_password: this._currentPassword,
new_password: this._password,
});
} catch (err) {
this._errorMsg = err.message;
return;
} finally {
this._loading = false;
}
this._statusMsg = this.hass.localize(
"ui.panel.profile.change_password.success"
);
this._currentPassword = undefined;
this._password = undefined;
this._passwordConfirm = undefined;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.error {
color: var(--error-color);
}
.status {
color: var(--primary-color);
}
#currentPassword {
margin-top: -8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-change-password-card": HaChangePasswordCard;
}
}

View File

@ -25,7 +25,7 @@ class HaPushNotificationsRow extends LocalizeMixin(PolymerElement) {
>[[localize('ui.panel.profile.push_notifications.header')]]</span >[[localize('ui.panel.profile.push_notifications.header')]]</span
> >
<span slot="description"> <span slot="description">
[[_description(_platformLoaded, _pushSupported)]] [[localize(_descrLocalizeKey)]]
<a <a
href="[[_computeDocumentationUrl(hass)]]" href="[[_computeDocumentationUrl(hass)]]"
target="_blank" target="_blank"
@ -45,6 +45,10 @@ class HaPushNotificationsRow extends LocalizeMixin(PolymerElement) {
return { return {
hass: Object, hass: Object,
narrow: Boolean, narrow: Boolean,
_descrLocalizeKey: {
type: String,
computed: "_descriptionKey(_platformLoaded, _pushSupported)",
},
_platformLoaded: { _platformLoaded: {
type: Boolean, type: Boolean,
computed: "_compPlatformLoaded(hass)", computed: "_compPlatformLoaded(hass)",
@ -72,7 +76,7 @@ class HaPushNotificationsRow extends LocalizeMixin(PolymerElement) {
return !platformLoaded || !pushSupported_; return !platformLoaded || !pushSupported_;
} }
_description(platformLoaded, pushSupported_) { _descriptionKey(platformLoaded, pushSupported_) {
let key; let key;
if (!pushSupported_) { if (!pushSupported_) {
key = "error_use_https"; key = "error_use_https";
@ -81,7 +85,7 @@ class HaPushNotificationsRow extends LocalizeMixin(PolymerElement) {
} else { } else {
key = "description"; key = "description";
} }
return this.localize(`ui.panel.profile.push_notifications.${key}`); return `ui.panel.profile.push_notifications.${key}`;
} }
} }

View File

@ -89,6 +89,7 @@
} }
}, },
"groups": { "groups": {
"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"
@ -97,7 +98,8 @@
"disabled_by": { "disabled_by": {
"user": "User", "user": "User",
"integration": "Integration", "integration": "Integration",
"config_entry": "Config Entry" "config_entry": "Config Entry",
"device": "Device"
} }
}, },
"ui": { "ui": {
@ -333,6 +335,16 @@
"show_attributes": "Show attributes" "show_attributes": "Show attributes"
} }
}, },
"target-picker": {
"expand_area_id": "Expand this area in the seperate devices and entities that it contains. After expanding it will not update the devices and entities when the area changes.",
"expand_device_id": "Expand this device in seperate entities. After expanding it will not update the entities when the device changes.",
"remove_area_id": "Remove area",
"remove_device_id": "Remove device",
"remove_entity_id": "Remove entity",
"add_area_id": "Pick area",
"add_device_id": "Pick device",
"add_entity_id": "Pick entity"
},
"user-picker": { "user-picker": {
"no_user": "No user", "no_user": "No user",
"add_user": "Add user", "add_user": "Add user",
@ -611,6 +623,8 @@
"unavailable": "This entity is not currently available.", "unavailable": "This entity is not currently available.",
"enabled_label": "Enable entity", "enabled_label": "Enable entity",
"enabled_cause": "Disabled by {cause}.", "enabled_cause": "Disabled by {cause}.",
"device_disabled": "The device of this entity is disabled.",
"open_device_settings": "Open device settings",
"enabled_description": "Disabled entities will not be added to Home Assistant.", "enabled_description": "Disabled entities will not be added to Home Assistant.",
"enabled_delay_confirm": "The enabled entities will be added to Home Assistant in {delay} seconds", "enabled_delay_confirm": "The enabled entities will be added to Home Assistant in {delay} seconds",
"enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities", "enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities",
@ -786,7 +800,7 @@
}, },
"areas": { "areas": {
"caption": "Areas", "caption": "Areas",
"description": "Manage areas in your home", "description": "Group devices into areas",
"data_table": { "data_table": {
"area": "Area", "area": "Area",
"devices": "Devices" "devices": "Devices"
@ -816,7 +830,7 @@
}, },
"tags": { "tags": {
"caption": "Tags", "caption": "Tags",
"description": "Manage tags", "description": "Trigger automations when a NFC tag, QR code, etc. is scanned",
"learn_more": "Learn more about tags", "learn_more": "Learn more about tags",
"no_tags": "No tags", "no_tags": "No tags",
"add_tag": "Add tag", "add_tag": "Add tag",
@ -847,7 +861,7 @@
}, },
"helpers": { "helpers": {
"caption": "Helpers", "caption": "Helpers",
"description": "Manage elements that help build automations", "description": "Elements that help build automations",
"types": { "types": {
"input_text": "Text", "input_text": "Text",
"input_number": "Number", "input_number": "Number",
@ -875,7 +889,7 @@
}, },
"core": { "core": {
"caption": "General", "caption": "General",
"description": "Change your general Home Assistant configuration", "description": "Unit system, location, time zone & other general parameters",
"section": { "section": {
"core": { "core": {
"header": "General Configuration", "header": "General Configuration",
@ -904,7 +918,7 @@
"caption": "Info", "caption": "Info",
"copy_raw": "Raw Text", "copy_raw": "Raw Text",
"copy_github": "For GitHub", "copy_github": "For GitHub",
"description": "View info about your Home Assistant installation", "description": "Version, system health and links to documentation",
"home_assistant_logo": "Home Assistant logo", "home_assistant_logo": "Home Assistant logo",
"path_configuration": "Path to configuration.yaml: {path}", "path_configuration": "Path to configuration.yaml: {path}",
"developed_by": "Developed by a bunch of awesome people.", "developed_by": "Developed by a bunch of awesome people.",
@ -939,7 +953,7 @@
}, },
"lovelace": { "lovelace": {
"caption": "Lovelace Dashboards", "caption": "Lovelace Dashboards",
"description": "Manage your Lovelace Dashboards", "description": "Create customized sets of cards to control your home",
"dashboards": { "dashboards": {
"default_dashboard": "This is the default dashboard", "default_dashboard": "This is the default dashboard",
"caption": "Dashboards", "caption": "Dashboards",
@ -1094,7 +1108,7 @@
}, },
"automation": { "automation": {
"caption": "Automations", "caption": "Automations",
"description": "Manage automations", "description": "Create custom behavior rules for your home",
"picker": { "picker": {
"header": "Automation Editor", "header": "Automation Editor",
"introduction": "The automation editor allows you to create and edit automations. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.", "introduction": "The automation editor allows you to create and edit automations. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.",
@ -1474,7 +1488,7 @@
}, },
"script": { "script": {
"caption": "Scripts", "caption": "Scripts",
"description": "Manage scripts", "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.",
@ -1524,7 +1538,7 @@
}, },
"scene": { "scene": {
"caption": "Scenes", "caption": "Scenes",
"description": "Manage scenes", "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",
@ -1570,7 +1584,7 @@
"cloud": { "cloud": {
"description_login": "Logged in as {email}", "description_login": "Logged in as {email}",
"description_not_login": "Not logged in", "description_not_login": "Not logged in",
"description_features": "Control away from home, integrate with Alexa and Google Assistant.", "description_features": "Control home when away and integrate with Alexa and Google Assistant",
"login": { "login": {
"title": "Cloud Login", "title": "Cloud Login",
"introduction": "Home Assistant Cloud provides you with a secure remote connection to your instance while away from home. It also allows you to connect with cloud-only services: Amazon Alexa and Google Assistant.", "introduction": "Home Assistant Cloud provides you with a secure remote connection to your instance while away from home. It also allows you to connect with cloud-only services: Amazon Alexa and Google Assistant.",
@ -1737,18 +1751,27 @@
"devices": { "devices": {
"add_prompt": "No {name} have been added using this device yet. You can add one by clicking the + button above.", "add_prompt": "No {name} have been added using this device yet. You can add one by clicking the + button above.",
"caption": "Devices", "caption": "Devices",
"description": "Manage connected devices", "description": "Manage configured devices",
"device_info": "Device info", "device_info": "Device info",
"unnamed_device": "Unnamed device", "unnamed_device": "Unnamed device",
"unknown_error": "Unknown error", "unknown_error": "Unknown error",
"name": "Name", "name": "Name",
"update": "Update", "update": "Update",
"no_devices": "No devices", "no_devices": "No devices",
"enabled_label": "Enable device",
"enabled_cause": "The device is disabled by {cause}.",
"disabled_by": {
"user": "User",
"integration": "Integration",
"config_entry": "Config Entry"
},
"enabled_description": "Disabled devices will not be shown and entities belonging to the device will be disabled and not added to Home Assistant.",
"automation": { "automation": {
"automations": "Automations", "automations": "Automations",
"no_automations": "No automations", "no_automations": "No automations",
"unknown_automation": "Unknown automation", "unknown_automation": "Unknown automation",
"create": "Create automation with device", "create": "Create automation with device",
"create_disable": "Can't create automation with disabled device",
"triggers": { "triggers": {
"caption": "Do something when...", "caption": "Do something when...",
"no_triggers": "No triggers", "no_triggers": "No triggers",
@ -1769,12 +1792,14 @@
"script": { "script": {
"scripts": "Scripts", "scripts": "Scripts",
"no_scripts": "No scripts", "no_scripts": "No scripts",
"create": "Create script with device" "create": "Create script with device",
"create_disable": "Can't create script with disabled device"
}, },
"scene": { "scene": {
"scenes": "Scenes", "scenes": "Scenes",
"no_scenes": "No scenes", "no_scenes": "No scenes",
"create": "Create scene with device" "create": "Create scene with device",
"create_disable": "Can't create scene with disabled device"
}, },
"cant_edit": "You can only edit items that are created in the UI.", "cant_edit": "You can only edit items that are created in the UI.",
"device_not_found": "Device not found.", "device_not_found": "Device not found.",
@ -1789,6 +1814,7 @@
"scenes": "Scenes", "scenes": "Scenes",
"confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?", "confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?",
"confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!", "confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!",
"disabled": "Disabled",
"data_table": { "data_table": {
"device": "Device", "device": "Device",
"manufacturer": "Manufacturer", "manufacturer": "Manufacturer",
@ -1800,7 +1826,16 @@
"no_area": "No area" "no_area": "No area"
}, },
"delete": "Delete", "delete": "Delete",
"confirm_delete": "Are you sure you want to delete this device?" "confirm_delete": "Are you sure you want to delete this device?",
"picker": {
"search": "Search devices",
"filter": {
"filter": "Filter",
"show_disabled": "Show disabled devices",
"hidden_devices": "{number} hidden {number, plural,\n one {device}\n other {devices}\n}",
"show_all": "Show all"
}
}
}, },
"entities": { "entities": {
"caption": "Entities", "caption": "Entities",
@ -1829,6 +1864,7 @@
"name": "Name", "name": "Name",
"entity_id": "Entity ID", "entity_id": "Entity ID",
"integration": "Integration", "integration": "Integration",
"area": "Area",
"status": "Status" "status": "Status"
}, },
"selected": "{number} selected", "selected": "{number} selected",
@ -1914,7 +1950,7 @@
}, },
"integrations": { "integrations": {
"caption": "Integrations", "caption": "Integrations",
"description": "Manage integrations", "description": "Manage integrations with services, devices, ...",
"integration": "integration", "integration": "integration",
"discovered": "Discovered", "discovered": "Discovered",
"attention": "Attention required", "attention": "Attention required",
@ -1991,15 +2027,19 @@
"users_privileges_note": "The user group feature is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators.", "users_privileges_note": "The user group feature is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators.",
"picker": { "picker": {
"headers": { "headers": {
"name": "Name", "name": "Display name",
"username": "Username",
"group": "Group", "group": "Group",
"system": "System" "system": "System generated",
"is_active": "Active",
"is_owner": "Owner"
}, },
"add_user": "Add user" "add_user": "Add user"
}, },
"editor": { "editor": {
"caption": "View user", "caption": "View user",
"name": "Name", "name": "Display name",
"username": "Username",
"change_password": "Change password", "change_password": "Change password",
"new_password": "New Password", "new_password": "New Password",
"password_changed": "Password was changed successfully", "password_changed": "Password was changed successfully",
@ -2016,12 +2056,11 @@
"system_generated_users_not_removable": "Unable to remove system generated users.", "system_generated_users_not_removable": "Unable to remove system generated users.",
"system_generated_users_not_editable": "Unable to update system generated users.", "system_generated_users_not_editable": "Unable to update system generated users.",
"unnamed_user": "Unnamed User", "unnamed_user": "Unnamed User",
"confirm_user_deletion": "Are you sure you want to delete {name}?" "confirm_user_deletion": "Are you sure you want to delete {name}?",
"active_tooltip": "Controls if user can login"
}, },
"add_user": { "add_user": {
"caption": "Add user", "caption": "Add user",
"name": "Name",
"username": "Username",
"password": "Password", "password": "Password",
"password_confirm": "Confirm Password", "password_confirm": "Confirm Password",
"password_not_match": "Passwords don't match", "password_not_match": "Passwords don't match",
@ -2194,6 +2233,16 @@
"issue_zigbee_command": "Issue Zigbee Command", "issue_zigbee_command": "Issue Zigbee Command",
"help_command_dropdown": "Select a command to interact with." "help_command_dropdown": "Select a command to interact with."
}, },
"device_pairing_card": {
"PAIRED": "Device Found",
"PAIRED_status_text": "Starting Interview",
"INTERVIEW_COMPLETE": "Interview Complete",
"INTERVIEW_COMPLETE_status_text": "Configuring",
"CONFIGURED": "Configuration Complete",
"CONFIGURED_status_text": "Initializing",
"INITIALIZED": "Initialization Complete",
"INITIALIZED_status_text": "The device is ready to use"
},
"network": { "network": {
"caption": "Network" "caption": "Network"
}, },
@ -2857,7 +2906,10 @@
"new_password": "New Password", "new_password": "New Password",
"confirm_new_password": "Confirm New Password", "confirm_new_password": "Confirm New Password",
"error_required": "Required", "error_required": "Required",
"submit": "Submit" "submit": "Submit",
"error_new_mismatch": "Entered new password values do not match",
"error_new_is_old": "New password must be different than current password",
"success": "Password changed successfully"
}, },
"mfa": { "mfa": {
"header": "Multi-factor Authentication Modules", "header": "Multi-factor Authentication Modules",

View File

@ -7,6 +7,7 @@
} }
}, },
"groups": { "groups": {
"owner": "Vlastník",
"system-admin": "Správci", "system-admin": "Správci",
"system-read-only": "Uživatelé jen pro čtení", "system-read-only": "Uživatelé jen pro čtení",
"system-users": "Uživatelé" "system-users": "Uživatelé"
@ -587,7 +588,7 @@
"by_service": "službou", "by_service": "službou",
"entries_not_found": "Nenalezeny žádné záznamy.", "entries_not_found": "Nenalezeny žádné záznamy.",
"messages": { "messages": {
"became_unavailable": "stalo se nedostupným", "became_unavailable": "bylo nedostupné",
"changed_to_state": "změněno na {state}", "changed_to_state": "změněno na {state}",
"cleared_device_class": "zrušeno (nebylo zjištěno {device_class})", "cleared_device_class": "zrušeno (nebylo zjištěno {device_class})",
"detected_device_class": "zjištěno {device_class}", "detected_device_class": "zjištěno {device_class}",
@ -1068,7 +1069,7 @@
"delete_confirm": "Opravdu smazat?", "delete_confirm": "Opravdu smazat?",
"duplicate": "Duplikovat", "duplicate": "Duplikovat",
"header": "Akce", "header": "Akce",
"introduction": "Akce jsou to, co Home Assistant provede při spuštění automatizace. \n\n [Více informací o akcích.] (Https://home-assistant.io/docs/automation/action/)", "introduction": "Akce jsou to, co Home Assistant provede při spuštění automatizace.",
"learn_more": "Další informace o akcích", "learn_more": "Další informace o akcích",
"name": "Akce", "name": "Akce",
"type_select": "Typ akce", "type_select": "Typ akce",
@ -1150,7 +1151,7 @@
"delete_confirm": "Opravdu smazat?", "delete_confirm": "Opravdu smazat?",
"duplicate": "Duplikovat", "duplicate": "Duplikovat",
"header": "Podmínky", "header": "Podmínky",
"introduction": "Podmínky jsou volitelnou součástí automatizačního pravidla a mohou být použity k zabránění spuštění akce. Podmínky vypadají velmi podobně jako spouštěče, ale jsou velmi odlišné. Spouštěč se podívá na události, ke kterým dochází v systému, zatímco podmínka se zabývá pouze tím, jak systém vypadá právě teď. Pro spouštěč se může jevit že je spínač zapnutý. Stav může vidět pouze tehdy, je-li přepínač aktuálně zapnutý nebo vypnutý. \n\n [Další informace o podmínkách.] (Https://home-assistant.io/docs/scripts/conditions/)", "introduction": "Podmínky jsou volitelné a pokud nejsou všechny splněny, zabrání dalšímu provádění automatizace.",
"learn_more": "Další informace o podmínkách", "learn_more": "Další informace o podmínkách",
"name": "Podmínka", "name": "Podmínka",
"type_select": "Typ podmínky", "type_select": "Typ podmínky",
@ -1229,7 +1230,7 @@
"edit_ui": "Upravit pomocí uživatelského rozhraní", "edit_ui": "Upravit pomocí uživatelského rozhraní",
"edit_yaml": "Upravit jako YAML", "edit_yaml": "Upravit jako YAML",
"enable_disable": "Povolit / zakázat automatizaci", "enable_disable": "Povolit / zakázat automatizaci",
"introduction": "Použijte automatizace k oživení svého domova", "introduction": "Pomocí automatizací oživte svůj domov.",
"load_error_not_editable": "Lze upravovat pouze automatizace v automations.yaml.", "load_error_not_editable": "Lze upravovat pouze automatizace v automations.yaml.",
"load_error_unknown": "Chyba při načítání automatizace ({err_no}).", "load_error_unknown": "Chyba při načítání automatizace ({err_no}).",
"max": { "max": {
@ -1254,7 +1255,7 @@
"delete_confirm": "Opravdu smazat?", "delete_confirm": "Opravdu smazat?",
"duplicate": "Duplikovat", "duplicate": "Duplikovat",
"header": "Spouštěče", "header": "Spouštěče",
"introduction": "Spouštěče spouštějí zpracování automatizačního pravidla. Pro stejné pravidlo je možné zadat více spouštěčů. Po spuštění spouštěče ověří Home Assistant případné podmínky a zavolá akci. \n\n [Další informace o spouštěčích.] (Https://home-assistant.io/docs/automation/trigger/)", "introduction": "Spouštěče spouštějí automatizaci. Pro jednu automatizaci je možné zadat více spouštěčů. Po spuštění ověří Home Assistant případné podmínky a zavolá akci.",
"learn_more": "Další informace o spouštěčích", "learn_more": "Další informace o spouštěčích",
"name": "Spouštěč", "name": "Spouštěč",
"type_select": "Typ spouštěče", "type_select": "Typ spouštěče",
@ -1391,7 +1392,7 @@
"error_no_url": "Zadejte adresu URL šablonky konfigurace", "error_no_url": "Zadejte adresu URL šablonky konfigurace",
"header": "Přidat novou šablonku konfigurace", "header": "Přidat novou šablonku konfigurace",
"import_btn": "Importovat šablonku konfigurace", "import_btn": "Importovat šablonku konfigurace",
"import_header": "Importovat {name} ({domain})", "import_header": "Importovat \"{name}\" (typ: {domain})",
"import_introduction": "Z Githubu a komunitních fór můžete importovat šablonky konfigurace sdílené ostatními uživateli. Zadejte adresu URL šablonky konfigurace.", "import_introduction": "Z Githubu a komunitních fór můžete importovat šablonky konfigurace sdílené ostatními uživateli. Zadejte adresu URL šablonky konfigurace.",
"importing": "Importuje se šablonka konfigurace", "importing": "Importuje se šablonka konfigurace",
"save_btn": "Uložit šablonku konfigurace", "save_btn": "Uložit šablonku konfigurace",
@ -1399,13 +1400,14 @@
"url": "URL šablonky konfigurace" "url": "URL šablonky konfigurace"
}, },
"overview": { "overview": {
"add_blueprint": "Přidat šablonku konfigurace",
"confirm_delete_header": "Odstranit tuto šablonku konfigurace?", "confirm_delete_header": "Odstranit tuto šablonku konfigurace?",
"confirm_delete_text": "Opravdu chcete smazat tuto šablonku konfigurace?",
"headers": { "headers": {
"domain": "Doména",
"file_name": "Název souboru",
"name": "Jméno" "name": "Jméno"
}, },
"learn_more": "Další informace o šablonkách konfigurace" "learn_more": "Další informace o šablonkách konfigurace",
"use_blueprint": "Vytvořit automatizaci"
} }
}, },
"cloud": { "cloud": {
@ -1722,6 +1724,7 @@
}, },
"header": "Entity", "header": "Entity",
"headers": { "headers": {
"area": "Oblast",
"entity_id": "ID entity", "entity_id": "ID entity",
"integration": "Integrace", "integration": "Integrace",
"name": "Název", "name": "Název",
@ -2378,7 +2381,7 @@
"delete_user": "Odstranit uživatele", "delete_user": "Odstranit uživatele",
"group": "Skupina", "group": "Skupina",
"id": "ID", "id": "ID",
"name": "Jméno", "name": "Zobrazované jméno",
"new_password": "Nové heslo", "new_password": "Nové heslo",
"owner": "Vlastník", "owner": "Vlastník",
"password_changed": "Heslo bylo změněno", "password_changed": "Heslo bylo změněno",
@ -2386,19 +2389,24 @@
"system_generated_users_not_editable": "Nelze aktualizovat uživatele generované systémem.", "system_generated_users_not_editable": "Nelze aktualizovat uživatele generované systémem.",
"system_generated_users_not_removable": "Nelze odebrat uživatele generované systémem.", "system_generated_users_not_removable": "Nelze odebrat uživatele generované systémem.",
"unnamed_user": "Nepojmenovaný uživatel", "unnamed_user": "Nepojmenovaný uživatel",
"update_user": "Aktualizovat" "update_user": "Aktualizovat",
"username": "Uživatelské jméno"
}, },
"picker": { "picker": {
"add_user": "Přidat uživatele", "add_user": "Přidat uživatele",
"headers": { "headers": {
"group": "Skupina", "group": "Skupina",
"name": "Název", "is_active": "Aktivní",
"system": "Systémový" "is_owner": "Vlastník",
"name": "Zobrazované jméno",
"system": "Vygenerovaný systémem",
"username": "Uživatelské jméno"
} }
}, },
"users_privileges_note": "Skupiny uživatelů jsou v přípravě. Uživatel je nebude moci spravovat prostřednictvím uživatelského rozhraní. Stále kontrolujeme API pro správu, abychom zajistili, že správně omezuje přístup pouze pro administrátory." "users_privileges_note": "Skupiny uživatelů jsou v přípravě. Uživatel je nebude moci spravovat prostřednictvím uživatelského rozhraní. Stále kontrolujeme API pro správu, abychom zajistili, že správně omezuje přístup pouze pro administrátory."
}, },
"zha": { "zha": {
"add_device": "Přidat zařízení",
"add_device_page": { "add_device_page": {
"discovered_text": "Jakmile se objeví nalezne, zobrazí se zde.", "discovered_text": "Jakmile se objeví nalezne, zobrazí se zde.",
"discovery_text": "Zde se objeví nalezená zařízení. Postupujte dle pokynů pro vaše zařízení a uveďte je do režimu párování.", "discovery_text": "Zde se objeví nalezená zařízení. Postupujte dle pokynů pro vaše zařízení a uveďte je do režimu párování.",
@ -2444,6 +2452,16 @@
"value": "Hodnota" "value": "Hodnota"
}, },
"description": "Správa Zigbee Home Automation", "description": "Správa Zigbee Home Automation",
"device_pairing_card": {
"CONFIGURED": "Nastavení dokončeno",
"CONFIGURED_status_text": "Inicializuji",
"INITIALIZED": "Inicializace dokončena",
"INITIALIZED_status_text": "Zařízení je připraveno k použití",
"INTERVIEW_COMPLETE": "Dotazování dokončeno",
"INTERVIEW_COMPLETE_status_text": "Nastavuji",
"PAIRED": "Zařízení nalezeno",
"PAIRED_status_text": "Začínám dotazování"
},
"devices": { "devices": {
"header": "Zigbee Home Automation - Zařízení" "header": "Zigbee Home Automation - Zařízení"
}, },
@ -2459,6 +2477,7 @@
"unbind_button_label": "Odpojit skupinu" "unbind_button_label": "Odpojit skupinu"
}, },
"groups": { "groups": {
"add_group": "Přidat do skupiny",
"add_members": "Přidání členů", "add_members": "Přidání členů",
"adding_members": "Přidání členů", "adding_members": "Přidání členů",
"caption": "Skupiny", "caption": "Skupiny",
@ -2501,7 +2520,11 @@
"hint_wakeup": "Některá zařízení, jako jsou senzory Xiaomi, mají tlačítko probuzení, které můžete stisknout v intervalu ~ 5 sekund, které udržuje zařízení probuzené, když s nimi komunikujete.", "hint_wakeup": "Některá zařízení, jako jsou senzory Xiaomi, mají tlačítko probuzení, které můžete stisknout v intervalu ~ 5 sekund, které udržuje zařízení probuzené, když s nimi komunikujete.",
"introduction": "Spouštějte ZHA příkazy, které ovlivňují specifické zařízení. Vyberte zařízení pro zobrazení seznamu dostupných příkazů." "introduction": "Spouštějte ZHA příkazy, které ovlivňují specifické zařízení. Vyberte zařízení pro zobrazení seznamu dostupných příkazů."
}, },
"title": "Zigbee Home Automation" "title": "Zigbee Home Automation",
"visualization": {
"caption": "Vizualizace",
"header": "Vizualizace sítě"
}
}, },
"zone": { "zone": {
"add_zone": "Přidat zónu", "add_zone": "Přidat zónu",

View File

@ -7,6 +7,7 @@
} }
}, },
"groups": { "groups": {
"owner": "Besitzer",
"system-admin": "Administratoren", "system-admin": "Administratoren",
"system-read-only": "Nur-Lesen Benutzer", "system-read-only": "Nur-Lesen Benutzer",
"system-users": "Benutzer" "system-users": "Benutzer"
@ -932,7 +933,7 @@
"telegram": "Telegram-Benachrichtigungsdienst neu laden", "telegram": "Telegram-Benachrichtigungsdienst neu laden",
"template": "Templates neu laden", "template": "Templates neu laden",
"trend": "Trend Entitäten neu laden", "trend": "Trend Entitäten neu laden",
"universal": "Universelle Medien Player Entitäten neu laden ", "universal": "Universelle Medien Player Entitäten neu laden",
"zone": "Zonen neu laden" "zone": "Zonen neu laden"
}, },
"server_control": { "server_control": {
@ -1239,7 +1240,7 @@
"edit_ui": "Mit der Benutzeroberfläche bearbeiten", "edit_ui": "Mit der Benutzeroberfläche bearbeiten",
"edit_yaml": "Als YAML bearbeiten", "edit_yaml": "Als YAML bearbeiten",
"enable_disable": "Automatisierung aktivieren / deaktivieren", "enable_disable": "Automatisierung aktivieren / deaktivieren",
"introduction": "Benutze Automatisierungen, um deinem Zuhause Leben einzuhauchen", "introduction": "Benutze Automatisierungen, um deinem Zuhause Leben einzuhauchen.",
"load_error_not_editable": "Nur Automatisierungen in automations.yaml sind editierbar.", "load_error_not_editable": "Nur Automatisierungen in automations.yaml sind editierbar.",
"load_error_unknown": "Fehler beim Laden der Automatisierung ({err_no}).", "load_error_unknown": "Fehler beim Laden der Automatisierung ({err_no}).",
"max": { "max": {
@ -1398,7 +1399,7 @@
}, },
"blueprint": { "blueprint": {
"add": { "add": {
"error_no_url": "Bitte gebe die URL des Blueprints ein.", "error_no_url": "Bitte gebe die URL des Bauplans ein.",
"header": "Neuen Bauplan hinzufügen", "header": "Neuen Bauplan hinzufügen",
"import_btn": "Bauplan importieren", "import_btn": "Bauplan importieren",
"import_header": "{name}({domain}) importieren", "import_header": "{name}({domain}) importieren",
@ -1407,7 +1408,7 @@
"save_btn": "Bauplan speichern", "save_btn": "Bauplan speichern",
"saving": "Bauplan wir gespeichert...", "saving": "Bauplan wir gespeichert...",
"unsupported_blueprint": "Dieser Bauplan wird nicht unterstützt", "unsupported_blueprint": "Dieser Bauplan wird nicht unterstützt",
"url": "URL des Blueprints" "url": "URL des Bauplans"
}, },
"caption": "Baupläne", "caption": "Baupläne",
"description": "Baupläne verwalten", "description": "Baupläne verwalten",
@ -1417,10 +1418,13 @@
"confirm_delete_text": "Bist du sicher, dass du diesen Bauplan löschen möchtest?", "confirm_delete_text": "Bist du sicher, dass du diesen Bauplan löschen möchtest?",
"header": "Bauplan-Editor", "header": "Bauplan-Editor",
"headers": { "headers": {
"domain": "Domain",
"file_name": "Dateiname",
"name": "Name" "name": "Name"
}, },
"introduction": "Mit dem Bauplan-Editor kannst du Baupläne erstellen und bearbeiten.", "introduction": "Mit dem Bauplan-Editor kannst du Baupläne erstellen und bearbeiten.",
"learn_more": "Erfahre mehr über Baupläne" "learn_more": "Erfahre mehr über Baupläne",
"use_blueprint": "Automatisierung erstellen"
} }
}, },
"cloud": { "cloud": {
@ -1737,6 +1741,7 @@
}, },
"header": "Entitäten", "header": "Entitäten",
"headers": { "headers": {
"area": "Bereich",
"entity_id": "Entitäts-ID", "entity_id": "Entitäts-ID",
"integration": "Integration", "integration": "Integration",
"name": "Name", "name": "Name",
@ -2401,19 +2406,24 @@
"system_generated_users_not_editable": "Systemgenerierte Benutzer können nicht aktualisiert werden.", "system_generated_users_not_editable": "Systemgenerierte Benutzer können nicht aktualisiert werden.",
"system_generated_users_not_removable": "Vom System generierte Benutzer können nicht entfernt werden.", "system_generated_users_not_removable": "Vom System generierte Benutzer können nicht entfernt werden.",
"unnamed_user": "Unbenannter Benutzer", "unnamed_user": "Unbenannter Benutzer",
"update_user": "Aktualisieren" "update_user": "Aktualisieren",
"username": "Benutzername"
}, },
"picker": { "picker": {
"add_user": "Benutzer hinzufügen", "add_user": "Benutzer hinzufügen",
"headers": { "headers": {
"group": "Gruppe", "group": "Gruppe",
"is_active": "Aktiv",
"is_owner": "Besitzer",
"name": "Name", "name": "Name",
"system": "System" "system": "System",
"username": "Benutzername"
} }
}, },
"users_privileges_note": "Benutzergruppen sind derzeit noch in Entwicklung. Der Benutzer wird nicht in der Lage sein, Änderungen an der Instanz über das UI vorzunehmen. Derzeit überprüfen wir noch alle API Endpunkte um sicherzustellen dass diese nur von Administratoren genutzt werden können." "users_privileges_note": "Benutzergruppen sind derzeit noch in Entwicklung. Der Benutzer wird nicht in der Lage sein, Änderungen an der Instanz über das UI vorzunehmen. Derzeit überprüfen wir noch alle API Endpunkte um sicherzustellen dass diese nur von Administratoren genutzt werden können."
}, },
"zha": { "zha": {
"add_device": "Gerät hinzufügen",
"add_device_page": { "add_device_page": {
"discovered_text": "Geräte werden hier angezeigt sobald sie erkannt worden sind.", "discovered_text": "Geräte werden hier angezeigt sobald sie erkannt worden sind.",
"discovery_text": "Erkannte Geräte werden hier angezeigt. Befolgen Sie die Anweisungen für Ihr Gerät und versetzen Sie das Gerät in den Pairing-Modus.", "discovery_text": "Erkannte Geräte werden hier angezeigt. Befolgen Sie die Anweisungen für Ihr Gerät und versetzen Sie das Gerät in den Pairing-Modus.",
@ -2459,6 +2469,14 @@
"value": "Wert" "value": "Wert"
}, },
"description": "Zigbee Home Automation Netzwerkmanagement", "description": "Zigbee Home Automation Netzwerkmanagement",
"device_pairing_card": {
"CONFIGURED": "Konfiguration abgeschlossen",
"CONFIGURED_status_text": "Initialisieren",
"INITIALIZED": "Initialisierung abgeschlossen",
"INITIALIZED_status_text": "Das Gerät ist einsatzbereit",
"INTERVIEW_COMPLETE_status_text": "Konfigurieren",
"PAIRED": "Gerät gefunden"
},
"devices": { "devices": {
"header": "Zigbee Home Automation - Gerät" "header": "Zigbee Home Automation - Gerät"
}, },
@ -2474,6 +2492,7 @@
"unbind_button_label": "Gruppe auflösen" "unbind_button_label": "Gruppe auflösen"
}, },
"groups": { "groups": {
"add_group": "Gruppe hinzufügen",
"add_members": "Mitglieder hinzufügen", "add_members": "Mitglieder hinzufügen",
"adding_members": "Füge Mitglieder hinzu", "adding_members": "Füge Mitglieder hinzu",
"caption": "Gruppen", "caption": "Gruppen",
@ -2516,7 +2535,11 @@
"hint_wakeup": "Einige Geräte, z. B. Xiaomi-Sensoren, verfügen über eine Wecktaste, die in Intervallen von ~5 Sekunden gedrückt werden kann, um die Geräte wach zu halten, während mit ihnen interagiert wird.", "hint_wakeup": "Einige Geräte, z. B. Xiaomi-Sensoren, verfügen über eine Wecktaste, die in Intervallen von ~5 Sekunden gedrückt werden kann, um die Geräte wach zu halten, während mit ihnen interagiert wird.",
"introduction": "ZHA-Befehle ausführen, die sich auf ein einzelnes Gerät auswirken. Wählen Sie ein Gerät aus, um eine Liste der verfügbaren Befehle anzuzeigen." "introduction": "ZHA-Befehle ausführen, die sich auf ein einzelnes Gerät auswirken. Wählen Sie ein Gerät aus, um eine Liste der verfügbaren Befehle anzuzeigen."
}, },
"title": "Zigbee Home Automation" "title": "Zigbee Home Automation",
"visualization": {
"caption": "Visualisierung",
"header": "Netzwerkvisualisierung"
}
}, },
"zone": { "zone": {
"add_zone": "Zone hinzufügen", "add_zone": "Zone hinzufügen",
@ -3401,7 +3424,7 @@
}, },
"enable_shortcuts": { "enable_shortcuts": {
"description": "Aktiviere oder deaktiviere Tastaturkürzel, um verschiedene Aktionen in der Benutzeroberfläche auszuführen.", "description": "Aktiviere oder deaktiviere Tastaturkürzel, um verschiedene Aktionen in der Benutzeroberfläche auszuführen.",
"header": "Tastatürkürzel" "header": "Tastaturkürzel"
}, },
"force_narrow": { "force_narrow": {
"description": "Dies blendet die Seitenleiste standardmäßig aus, ähnlich der Nutzung auf Mobilgeräten.", "description": "Dies blendet die Seitenleiste standardmäßig aus, ähnlich der Nutzung auf Mobilgeräten.",

View File

@ -7,6 +7,7 @@
} }
}, },
"groups": { "groups": {
"owner": "Owner",
"system-admin": "Administrators", "system-admin": "Administrators",
"system-read-only": "Read-Only Users", "system-read-only": "Read-Only Users",
"system-users": "Users" "system-users": "Users"
@ -1239,7 +1240,7 @@
"edit_ui": "Edit with UI", "edit_ui": "Edit with UI",
"edit_yaml": "Edit as YAML", "edit_yaml": "Edit as YAML",
"enable_disable": "Enable/Disable automation", "enable_disable": "Enable/Disable automation",
"introduction": "Use automations to bring your home alive.", "introduction": "Use automations to bring your home to live.",
"load_error_not_editable": "Only automations in automations.yaml are editable.", "load_error_not_editable": "Only automations in automations.yaml are editable.",
"load_error_unknown": "Error loading automation ({err_no}).", "load_error_unknown": "Error loading automation ({err_no}).",
"max": { "max": {
@ -1399,11 +1400,13 @@
"blueprint": { "blueprint": {
"add": { "add": {
"error_no_url": "Please enter the URL of the blueprint.", "error_no_url": "Please enter the URL of the blueprint.",
"file_name": "Local blueprint file name",
"header": "Add new blueprint", "header": "Add new blueprint",
"import_btn": "Import blueprint", "import_btn": "Import blueprint",
"import_header": "Import {name} ({domain})", "import_header": "Import \"{name}\" (type: {domain})",
"import_introduction": "You can import blueprints of other users from Github and the community forums. Enter the URL of the blueprint below.", "import_introduction": "You can import blueprints of other users from Github and the community forums. Enter the URL of the blueprint below.",
"importing": "Importing blueprint...", "importing": "Importing blueprint...",
"raw_blueprint": "Blueprint content",
"save_btn": "Save blueprint", "save_btn": "Save blueprint",
"saving": "Saving blueprint...", "saving": "Saving blueprint...",
"unsupported_blueprint": "This blueprint is not supported", "unsupported_blueprint": "This blueprint is not supported",
@ -1412,15 +1415,19 @@
"caption": "Blueprints", "caption": "Blueprints",
"description": "Manage blueprints", "description": "Manage blueprints",
"overview": { "overview": {
"add_blueprint": "Add blueprint", "add_blueprint": "Import blueprint",
"confirm_delete_header": "Delete this blueprint?", "confirm_delete_header": "Delete this blueprint?",
"confirm_delete_text": "Are you sure you want to delete this blueprint", "confirm_delete_text": "Are you sure you want to delete this blueprint?",
"delete_blueprint": "Delete blueprint",
"header": "Blueprint Editor", "header": "Blueprint Editor",
"headers": { "headers": {
"domain": "Domain",
"file_name": "File name",
"name": "Name" "name": "Name"
}, },
"introduction": "The blueprint editor allows you to create and edit blueprints.", "introduction": "The blueprint editor allows you to create and edit blueprints.",
"learn_more": "Learn more about blueprints" "learn_more": "Learn more about blueprints",
"use_blueprint": "Create automation"
} }
}, },
"cloud": { "cloud": {
@ -1737,6 +1744,7 @@
}, },
"header": "Entities", "header": "Entities",
"headers": { "headers": {
"area": "Area",
"entity_id": "Entity ID", "entity_id": "Entity ID",
"integration": "Integration", "integration": "Integration",
"name": "Name", "name": "Name",
@ -2209,7 +2217,7 @@
"without_device": "Entities without device" "without_device": "Entities without device"
}, },
"icon": "Icon", "icon": "Icon",
"introduction": "Use scenes to bring your home alive.", "introduction": "Use scenes to bring your home to live.",
"load_error_not_editable": "Only scenes in scenes.yaml are editable.", "load_error_not_editable": "Only scenes in scenes.yaml are editable.",
"load_error_unknown": "Error loading scene ({err_no}).", "load_error_unknown": "Error loading scene ({err_no}).",
"name": "Name", "name": "Name",
@ -2393,7 +2401,7 @@
"delete_user": "Delete user", "delete_user": "Delete user",
"group": "Group", "group": "Group",
"id": "ID", "id": "ID",
"name": "Name", "name": "Display name",
"new_password": "New Password", "new_password": "New Password",
"owner": "Owner", "owner": "Owner",
"password_changed": "Password was changed successfully", "password_changed": "Password was changed successfully",
@ -2401,19 +2409,24 @@
"system_generated_users_not_editable": "Unable to update system generated users.", "system_generated_users_not_editable": "Unable to update system generated users.",
"system_generated_users_not_removable": "Unable to remove system generated users.", "system_generated_users_not_removable": "Unable to remove system generated users.",
"unnamed_user": "Unnamed User", "unnamed_user": "Unnamed User",
"update_user": "Update" "update_user": "Update",
"username": "Username"
}, },
"picker": { "picker": {
"add_user": "Add user", "add_user": "Add user",
"headers": { "headers": {
"group": "Group", "group": "Group",
"name": "Name", "is_active": "Active",
"system": "System" "is_owner": "Owner",
"name": "Display name",
"system": "System generated",
"username": "Username"
} }
}, },
"users_privileges_note": "The user group feature is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators." "users_privileges_note": "The user group feature is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators."
}, },
"zha": { "zha": {
"add_device": "Add Device",
"add_device_page": { "add_device_page": {
"discovered_text": "Devices will show up here once discovered.", "discovered_text": "Devices will show up here once discovered.",
"discovery_text": "Discovered devices will show up here. Follow the instructions for your device(s) and place the device(s) in pairing mode.", "discovery_text": "Discovered devices will show up here. Follow the instructions for your device(s) and place the device(s) in pairing mode.",
@ -2459,6 +2472,16 @@
"value": "Value" "value": "Value"
}, },
"description": "Zigbee Home Automation network management", "description": "Zigbee Home Automation network management",
"device_pairing_card": {
"CONFIGURED": "Configuration Complete",
"CONFIGURED_status_text": "Initializing",
"INITIALIZED": "Initialization Complete",
"INITIALIZED_status_text": "The device is ready to use",
"INTERVIEW_COMPLETE": "Interview Complete",
"INTERVIEW_COMPLETE_status_text": "Configuring",
"PAIRED": "Device Found",
"PAIRED_status_text": "Starting Interview"
},
"devices": { "devices": {
"header": "Zigbee Home Automation - Device" "header": "Zigbee Home Automation - Device"
}, },
@ -2474,6 +2497,7 @@
"unbind_button_label": "Unbind Group" "unbind_button_label": "Unbind Group"
}, },
"groups": { "groups": {
"add_group": "Add Group",
"add_members": "Add Members", "add_members": "Add Members",
"adding_members": "Adding Members", "adding_members": "Adding Members",
"caption": "Groups", "caption": "Groups",
@ -2516,7 +2540,11 @@
"hint_wakeup": "Some devices such as Xiaomi sensors have a wake up button that you can press at ~5 second intervals that keep devices awake while you interact with them.", "hint_wakeup": "Some devices such as Xiaomi sensors have a wake up button that you can press at ~5 second intervals that keep devices awake while you interact with them.",
"introduction": "Run ZHA commands that affect a single device. Pick a device to see a list of available commands." "introduction": "Run ZHA commands that affect a single device. Pick a device to see a list of available commands."
}, },
"title": "Zigbee Home Automation" "title": "Zigbee Home Automation",
"visualization": {
"caption": "Visualization",
"header": "Network Visualization"
}
}, },
"zone": { "zone": {
"add_zone": "Add Zone", "add_zone": "Add Zone",

View File

@ -7,6 +7,7 @@
} }
}, },
"groups": { "groups": {
"owner": "Propietario",
"system-admin": "Administradores", "system-admin": "Administradores",
"system-read-only": "Usuarios de solo lectura", "system-read-only": "Usuarios de solo lectura",
"system-users": "Usuarios" "system-users": "Usuarios"
@ -1147,7 +1148,7 @@
}, },
"alias": "Nombre", "alias": "Nombre",
"blueprint": { "blueprint": {
"blueprint_to_use": "Plano a utilizar", "blueprint_to_use": "Plano a usar",
"header": "Plano", "header": "Plano",
"inputs": "Entradas", "inputs": "Entradas",
"manage_blueprints": "Administrar planos", "manage_blueprints": "Administrar planos",
@ -1239,7 +1240,7 @@
"edit_ui": "Editar con la interfaz de usuario", "edit_ui": "Editar con la interfaz de usuario",
"edit_yaml": "Editar como YAML", "edit_yaml": "Editar como YAML",
"enable_disable": "Activar/Desactivar automatización", "enable_disable": "Activar/Desactivar automatización",
"introduction": "Utiliza automatizaciones para darle vida a tu hogar.", "introduction": "Usa automatizaciones para darle vida a tu hogar.",
"load_error_not_editable": "Solo las automatizaciones en automations.yaml son editables.", "load_error_not_editable": "Solo las automatizaciones en automations.yaml son editables.",
"load_error_unknown": "Error al cargar la automatización ({err_no}).", "load_error_unknown": "Error al cargar la automatización ({err_no}).",
"max": { "max": {
@ -1399,11 +1400,13 @@
"blueprint": { "blueprint": {
"add": { "add": {
"error_no_url": "Por favor, introduce la URL del plano.", "error_no_url": "Por favor, introduce la URL del plano.",
"file_name": "Nombre de archivo del plano local",
"header": "Añadir nuevo plano", "header": "Añadir nuevo plano",
"import_btn": "Importar plano", "import_btn": "Importar plano",
"import_header": "Importar {name} ({domain})", "import_header": "Importar {name} (tipo: {domain})",
"import_introduction": "Puedes importar planos de otros usuarios desde Github y los foros de la comunidad. Introduce la URL del plano a continuación.", "import_introduction": "Puedes importar planos de otros usuarios desde Github y los foros de la comunidad. Introduce la URL del plano a continuación.",
"importing": "Importando plano...", "importing": "Importando plano...",
"raw_blueprint": "Contenido del plano",
"save_btn": "Guardar plano", "save_btn": "Guardar plano",
"saving": "Guardando plano...", "saving": "Guardando plano...",
"unsupported_blueprint": "Este plano no es compatible", "unsupported_blueprint": "Este plano no es compatible",
@ -1412,15 +1415,19 @@
"caption": "Planos", "caption": "Planos",
"description": "Administrar planos", "description": "Administrar planos",
"overview": { "overview": {
"add_blueprint": "Añadir plano", "add_blueprint": "Importar plano",
"confirm_delete_header": "¿Eliminar este plano?", "confirm_delete_header": "¿Eliminar este plano?",
"confirm_delete_text": "¿Estás seguro de que quieres borrar este plano?", "confirm_delete_text": "¿Estás seguro de que quieres borrar este plano?",
"delete_blueprint": "Eliminar plano",
"header": "Editor de planos", "header": "Editor de planos",
"headers": { "headers": {
"domain": "Dominio",
"file_name": "Nombre de archivo",
"name": "Nombre" "name": "Nombre"
}, },
"introduction": "El editor de planos te permite crear y editar planos.", "introduction": "El editor de planos te permite crear y editar planos.",
"learn_more": "Más información sobre planos" "learn_more": "Más información sobre planos",
"use_blueprint": "Crear automatización"
} }
}, },
"cloud": { "cloud": {
@ -1737,6 +1744,7 @@
}, },
"header": "Entidades", "header": "Entidades",
"headers": { "headers": {
"area": "Área",
"entity_id": "ID de entidad", "entity_id": "ID de entidad",
"integration": "Integración", "integration": "Integración",
"name": "Nombre", "name": "Nombre",
@ -2209,7 +2217,7 @@
"without_device": "Entidades sin dispositivo" "without_device": "Entidades sin dispositivo"
}, },
"icon": "Icono", "icon": "Icono",
"introduction": "Usa escenas para dar vida a tu hogar.", "introduction": "Usa escenas para darle vida a tu hogar.",
"load_error_not_editable": "Solo las escenas de scenes.yaml son editables.", "load_error_not_editable": "Solo las escenas de scenes.yaml son editables.",
"load_error_unknown": "Error al cargar la escena ({err_no}).", "load_error_unknown": "Error al cargar la escena ({err_no}).",
"name": "Nombre", "name": "Nombre",
@ -2393,7 +2401,7 @@
"delete_user": "Eliminar usuario", "delete_user": "Eliminar usuario",
"group": "Grupo", "group": "Grupo",
"id": "ID", "id": "ID",
"name": "Nombre", "name": "Nombre para mostrar",
"new_password": "Nueva contraseña", "new_password": "Nueva contraseña",
"owner": "Propietario", "owner": "Propietario",
"password_changed": "La contraseña se ha cambiado correctamente", "password_changed": "La contraseña se ha cambiado correctamente",
@ -2401,19 +2409,24 @@
"system_generated_users_not_editable": "No se pueden actualizar los usuarios generados por el sistema.", "system_generated_users_not_editable": "No se pueden actualizar los usuarios generados por el sistema.",
"system_generated_users_not_removable": "No se pueden eliminar los usuarios generados por el sistema.", "system_generated_users_not_removable": "No se pueden eliminar los usuarios generados por el sistema.",
"unnamed_user": "Usuario sin nombre", "unnamed_user": "Usuario sin nombre",
"update_user": "Actualizar" "update_user": "Actualizar",
"username": "Nombre de usuario"
}, },
"picker": { "picker": {
"add_user": "Añadir usuario", "add_user": "Añadir usuario",
"headers": { "headers": {
"group": "Grupo", "group": "Grupo",
"name": "Nombre", "is_active": "Activo",
"system": "Sistema" "is_owner": "Propietario",
"name": "Nombre para mostrar",
"system": "Generado por el sistema",
"username": "Nombre de usuario"
} }
}, },
"users_privileges_note": "El grupo de usuarios es un trabajo en progreso. El usuario no podrá administrar la instancia a través de la IU. Todavía estamos auditando todos los endpoints de la API de administración para garantizar que se limita correctamente el acceso sólo a los administradores." "users_privileges_note": "El grupo de usuarios es un trabajo en progreso. El usuario no podrá administrar la instancia a través de la IU. Todavía estamos auditando todos los endpoints de la API de administración para garantizar que se limita correctamente el acceso sólo a los administradores."
}, },
"zha": { "zha": {
"add_device": "Añadir dispositivo",
"add_device_page": { "add_device_page": {
"discovered_text": "Los dispositivos aparecerán aquí una vez descubiertos.", "discovered_text": "Los dispositivos aparecerán aquí una vez descubiertos.",
"discovery_text": "Los dispositivos detectados aparecerán aquí. Ponlos en modo emparejamiento siguiendo sus instrucciones.", "discovery_text": "Los dispositivos detectados aparecerán aquí. Ponlos en modo emparejamiento siguiendo sus instrucciones.",
@ -2459,6 +2472,16 @@
"value": "Valor" "value": "Valor"
}, },
"description": "Administración de red de Domótica Zigbee", "description": "Administración de red de Domótica Zigbee",
"device_pairing_card": {
"CONFIGURED": "Configuración completada",
"CONFIGURED_status_text": "Inicializando",
"INITIALIZED": "Inicialización completada",
"INITIALIZED_status_text": "El dispositivo está listo para ser usado",
"INTERVIEW_COMPLETE": "Entrevista completada",
"INTERVIEW_COMPLETE_status_text": "Configurando",
"PAIRED": "Dispositivo encontrado",
"PAIRED_status_text": "Iniciando entrevista"
},
"devices": { "devices": {
"header": "Domótica Zigbee - Dispositivo" "header": "Domótica Zigbee - Dispositivo"
}, },
@ -2474,6 +2497,7 @@
"unbind_button_label": "Desvincular grupo" "unbind_button_label": "Desvincular grupo"
}, },
"groups": { "groups": {
"add_group": "Añadir grupo",
"add_members": "Añadir miembros", "add_members": "Añadir miembros",
"adding_members": "Agregar miembros", "adding_members": "Agregar miembros",
"caption": "Grupos", "caption": "Grupos",
@ -2516,7 +2540,11 @@
"hint_wakeup": "Algunos dispositivos, como los sensores Xiaomi, tienen un botón de activación que puedes presionar a intervalos de ~ 5 segundos para mantener los dispositivos despiertos mientras interactúas con ellos.", "hint_wakeup": "Algunos dispositivos, como los sensores Xiaomi, tienen un botón de activación que puedes presionar a intervalos de ~ 5 segundos para mantener los dispositivos despiertos mientras interactúas con ellos.",
"introduction": "Ejecuta comandos ZHA que afecten a un único dispositivo. Elije un dispositivo para ver una lista de comandos disponibles." "introduction": "Ejecuta comandos ZHA que afecten a un único dispositivo. Elije un dispositivo para ver una lista de comandos disponibles."
}, },
"title": "Domótica Zigbee" "title": "Domótica Zigbee",
"visualization": {
"caption": "Visualización",
"header": "Visualización de la red"
}
}, },
"zone": { "zone": {
"add_zone": "Añadir zona", "add_zone": "Añadir zona",

View File

@ -7,6 +7,7 @@
} }
}, },
"groups": { "groups": {
"owner": "Omanik",
"system-admin": "Administraatorid", "system-admin": "Administraatorid",
"system-read-only": "Ainult lugemisõigusega kasutajad", "system-read-only": "Ainult lugemisõigusega kasutajad",
"system-users": "Kasutajad" "system-users": "Kasutajad"
@ -967,7 +968,7 @@
"manuf": "{manufacturer} järgi", "manuf": "{manufacturer} järgi",
"no_area": "Ala puudub", "no_area": "Ala puudub",
"power_source": "Toiteallikas", "power_source": "Toiteallikas",
"quirk": "Erim", "quirk": "Pistikäpp",
"services": { "services": {
"reconfigure": "Taasseadista (tervenda) ZHA seade. Kasuta seda, kui seadmega on probleeme. Kui seade on akutoitega, siis veendu, et see oleks ärkvel ja oleks valmis käske vastu võtma.", "reconfigure": "Taasseadista (tervenda) ZHA seade. Kasuta seda, kui seadmega on probleeme. Kui seade on akutoitega, siis veendu, et see oleks ärkvel ja oleks valmis käske vastu võtma.",
"remove": "Eemalda seade Zigbee võrgust.", "remove": "Eemalda seade Zigbee võrgust.",
@ -1399,11 +1400,13 @@
"blueprint": { "blueprint": {
"add": { "add": {
"error_no_url": "Sisesta kavandi URL.", "error_no_url": "Sisesta kavandi URL.",
"file_name": "Kohaliku kavandifaili nimi",
"header": "Uue kavandi lisamine", "header": "Uue kavandi lisamine",
"import_btn": "Impordi kavand", "import_btn": "Impordi kavand",
"import_header": "Impordi {name} ( {domain} )", "import_header": "Impordi {name} ( type:{domain} )",
"import_introduction": "Teiste kasutajate kavandeid saad importida Githubist ja kogukonna foorumitest. Sisesta alloleva kavandi URL.", "import_introduction": "Teiste kasutajate kavandeid saad importida Githubist ja kogukonna foorumitest. Sisesta alloleva kavandi URL.",
"importing": "Kavandi importimine ...", "importing": "Kavandi importimine ...",
"raw_blueprint": "Kavandi sisu",
"save_btn": "Salvesta kavand", "save_btn": "Salvesta kavand",
"saving": "Kavandi salvestamine ...", "saving": "Kavandi salvestamine ...",
"unsupported_blueprint": "Seda kavandit ei toetata", "unsupported_blueprint": "Seda kavandit ei toetata",
@ -1412,15 +1415,19 @@
"caption": "", "caption": "",
"description": "Kavandite haldamine", "description": "Kavandite haldamine",
"overview": { "overview": {
"add_blueprint": "Lisa kavand", "add_blueprint": "Laadi kavand",
"confirm_delete_header": "Kas kustutada see kavand?", "confirm_delete_header": "Kas kustutada see kavand?",
"confirm_delete_text": "Kas soovid kindlasti selle kavandi kustutada?", "confirm_delete_text": "Kas soovid kindlasti selle kavandi kustutada?",
"delete_blueprint": "Kustuta kavand",
"header": "Kavandi redaktor", "header": "Kavandi redaktor",
"headers": { "headers": {
"domain": "Domeen",
"file_name": "Faili nimi",
"name": "Nimi" "name": "Nimi"
}, },
"introduction": "Kavandite redaktor võimaldab luua ja muuta kavandeid.", "introduction": "Kavandite redaktor võimaldab luua ja muuta kavandeid.",
"learn_more": "Lisateave kavandite kohta" "learn_more": "Lisateave kavandite kohta",
"use_blueprint": "Loo automatiseering"
} }
}, },
"cloud": { "cloud": {
@ -1737,6 +1744,7 @@
}, },
"header": "Olemid", "header": "Olemid",
"headers": { "headers": {
"area": "Ala",
"entity_id": "Olemi ID", "entity_id": "Olemi ID",
"integration": "Sidumine", "integration": "Sidumine",
"name": "Nimi", "name": "Nimi",
@ -2393,7 +2401,7 @@
"delete_user": "Kustuta kasutaja", "delete_user": "Kustuta kasutaja",
"group": "Grupp", "group": "Grupp",
"id": "ID", "id": "ID",
"name": "Nimi", "name": "Kuvatav nimi",
"new_password": "Uus salasõna", "new_password": "Uus salasõna",
"owner": "Omanik", "owner": "Omanik",
"password_changed": "Salasõna muutmine õnnestus", "password_changed": "Salasõna muutmine õnnestus",
@ -2401,19 +2409,24 @@
"system_generated_users_not_editable": "Süsteemi loodud kasutajaid ei saa värskendada.", "system_generated_users_not_editable": "Süsteemi loodud kasutajaid ei saa värskendada.",
"system_generated_users_not_removable": "Süsteemi loodud kasutajaid ei saa eemaldada.", "system_generated_users_not_removable": "Süsteemi loodud kasutajaid ei saa eemaldada.",
"unnamed_user": "Nimetu kasutaja", "unnamed_user": "Nimetu kasutaja",
"update_user": "Uuenda" "update_user": "Uuenda",
"username": "Kasutajanimi"
}, },
"picker": { "picker": {
"add_user": "Lisa kasutaja", "add_user": "Lisa kasutaja",
"headers": { "headers": {
"group": "Grupp", "group": "Grupp",
"name": "Nimi", "is_active": "Aktiivne",
"system": "Süsteem" "is_owner": "Omanik",
"name": "Kuvatav nimi",
"system": "Süsteemi poolt loodud",
"username": "Kasutajanimi"
} }
}, },
"users_privileges_note": "Kasutajarühma funktsioon on pooleli. Kasutajal ei ole võimalik kasutajaliidese kaudu Home Assistanti hallata. Auditeerime endiselt kõiki haldus-API lõpp-punkte tagamaks, et need lubaks juurdepääsu ainult administraatoritele." "users_privileges_note": "Kasutajarühma funktsioon on pooleli. Kasutajal ei ole võimalik kasutajaliidese kaudu Home Assistanti hallata. Auditeerime endiselt kõiki haldus-API lõpp-punkte tagamaks, et need lubaks juurdepääsu ainult administraatoritele."
}, },
"zha": { "zha": {
"add_device": "Lisa seade",
"add_device_page": { "add_device_page": {
"discovered_text": "Seadmed ilmuvad siia kui avastatakse.", "discovered_text": "Seadmed ilmuvad siia kui avastatakse.",
"discovery_text": "Leitud seadmed kuvatakse siin. Järgige seadme(te) juhiseid ja pange seade või seadmed sidumisrežiimile.", "discovery_text": "Leitud seadmed kuvatakse siin. Järgige seadme(te) juhiseid ja pange seade või seadmed sidumisrežiimile.",
@ -2459,6 +2472,16 @@
"value": "Väärtus" "value": "Väärtus"
}, },
"description": "Zigbee Home Automation võrgu haldamine", "description": "Zigbee Home Automation võrgu haldamine",
"device_pairing_card": {
"CONFIGURED": "Seadistamine on lõpetatud",
"CONFIGURED_status_text": "Lähtestan",
"INITIALIZED": "Lähtestamine on lõpetatud",
"INITIALIZED_status_text": "Seade on kasutamiseks valmis",
"INTERVIEW_COMPLETE": "Küsitlemine on lõpetatud",
"INTERVIEW_COMPLETE_status_text": "Seadistan",
"PAIRED": "Seade on leitud",
"PAIRED_status_text": "Alustan küsitlemist"
},
"devices": { "devices": {
"header": "Zigbee Home Automation - Seade" "header": "Zigbee Home Automation - Seade"
}, },
@ -2474,6 +2497,7 @@
"unbind_button_label": "Sidesta grupp lahti" "unbind_button_label": "Sidesta grupp lahti"
}, },
"groups": { "groups": {
"add_group": "Lisa grupp",
"add_members": "Lisa liikmeid", "add_members": "Lisa liikmeid",
"adding_members": "Liikmete lisamine", "adding_members": "Liikmete lisamine",
"caption": "Grupid", "caption": "Grupid",
@ -2516,7 +2540,11 @@
"hint_wakeup": "Mõnel seadmel (näiteks Xiaomi anduritel) on äratusnupp mida saate vajutada ~ 5-sekundiliste intervallidega ja mis hoiab seadmeid ärkvel kui te nendega suhtlete.", "hint_wakeup": "Mõnel seadmel (näiteks Xiaomi anduritel) on äratusnupp mida saate vajutada ~ 5-sekundiliste intervallidega ja mis hoiab seadmeid ärkvel kui te nendega suhtlete.",
"introduction": "Käivitage ZHA käsud, mis mõjutavad ühte seadet. Saadaolevate käskude loendi kuvamiseks valige seade." "introduction": "Käivitage ZHA käsud, mis mõjutavad ühte seadet. Saadaolevate käskude loendi kuvamiseks valige seade."
}, },
"title": "Zigbee Home Automation" "title": "Zigbee Home Automation",
"visualization": {
"caption": "Visualiseerimine",
"header": "Võrgu visualiseerimine"
}
}, },
"zone": { "zone": {
"add_zone": "Lisa tsoon", "add_zone": "Lisa tsoon",

View File

@ -1171,7 +1171,7 @@
"trigger": "Laukaisin" "trigger": "Laukaisin"
}, },
"event": { "event": {
"context_user_pick": "Lisää käyttäjä", "context_user_pick": "Valitse käyttäjä",
"context_user_picked": "Käyttäjän laukaisutapahtuma", "context_user_picked": "Käyttäjän laukaisutapahtuma",
"context_users": "Rajoita tapahtumiin, jotka ovat käynnistäneet", "context_users": "Rajoita tapahtumiin, jotka ovat käynnistäneet",
"event_data": "Tapahtuman tietosisältö", "event_data": "Tapahtuman tietosisältö",
@ -2113,6 +2113,7 @@
"person": "Lataa henkilöt uudelleen", "person": "Lataa henkilöt uudelleen",
"ping": "Lataa ping-binaarianturin entiteetit uudelleen", "ping": "Lataa ping-binaarianturin entiteetit uudelleen",
"reload": "Lataa uudelleen {domain}", "reload": "Lataa uudelleen {domain}",
"rest": "Uudelleenlataa rest-entiteetit ja ilmoita palveluiden kuuntelijoille",
"rpi_gpio": "Lataa Raspberry Pi GPIO -entiteetit uudelleen", "rpi_gpio": "Lataa Raspberry Pi GPIO -entiteetit uudelleen",
"scene": "Lataa tilanteet uudelleen", "scene": "Lataa tilanteet uudelleen",
"script": "Lataa skriptit uudelleen", "script": "Lataa skriptit uudelleen",
@ -2862,7 +2863,7 @@
}, },
"raw_editor": { "raw_editor": {
"confirm_remove_config_text": "Lovelace käyttöliittymän näkymät luodaan automaattisesti alueistasi ja laitteistasi, jos poistat nykyisen määrityksen.", "confirm_remove_config_text": "Lovelace käyttöliittymän näkymät luodaan automaattisesti alueistasi ja laitteistasi, jos poistat nykyisen määrityksen.",
"confirm_remove_config_title": "Haluatko varmasti poistaa Lovelace-käyttöliittymän asetukset? Lovelace käyttöliittymän asetukset luodaan automaattisesti alueistasi ja laitteistasi.", "confirm_remove_config_title": "Haluatko varmasti poistaa Lovelace-käyttöliittymän asetukset?",
"confirm_unsaved_changes": "Sinulla on tallentamattomia muutoksia. Haluatko varmasti poistua?", "confirm_unsaved_changes": "Sinulla on tallentamattomia muutoksia. Haluatko varmasti poistua?",
"confirm_unsaved_comments": "Asetuksesi sisältää kommentoituja rivejä. Kommentoituja rivejä ei tallenneta. Haluatko jatkaa?", "confirm_unsaved_comments": "Asetuksesi sisältää kommentoituja rivejä. Kommentoituja rivejä ei tallenneta. Haluatko jatkaa?",
"error_invalid_config": "Asetuksesi ovat virheelliset: {error}", "error_invalid_config": "Asetuksesi ovat virheelliset: {error}",
@ -2906,7 +2907,7 @@
}, },
"menu": { "menu": {
"close": "Sulje", "close": "Sulje",
"configure_ui": "Määrittele käyttöliittymä", "configure_ui": "Muokkaa käyttöliittymää",
"exit_edit_mode": "Poistu käyttöliittymän muokkaustilasta", "exit_edit_mode": "Poistu käyttöliittymän muokkaustilasta",
"help": "Apua", "help": "Apua",
"refresh": "Päivitä", "refresh": "Päivitä",
@ -2915,7 +2916,7 @@
}, },
"reload_lovelace": "Lataa Lovelace uudelleen", "reload_lovelace": "Lataa Lovelace uudelleen",
"reload_resources": { "reload_resources": {
"refresh_body": "Sinun täytyy päivittää sivu viimeistelläksesi uudelleenlatauksen, haluatko päivittää nyt?", "refresh_body": "Sinun täytyy päivittää sivu viimeistelläksesi uudelleenlatauksen. Haluatko päivittää nyt?",
"refresh_header": "Haluatko päivittää?" "refresh_header": "Haluatko päivittää?"
}, },
"unused_entities": { "unused_entities": {

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