diff --git a/build-scripts/gulp/compress.js b/build-scripts/gulp/compress.js index de604f7fa3..aee165c5f7 100644 --- a/build-scripts/gulp/compress.js +++ b/build-scripts/gulp/compress.js @@ -9,25 +9,30 @@ const paths = require("../paths"); gulp.task("compress-app", function compressApp() { const jsLatest = gulp .src(path.resolve(paths.output, "**/*.js")) - .pipe(zopfli()) + .pipe(zopfli({ threshold: 150 })) .pipe(gulp.dest(paths.output)); const jsEs5 = gulp .src(path.resolve(paths.output_es5, "**/*.js")) - .pipe(zopfli()) + .pipe(zopfli({ threshold: 150 })) .pipe(gulp.dest(paths.output_es5)); const polyfills = gulp .src(path.resolve(paths.static, "polyfills/*.js")) - .pipe(zopfli()) + .pipe(zopfli({ threshold: 150 })) .pipe(gulp.dest(path.resolve(paths.static, "polyfills"))); const translations = gulp - .src(path.resolve(paths.static, "translations/*.json")) - .pipe(zopfli()) + .src(path.resolve(paths.static, "translations/**/*.json")) + .pipe(zopfli({ threshold: 150 })) .pipe(gulp.dest(path.resolve(paths.static, "translations"))); - return merge(jsLatest, jsEs5, polyfills, translations); + const icons = gulp + .src(path.resolve(paths.static, "mdi/*.json")) + .pipe(zopfli({ threshold: 150 })) + .pipe(gulp.dest(path.resolve(paths.static, "mdi"))); + + return merge(jsLatest, jsEs5, polyfills, translations, icons); }); gulp.task("compress-hassio", function compressApp() { diff --git a/package.json b/package.json index e1c47e9cf3..d5f286178b 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@polymer/paper-toast": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1", "@polymer/polymer": "3.1.0", - "@thomasloven/round-slider": "0.3.7", + "@thomasloven/round-slider": "0.4.1", "@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-date-picker": "^4.0.7", "@webcomponents/shadycss": "^1.9.0", diff --git a/public/static/images/weather/cloudy.png b/public/static/images/weather/cloudy.png deleted file mode 100644 index 1277a7ad67..0000000000 Binary files a/public/static/images/weather/cloudy.png and /dev/null differ diff --git a/public/static/images/weather/lightning-rainy.png b/public/static/images/weather/lightning-rainy.png deleted file mode 100644 index 07f78b6c6f..0000000000 Binary files a/public/static/images/weather/lightning-rainy.png and /dev/null differ diff --git a/public/static/images/weather/lightning.png b/public/static/images/weather/lightning.png deleted file mode 100644 index a6aaed9a0c..0000000000 Binary files a/public/static/images/weather/lightning.png and /dev/null differ diff --git a/public/static/images/weather/night.png b/public/static/images/weather/night.png deleted file mode 100644 index 66f934624d..0000000000 Binary files a/public/static/images/weather/night.png and /dev/null differ diff --git a/public/static/images/weather/partly-cloudy.png b/public/static/images/weather/partly-cloudy.png deleted file mode 100644 index 6c59f6cc93..0000000000 Binary files a/public/static/images/weather/partly-cloudy.png and /dev/null differ diff --git a/public/static/images/weather/pouring.png b/public/static/images/weather/pouring.png deleted file mode 100644 index 487d5ac827..0000000000 Binary files a/public/static/images/weather/pouring.png and /dev/null differ diff --git a/public/static/images/weather/rainy.png b/public/static/images/weather/rainy.png deleted file mode 100644 index 0d429b11b0..0000000000 Binary files a/public/static/images/weather/rainy.png and /dev/null differ diff --git a/public/static/images/weather/snowy.png b/public/static/images/weather/snowy.png deleted file mode 100644 index e28b6e8274..0000000000 Binary files a/public/static/images/weather/snowy.png and /dev/null differ diff --git a/public/static/images/weather/sunny.png b/public/static/images/weather/sunny.png deleted file mode 100644 index 42016dfb46..0000000000 Binary files a/public/static/images/weather/sunny.png and /dev/null differ diff --git a/public/static/images/weather/windy.png b/public/static/images/weather/windy.png deleted file mode 100644 index 6db0122923..0000000000 Binary files a/public/static/images/weather/windy.png and /dev/null differ diff --git a/setup.py b/setup.py index 44e5ce0fe4..770c6db60a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200509.0", + version="20200512.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/components/ha-tab.ts b/src/components/ha-tab.ts index 0b956429c1..e58f820303 100644 --- a/src/components/ha-tab.ts +++ b/src/components/ha-tab.ts @@ -16,14 +16,6 @@ import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; import "./ha-icon"; import "./ha-svg-icon"; import { ifDefined } from "lit-html/directives/if-defined"; -import { fireEvent } from "../common/dom/fire_event"; - -declare global { - // for fire event - interface HASSDomEvents { - activated: undefined; - } -} @customElement("ha-tab") export class HaTab extends LitElement { @@ -54,11 +46,10 @@ export class HaTab extends LitElement { @touchend=${this.handleRippleDeactivate} @touchcancel=${this.handleRippleDeactivate} @keydown=${this._handleKeyDown} - @click=${this._handleClick} > ${this.narrow ? html`` : ""} ${!this.narrow || this.active - ? html` ${this.name} ` + ? html`${this.name}` : ""} ${this._shouldRenderRipple ? html`` : ""} @@ -72,14 +63,10 @@ export class HaTab extends LitElement { private _handleKeyDown(ev: KeyboardEvent): void { if (ev.keyCode === 13) { - fireEvent(this, "activated"); + (ev.target as HTMLElement).click(); } } - private _handleClick(): void { - fireEvent(this, "activated"); - } - @eventOptions({ passive: true }) private handleRippleActivate(evt?: Event) { this._rippleHandlers.startPress(evt); diff --git a/src/data/integration.ts b/src/data/integration.ts index ce0bf07fb5..8a59d2d4d7 100644 --- a/src/data/integration.ts +++ b/src/data/integration.ts @@ -18,7 +18,11 @@ export interface IntegrationManifest { quality_scale?: string; } -export const integrationIssuesUrl = (domain: string) => +export const integrationIssuesUrl = ( + domain: string, + manifest: IntegrationManifest +) => + manifest.issue_tracker || `https://github.com/home-assistant/home-assistant/issues?q=is%3Aissue+is%3Aopen+label%3A%22integration%3A+${domain}%22`; export const domainToName = (localize: LocalizeFunc, domain: string) => diff --git a/src/data/weather.ts b/src/data/weather.ts index 4fe0919191..199a5e68e9 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -1,26 +1,52 @@ -import { HomeAssistant, WeatherEntity } from "../types"; +import { SVGTemplateResult, svg, html, TemplateResult, css } from "lit-element"; +import { styleMap } from "lit-html/directives/style-map"; -export const weatherImages = { - "clear-night": "/static/images/weather/night.png", - cloudy: "/static/images/weather/cloudy.png", - fog: "/static/images/weather/cloudy.png", - lightning: "/static/images/weather/lightning.png", - "lightning-rainy": "/static/images/weather/lightning-rainy.png", - partlycloudy: "/static/images/weather/partly-cloudy.png", - pouring: "/static/images/weather/pouring.png", - rainy: "/static/images/weather/rainy.png", - hail: "/static/images/weather/rainy.png", - snowy: "/static/images/weather/snowy.png", - "snowy-rainy": "/static/images/weather/snowy.png", - sunny: "/static/images/weather/sunny.png", - windy: "/static/images/weather/windy.png", - "windy-variant": "/static/images/weather/windy.png", -}; +import type { HomeAssistant, WeatherEntity } from "../types"; + +export const weatherSVGs = new Set([ + "clear-night", + "cloudy", + "fog", + "lightning", + "lightning-rainy", + "partlycloudy", + "pouring", + "rainy", + "hail", + "snowy", + "snowy-rainy", + "sunny", + "windy", + "windy-variant", +]); export const weatherIcons = { exceptional: "hass:alert-circle-outline", }; +const cloudyStates = new Set([ + "partlycloudy", + "cloudy", + "fog", + "windy", + "windy-variant", + "hail", + "rainy", + "snowy", + "snowy-rainy", + "pouring", + "lightning", + "lightning-rainy", +]); + +const rainStates = new Set(["hail", "rainy", "pouring"]); + +const windyStates = new Set(["windy", "windy-variant"]); + +const snowyStates = new Set(["snowy", "snowy-rainy"]); + +const lightningStates = new Set(["lightning", "lightning-rainy"]); + export const cardinalDirections = [ "N", "NNE", @@ -164,3 +190,183 @@ const getWeatherExtrema = ( } `; }; + +export const weatherSVGStyles = css` + .rain { + fill: var(--weather-icon-rain-color, #30b3ff); + } + .sun { + fill: var(--weather-icon-sun-color, #fdd93c); + } + .moon { + fill: var(--weather-icon-moon-color, #fdf9cc); + } + .cloud-back { + fill: var(--weather-icon-cloud-back-color, #d4d4d4); + } + .cloud-front { + fill: var(--weather-icon-cloud-front-color, #f9f9f9); + } +`; + +export const getWeatherStateSVG = (state: string): SVGTemplateResult => { + return svg` + + ${ + state === "sunny" + ? svg` + + ` + : "" + } + ${ + state === "clear-night" + ? svg` + + ` + : "" + } + ${ + state === "partlycloudy" + ? svg` + + ` + : "" + } + ${ + cloudyStates.has(state) + ? svg` + + + ` + : "" + } + ${ + rainStates.has(state) + ? svg` + + + + + ` + : "" + } + ${ + state === "pouring" + ? svg` + + + ` + : "" + } + ${ + windyStates.has(state) + ? svg` + + + ` + : "" + } + ${ + snowyStates.has(state) + ? svg` + + + + ` + : "" + } + ${ + lightningStates.has(state) + ? svg` + + ` + : "" + } + `; +}; + +export const getWeatherStateIcon = ( + state: string, + element: HTMLElement +): TemplateResult | undefined => { + const userDefinedIcon = getComputedStyle(element).getPropertyValue( + `--weather-icon-${state}` + ); + + if (userDefinedIcon) { + return html` +
+ `; + } + + if (weatherSVGs.has(state)) { + return html`${getWeatherStateSVG(state)}`; + } + + if (state in weatherIcons) { + return html` + + `; + } + + return undefined; +}; diff --git a/src/data/zha.ts b/src/data/zha.ts index af8f79be8d..41c4f0805c 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -3,6 +3,7 @@ import { HomeAssistant } from "../types"; export interface ZHAEntityReference extends HassEntity { name: string; + original_name?: string; } export interface ZHADevice { @@ -26,6 +27,12 @@ export interface ZHADevice { signature: any; } +export interface ZHADeviceEndpoint { + device: ZHADevice; + endpoint_id: number; + entities: ZHAEntityReference[]; +} + export interface Attribute { name: string; id: number; @@ -56,7 +63,12 @@ export interface ReadAttributeServiceData { export interface ZHAGroup { name: string; group_id: number; - members: ZHADevice[]; + members: ZHADeviceEndpoint[]; +} + +export interface ZHAGroupMember { + ieee: string; + endpoint_id: string; } export const reconfigureNode = ( @@ -213,7 +225,7 @@ export const fetchGroup = ( export const fetchGroupableDevices = ( hass: HomeAssistant -): Promise => +): Promise => hass.callWS({ type: "zha/devices/groupable", }); @@ -221,7 +233,7 @@ export const fetchGroupableDevices = ( export const addMembersToGroup = ( hass: HomeAssistant, groupId: number, - membersToAdd: string[] + membersToAdd: ZHAGroupMember[] ): Promise => hass.callWS({ type: "zha/group/members/add", @@ -232,7 +244,7 @@ export const addMembersToGroup = ( export const removeMembersFromGroup = ( hass: HomeAssistant, groupId: number, - membersToRemove: string[] + membersToRemove: ZHAGroupMember[] ): Promise => hass.callWS({ type: "zha/group/members/remove", @@ -243,7 +255,7 @@ export const removeMembersFromGroup = ( export const addGroup = ( hass: HomeAssistant, groupName: string, - membersToAdd?: string[] + membersToAdd?: ZHAGroupMember[] ): Promise => hass.callWS({ type: "zha/group/add", diff --git a/src/entrypoints/core.ts b/src/entrypoints/core.ts index 9e738b8042..a80af0d428 100644 --- a/src/entrypoints/core.ts +++ b/src/entrypoints/core.ts @@ -104,6 +104,11 @@ window.hassConnection.then(({ conn }) => { }); window.addEventListener("error", (e) => { + if (!__DEV__ && e.message === "ResizeObserver loop limit exceeded") { + e.stopImmediatePropagation(); + e.stopPropagation(); + return; + } const homeAssistant = document.querySelector("home-assistant") as any; if ( homeAssistant && diff --git a/src/layouts/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts index f5a4d1be4b..454898f824 100644 --- a/src/layouts/hass-tabs-subpage.ts +++ b/src/layouts/hass-tabs-subpage.ts @@ -74,7 +74,7 @@ class HassTabsSubpage extends LitElement { html` { - const translationsPromisses: Promise[] = []; + const translationsPromisses: Promise[] = []; flowsInProgress.forEach((flow) => { // To render title placeholders if (flow.context.title_placeholders) { diff --git a/src/panels/config/zha/zha-add-group-page.ts b/src/panels/config/zha/zha-add-group-page.ts index c52e6da84a..eb85e899ab 100644 --- a/src/panels/config/zha/zha-add-group-page.ts +++ b/src/panels/config/zha/zha-add-group-page.ts @@ -18,31 +18,31 @@ import type { SelectionChangedEvent } from "../../../components/data-table/ha-da import { addGroup, fetchGroupableDevices, - ZHADevice, ZHAGroup, + ZHADeviceEndpoint, } from "../../../data/zha"; import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-subpage"; import type { PolymerChangedEvent } from "../../../polymer-types"; import type { HomeAssistant } from "../../../types"; import "../ha-config-section"; -import "./zha-devices-data-table"; -import type { ZHADevicesDataTable } from "./zha-devices-data-table"; +import "./zha-device-endpoint-data-table"; +import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table"; @customElement("zha-add-group-page") export class ZHAAddGroupPage extends LitElement { - @property() public hass!: HomeAssistant; + @property({ type: Object }) public hass!: HomeAssistant; - @property() public narrow!: boolean; + @property({ type: Boolean }) public narrow!: boolean; - @property() public devices: ZHADevice[] = []; + @property({ type: Array }) public deviceEndpoints: ZHADeviceEndpoint[] = []; @property() private _processingAdd = false; @property() private _groupName = ""; - @query("zha-devices-data-table") - private _zhaDevicesDataTable!: ZHADevicesDataTable; + @query("zha-device-endpoint-data-table") + private _zhaDevicesDataTable!: ZHADeviceEndpointDataTable; private _firstUpdatedCalled = false; @@ -87,14 +87,14 @@ export class ZHAAddGroupPage extends LitElement { ${this.hass.localize("ui.panel.config.zha.groups.add_members")} - - +
{ this._processingAdd = true; - const group: ZHAGroup = await addGroup( - this.hass, - this._groupName, - this._selectedDevicesToAdd - ); + const members = this._selectedDevicesToAdd.map((member) => { + const memberParts = member.split("_"); + return { ieee: memberParts[0], endpoint_id: memberParts[1] }; + }); + const group: ZHAGroup = await addGroup(this.hass, this._groupName, members); this._selectedDevicesToAdd = []; this._processingAdd = false; this._groupName = ""; diff --git a/src/panels/config/zha/zha-config-dashboard.ts b/src/panels/config/zha/zha-config-dashboard.ts index 4acf5faa91..88422f56c8 100644 --- a/src/panels/config/zha/zha-config-dashboard.ts +++ b/src/panels/config/zha/zha-config-dashboard.ts @@ -16,6 +16,7 @@ import "../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer, RowClickedEvent, + DataTableRowData, } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-card"; import "../../../components/ha-icon-next"; @@ -27,19 +28,19 @@ import type { HomeAssistant, Route } from "../../../types"; import "../ha-config-section"; import { formatAsPaddedHex, sortZHADevices } from "./functions"; -export interface DeviceRowData extends ZHADevice { +export interface DeviceRowData extends DataTableRowData { device?: DeviceRowData; } @customElement("zha-config-dashboard") class ZHAConfigDashboard extends LitElement { - @property() public hass!: HomeAssistant; + @property({ type: Object }) public hass!: HomeAssistant; - @property() public route!: Route; + @property({ type: Object }) public route!: Route; - @property() public narrow!: boolean; + @property({ type: Boolean }) public narrow!: boolean; - @property() public isWide!: boolean; + @property({ type: Boolean }) public isWide!: boolean; @property() private _devices: ZHADevice[] = []; @@ -91,7 +92,7 @@ class ZHAConfigDashboard extends LitElement { title: "IEEE", sortable: true, filterable: true, - width: "25%", + width: "30%", }, } ); diff --git a/src/panels/config/zha/zha-device-endpoint-data-table.ts b/src/panels/config/zha/zha-device-endpoint-data-table.ts new file mode 100644 index 0000000000..e758bb79e0 --- /dev/null +++ b/src/panels/config/zha/zha-device-endpoint-data-table.ts @@ -0,0 +1,182 @@ +import { + customElement, + html, + LitElement, + property, + query, + TemplateResult, + css, + CSSResult, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import "../../../components/data-table/ha-data-table"; +import type { + DataTableColumnContainer, + HaDataTable, + DataTableRowData, +} from "../../../components/data-table/ha-data-table"; +import "../../../components/entity/ha-state-icon"; +import type { ZHADeviceEndpoint, ZHAEntityReference } from "../../../data/zha"; +import { showZHADeviceInfoDialog } from "../../../dialogs/zha-device-info-dialog/show-dialog-zha-device-info"; +import type { HomeAssistant } from "../../../types"; + +export interface DeviceEndpointRowData extends DataTableRowData { + id: string; + name: string; + model: string; + manufacturer: string; + endpoint_id: number; + entities: ZHAEntityReference[]; +} + +@customElement("zha-device-endpoint-data-table") +export class ZHADeviceEndpointDataTable extends LitElement { + @property({ type: Object }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow = false; + + @property({ type: Boolean }) public selectable = false; + + @property({ type: Array }) public deviceEndpoints: ZHADeviceEndpoint[] = []; + + @query("ha-data-table") private _dataTable!: HaDataTable; + + private _deviceEndpoints = memoizeOne( + (deviceEndpoints: ZHADeviceEndpoint[]) => { + const outputDevices: DeviceEndpointRowData[] = []; + + deviceEndpoints.forEach((deviceEndpoint) => { + outputDevices.push({ + name: + deviceEndpoint.device.user_given_name || deviceEndpoint.device.name, + model: deviceEndpoint.device.model, + manufacturer: deviceEndpoint.device.manufacturer, + id: deviceEndpoint.device.ieee + "_" + deviceEndpoint.endpoint_id, + ieee: deviceEndpoint.device.ieee, + endpoint_id: deviceEndpoint.endpoint_id, + entities: deviceEndpoint.entities, + }); + }); + + return outputDevices; + } + ); + + private _columns = memoizeOne( + (narrow: boolean): DataTableColumnContainer => + narrow + ? { + name: { + title: "Devices", + sortable: true, + filterable: true, + direction: "asc", + grows: true, + template: (name) => html` +
+ ${name} +
+ `, + }, + endpoint_id: { + title: "Endpoint", + sortable: true, + filterable: true, + }, + } + : { + name: { + title: "Name", + sortable: true, + filterable: true, + direction: "asc", + grows: true, + template: (name) => html` +
+ ${name} +
+ `, + }, + endpoint_id: { + title: "Endpoint", + sortable: true, + filterable: true, + }, + entities: { + title: "Associated Entities", + sortable: false, + filterable: false, + width: "50%", + template: (entities) => html` + ${entities.length + ? entities.length > 3 + ? html`${entities.slice(0, 2).map( + (entity) => + html`
+ ${entity.name || entity.original_name} +
` + )} +
And ${entities.length - 2} more...
` + : entities.map( + (entity) => + html`
+ ${entity.name || entity.original_name} +
` + ) + : "This endpoint has no associated entities"} + `, + }, + } + ); + + public clearSelection() { + this._dataTable.clearSelection(); + } + + protected render(): TemplateResult { + return html` + + `; + } + + private async _handleClicked(ev: CustomEvent) { + const rowId = ((ev.target as HTMLElement).closest( + ".mdc-data-table__row" + ) as any).rowId; + const ieee = rowId.substring(0, rowId.indexOf("_")); + showZHADeviceInfoDialog(this, { ieee }); + } + + static get styles(): CSSResult[] { + return [ + css` + .table-cell-text { + word-break: break-word; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-device-endpoint-data-table": ZHADeviceEndpointDataTable; + } +} diff --git a/src/panels/config/zha/zha-devices-data-table.ts b/src/panels/config/zha/zha-devices-data-table.ts deleted file mode 100644 index 3808868633..0000000000 --- a/src/panels/config/zha/zha-devices-data-table.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - customElement, - html, - LitElement, - property, - query, - TemplateResult, -} from "lit-element"; -import memoizeOne from "memoize-one"; -import "../../../components/data-table/ha-data-table"; -import type { - DataTableColumnContainer, - HaDataTable, -} from "../../../components/data-table/ha-data-table"; -import "../../../components/entity/ha-state-icon"; -import type { ZHADevice } from "../../../data/zha"; -import { showZHADeviceInfoDialog } from "../../../dialogs/zha-device-info-dialog/show-dialog-zha-device-info"; -import type { HomeAssistant } from "../../../types"; - -export interface DeviceRowData extends ZHADevice { - device?: DeviceRowData; -} - -@customElement("zha-devices-data-table") -export class ZHADevicesDataTable extends LitElement { - @property() public hass!: HomeAssistant; - - @property() public narrow = false; - - @property({ type: Boolean }) public selectable = false; - - @property() public devices: ZHADevice[] = []; - - @query("ha-data-table") private _dataTable!: HaDataTable; - - private _devices = memoizeOne((devices: ZHADevice[]) => { - let outputDevices: DeviceRowData[] = devices; - - outputDevices = outputDevices.map((device) => { - return { - ...device, - name: device.user_given_name || device.name, - model: device.model, - manufacturer: device.manufacturer, - id: device.ieee, - }; - }); - - return outputDevices; - }); - - private _columns = memoizeOne( - (narrow: boolean): DataTableColumnContainer => - narrow - ? { - name: { - title: "Devices", - sortable: true, - filterable: true, - direction: "asc", - grows: true, - template: (name) => html` -
- ${name} -
- `, - }, - } - : { - name: { - title: "Name", - sortable: true, - filterable: true, - direction: "asc", - grows: true, - template: (name) => html` -
- ${name} -
- `, - }, - manufacturer: { - title: "Manufacturer", - sortable: true, - filterable: true, - width: "20%", - }, - model: { - title: "Model", - sortable: true, - filterable: true, - width: "20%", - }, - } - ); - - public clearSelection() { - this._dataTable.clearSelection(); - } - - protected render(): TemplateResult { - return html` - - `; - } - - private async _handleClicked(ev: CustomEvent) { - const ieee = ((ev.target as HTMLElement).closest( - ".mdc-data-table__row" - ) as any).rowId; - showZHADeviceInfoDialog(this, { ieee }); - } -} - -declare global { - interface HTMLElementTagNameMap { - "zha-devices-data-table": ZHADevicesDataTable; - } -} diff --git a/src/panels/config/zha/zha-group-page.ts b/src/panels/config/zha/zha-group-page.ts index cdb0ee2dc0..461df7d9fd 100644 --- a/src/panels/config/zha/zha-group-page.ts +++ b/src/panels/config/zha/zha-group-page.ts @@ -9,8 +9,8 @@ import { LitElement, property, PropertyValues, + query, } from "lit-element"; -import memoizeOne from "memoize-one"; import { HASSDomEvent } from "../../../common/dom/fire_event"; import { navigate } from "../../../common/navigate"; import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; @@ -20,8 +20,8 @@ import { fetchGroupableDevices, removeGroups, removeMembersFromGroup, - ZHADevice, ZHAGroup, + ZHADeviceEndpoint, } from "../../../data/zha"; import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-subpage"; @@ -29,37 +29,40 @@ import { HomeAssistant } from "../../../types"; import "../ha-config-section"; import { formatAsPaddedHex } from "./functions"; import "./zha-device-card"; -import "./zha-devices-data-table"; +import "./zha-device-endpoint-data-table"; +import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table"; @customElement("zha-group-page") export class ZHAGroupPage extends LitElement { - @property() public hass!: HomeAssistant; + @property({ type: Object }) public hass!: HomeAssistant; - @property() public group?: ZHAGroup; + @property({ type: Object }) public group?: ZHAGroup; - @property() public groupId!: number; + @property({ type: Number }) public groupId!: number; - @property() public narrow!: boolean; + @property({ type: Boolean }) public narrow!: boolean; - @property() public isWide!: boolean; + @property({ type: Boolean }) public isWide!: boolean; - @property() public devices: ZHADevice[] = []; + @property({ type: Array }) public deviceEndpoints: ZHADeviceEndpoint[] = []; @property() private _processingAdd = false; @property() private _processingRemove = false; - @property() private _filteredDevices: ZHADevice[] = []; + @property() private _filteredDeviceEndpoints: ZHADeviceEndpoint[] = []; @property() private _selectedDevicesToAdd: string[] = []; @property() private _selectedDevicesToRemove: string[] = []; - private _firstUpdatedCalled = false; + @query("#addMembers") + private _zhaAddMembersDataTable!: ZHADeviceEndpointDataTable; - private _members = memoizeOne( - (group: ZHAGroup): ZHADevice[] => group.members - ); + @query("#removeMembers") + private _zhaRemoveMembersDataTable!: ZHADeviceEndpointDataTable; + + private _firstUpdatedCalled = false; public connectedCallback(): void { super.connectedCallback(); @@ -74,8 +77,8 @@ export class ZHAGroupPage extends LitElement { this._processingRemove = false; this._selectedDevicesToRemove = []; this._selectedDevicesToAdd = []; - this.devices = []; - this._filteredDevices = []; + this.deviceEndpoints = []; + this._filteredDeviceEndpoints = []; } protected firstUpdated(changedProperties: PropertyValues): void { @@ -97,8 +100,6 @@ export class ZHAGroupPage extends LitElement { `; } - const members = this._members(this.group); - return html` - ${members.length - ? members.map( + ${this.group.members.length + ? this.group.members.map( (member) => html` `} - ${members.length + ${this.group.members.length ? html`
${this.hass.localize( @@ -148,14 +149,15 @@ export class ZHAGroupPage extends LitElement { )}
- - +
- - +
{ - return !this.group!.members.some((member) => member.ieee === device.ieee); - }); + this._filteredDeviceEndpoints = this.deviceEndpoints.filter( + (deviceEndpoint) => { + return !this.group!.members.some( + (member) => + member.device.ieee === deviceEndpoint.device.ieee && + member.endpoint_id === deviceEndpoint.endpoint_id + ); + } + ); } private _handleAddSelectionChanged( @@ -244,25 +253,27 @@ export class ZHAGroupPage extends LitElement { private async _addMembersToGroup(): Promise { this._processingAdd = true; - this.group = await addMembersToGroup( - this.hass, - this.groupId, - this._selectedDevicesToAdd - ); + const members = this._selectedDevicesToAdd.map((member) => { + const memberParts = member.split("_"); + return { ieee: memberParts[0], endpoint_id: memberParts[1] }; + }); + this.group = await addMembersToGroup(this.hass, this.groupId, members); this._filterDevices(); this._selectedDevicesToAdd = []; + this._zhaAddMembersDataTable.clearSelection(); this._processingAdd = false; } private async _removeMembersFromGroup(): Promise { this._processingRemove = true; - this.group = await removeMembersFromGroup( - this.hass, - this.groupId, - this._selectedDevicesToRemove - ); + const members = this._selectedDevicesToRemove.map((member) => { + const memberParts = member.split("_"); + return { ieee: memberParts[0], endpoint_id: memberParts[1] }; + }); + this.group = await removeMembersFromGroup(this.hass, this.groupId, members); this._filterDevices(); this._selectedDevicesToRemove = []; + this._zhaRemoveMembersDataTable.clearSelection(); this._processingRemove = false; } diff --git a/src/panels/developer-tools/info/integrations-card.ts b/src/panels/developer-tools/info/integrations-card.ts index 1c9c9761ba..6dd26b38ba 100644 --- a/src/panels/developer-tools/info/integrations-card.ts +++ b/src/panels/developer-tools/info/integrations-card.ts @@ -34,7 +34,11 @@ class IntegrationsCard extends LitElement { protected render(): TemplateResult { return html` - + ${this._sortedIntegrations(this.hass!.config.components).map( @@ -62,22 +66,29 @@ class IntegrationsCard extends LitElement { target="_blank" rel="noreferrer" > - Documentation + ${this.hass.localize( + "ui.panel.developer-tools.tabs.info.documentation" + )} - ${!manifest.is_built_in - ? "" - : html` + ${manifest.is_built_in || manifest.issue_tracker + ? html` - `} + ` + : ""} `} `; diff --git a/src/panels/developer-tools/logs/dialog-system-log-detail.ts b/src/panels/developer-tools/logs/dialog-system-log-detail.ts index f799ec0475..7db4d653c2 100644 --- a/src/panels/developer-tools/logs/dialog-system-log-detail.ts +++ b/src/panels/developer-tools/logs/dialog-system-log-detail.ts @@ -84,15 +84,19 @@ class DialogSystemLogDetail extends LitElement { target="_blank" rel="noreferrer" >documentation${!this._manifest.is_built_in - ? "" - : html`, + >${this._manifest.is_built_in || + this._manifest.issue_tracker + ? html`, issues`}) + >` + : ""}) `} ` : ""} diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts index 387ec493ac..64001e5e62 100644 --- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts +++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts @@ -231,7 +231,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { stateLabel === "triggered" || !stateLabel ? "" - : stateLabel; + : this._stateDisplay(state); } private _actionDisplay(state: string): string { diff --git a/src/panels/lovelace/cards/hui-gauge-card.ts b/src/panels/lovelace/cards/hui-gauge-card.ts index c2e24d6d00..9378f4da0f 100644 --- a/src/panels/lovelace/cards/hui-gauge-card.ts +++ b/src/panels/lovelace/cards/hui-gauge-card.ts @@ -10,17 +10,21 @@ import { TemplateResult, } from "lit-element"; import { styleMap } from "lit-html/directives/style-map"; +import "@thomasloven/round-slider"; + import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { isValidEntityId } from "../../../common/entity/valid_entity_id"; import "../../../components/ha-card"; -import { HomeAssistant } from "../../../types"; +import type { HomeAssistant } from "../../../types"; import { findEntities } from "../common/find-entites"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-warning"; -import { LovelaceCard, LovelaceCardEditor } from "../types"; -import { GaugeCardConfig } from "./types"; +import type { LovelaceCard, LovelaceCardEditor } from "../types"; +import type { GaugeCardConfig } from "./types"; +import { debounce } from "../../../common/util/debounce"; +import { installResizeObserver } from "../common/install-resize-observer"; export const severityMap = { red: "var(--label-badge-red)", @@ -63,11 +67,20 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { @property() public hass?: HomeAssistant; - @property() private _baseUnit = "50px"; - @property() private _config?: GaugeCardConfig; - private _updated?: boolean; + private _resizeObserver?: ResizeObserver; + + public connectedCallback(): void { + super.connectedCallback(); + this.updateComplete.then(() => this._attachObserver()); + } + + public disconnectedCallback(): void { + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + } + } public getCardSize(): number { return 2; @@ -83,11 +96,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { this._config = { min: 0, max: 100, ...config }; } - public connectedCallback(): void { - super.connectedCallback(); - this._setBaseUnit(); - } - protected render(): TemplateResult { if (!this._config || !this.hass) { return html``; @@ -121,33 +129,32 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { `; } + const sliderBarColor = this._computeSeverity(state); + return html` -
-
-
-
-
+
-
+
${stateObj.state} ${this._config.unit || stateObj.attributes.unit_of_measurement || ""}
-
+
${this._config.name || computeStateName(stateObj)}
@@ -160,10 +167,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { } protected firstUpdated(): void { - this._updated = true; - this._setBaseUnit(); - // eslint-disable-next-line wc/no-self-class - this.classList.add("init"); + this._attachObserver(); } protected updated(changedProps: PropertyValues): void { @@ -187,16 +191,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { } } - private _setBaseUnit(): void { - if (!this.isConnected || !this._updated) { - return; - } - const baseUnit = this._computeBaseUnit(); - if (baseUnit !== "0px") { - this._baseUnit = baseUnit; - } - } - private _computeSeverity(numberValue: number): string { const sections = this._config!.severity; @@ -229,95 +223,122 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { return severityMap.normal; } - private _translateTurn(value: number): number { - const { min, max } = this._config!; - const maxTurnValue = Math.min(Math.max(value, min!), max!); - return (5 * (maxTurnValue - min!)) / (max! - min!) / 10; - } - - private _computeBaseUnit(): string { - return this.clientWidth < 200 ? this.clientWidth / 5 + "px" : "50px"; - } - private _handleClick(): void { fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); } + private async _attachObserver(): Promise { + await installResizeObserver(); + + this._resizeObserver = new ResizeObserver( + debounce(() => this._measureCard(), 250, false) + ); + + const card = this.shadowRoot!.querySelector("ha-card"); + // If we show an error or warning there is no ha-card + if (!card) { + return; + } + this._resizeObserver.observe(card); + } + + private _measureCard() { + if (this.offsetWidth < 200) { + this.setAttribute("narrow", ""); + } else { + this.removeAttribute("narrow"); + } + if (this.offsetWidth < 150) { + this.setAttribute("veryNarrow", ""); + } else { + this.removeAttribute("veryNarrow"); + } + } + static get styles(): CSSResult { return css` ha-card { cursor: pointer; - padding: 16px 16px 0 16px; height: 100%; + overflow: hidden; + padding: 16px 16px 0 16px; display: flex; + align-items: center; + justify-content: center; flex-direction: column; box-sizing: border-box; - justify-content: center; - align-items: center; } + ha-card:focus { outline: none; background: var(--divider-color); } - .container { - width: calc(var(--base-unit) * 4); - height: calc(var(--base-unit) * 2); - overflow: hidden; - position: relative; - } - .gauge-a { - position: absolute; - background-color: var(--primary-background-color); - width: calc(var(--base-unit) * 4); - height: calc(var(--base-unit) * 2); - top: 0%; - border-radius: calc(var(--base-unit) * 2.5) calc(var(--base-unit) * 2.5) - 0px 0px; - } - .gauge-b { - position: absolute; - background-color: var(--paper-card-background-color); - width: calc(var(--base-unit) * 2.5); - height: calc(var(--base-unit) * 1.25); - top: calc(var(--base-unit) * 0.75); - margin-left: calc(var(--base-unit) * 0.75); - margin-right: auto; - border-radius: calc(var(--base-unit) * 2.5) calc(var(--base-unit) * 2.5) - 0px 0px; - } - .gauge-c { - position: absolute; - background-color: var(--label-badge-blue); - width: calc(var(--base-unit) * 4); - height: calc(var(--base-unit) * 2); - top: calc(var(--base-unit) * 2); - margin-left: auto; - margin-right: auto; - border-radius: 0px 0px calc(var(--base-unit) * 2) - calc(var(--base-unit) * 2); - transform-origin: center top; - } - .init .gauge-c { - transition: all 1.3s ease-in-out; + + round-slider { + max-width: 200px; + --round-slider-path-width: 35px; + --round-slider-path-color: var(--disabled-text-color); + --round-slider-linecap: "butt"; } + .gauge-data { + line-height: 1; text-align: center; - color: var(--primary-text-color); - line-height: calc(var(--base-unit) * 0.3); - width: 100%; position: relative; - top: calc(var(--base-unit) * -0.5); + color: var(--primary-text-color); + margin-top: -28px; + margin-bottom: 14px; } - .init .gauge-data { - transition: all 1s ease-out; + + .gauge-data .percent { + font-size: 28px; } - .gauge-data #percent { - font-size: calc(var(--base-unit) * 0.55); - line-height: calc(var(--base-unit) * 0.55); + + .gauge-data .name { + padding-top: 6px; + font-size: 14px; } - .gauge-data #name { - padding-top: calc(var(--base-unit) * 0.15); - font-size: calc(var(--base-unit) * 0.3); + + /* ============= NARROW ============= */ + + :host([narrow]) round-slider { + --round-slider-path-width: 22px; + } + + :host([narrow]) .gauge-data { + margin-top: -24px; + margin-bottom: 12px; + } + + :host([narrow]) .gauge-data .percent { + font-size: 24px; + } + + :host([narrow]) .gauge-data .name { + font-size: 12px; + } + + /* ============= VERY NARROW ============= */ + + :host([veryNarrow]) round-slider { + --round-slider-path-width: 15px; + } + + :host([veryNarrow]) ha-card { + padding-bottom: 16px; + } + + :host([veryNarrow]) .gauge-data { + margin-top: 0; + margin-bottom: 0; + } + + :host([veryNarrow]) .gauge-data .percent { + font-size: 20px; + } + + :host([veryNarrow]) .gauge-data .name { + font-size: 10px; } `; } diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index f417ed131b..9e66390f5b 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -46,6 +46,7 @@ import "../components/hui-marquee"; import type { LovelaceCard, LovelaceCardEditor } from "../types"; import "../components/hui-warning"; import { MediaControlCardConfig } from "./types"; +import { installResizeObserver } from "../common/install-resize-observer"; function getContrastRatio( rgb1: [number, number, number], @@ -223,7 +224,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { public connectedCallback(): void { super.connectedCallback(); - this.updateComplete.then(() => this._measureCard()); + this.updateComplete.then(() => this._attachObserver()); if (!this.hass || !this._config) { return; @@ -252,6 +253,9 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { clearInterval(this._progressInterval); this._progressInterval = undefined; } + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + } } protected render(): TemplateResult { @@ -624,15 +628,8 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { this._cardHeight = card.offsetHeight; } - private _attachObserver(): void { - if (typeof ResizeObserver !== "function") { - import("resize-observer").then((modules) => { - modules.install(); - this._attachObserver(); - }); - return; - } - + private async _attachObserver(): Promise { + await installResizeObserver(); this._resizeObserver = new ResizeObserver( debounce(() => this._measureCard(), 250, false) ); diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts index 2d9adf3356..3362162792 100644 --- a/src/panels/lovelace/cards/hui-weather-forecast-card.ts +++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts @@ -21,16 +21,17 @@ import { UNAVAILABLE } from "../../../data/entity"; import { getSecondaryWeatherAttribute, getWeatherUnit, - weatherIcons, - weatherImages, + getWeatherStateIcon, + weatherSVGStyles, } from "../../../data/weather"; -import { HomeAssistant, WeatherEntity } from "../../../types"; +import type { HomeAssistant, WeatherEntity } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; import { findEntities } from "../common/find-entites"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-warning"; -import { LovelaceCard, LovelaceCardEditor } from "../types"; -import { WeatherForecastCardConfig } from "./types"; +import type { LovelaceCard, LovelaceCardEditor } from "../types"; +import type { WeatherForecastCardConfig } from "./types"; +import { installResizeObserver } from "../common/install-resize-observer"; const DAY_IN_MILLISECONDS = 86400000; @@ -72,7 +73,13 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { public connectedCallback(): void { super.connectedCallback(); - this.updateComplete.then(() => this._measureCard()); + this.updateComplete.then(() => this._attachObserver()); + } + + public disconnectedCallback(): void { + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + } } public getCardSize(): number { @@ -158,6 +165,8 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { hourly = timeDiff < DAY_IN_MILLISECONDS; } + const weatherStateIcon = getWeatherStateIcon(stateObj.state, this); + return html`
- ${stateObj.state in weatherImages - ? html` - - ` - : html` - - `} + ${weatherStateIcon || + html` + + `}
-
- ${this._config.name || computeStateName(stateObj)} -
${computeStateDisplay( this.hass.localize, @@ -192,6 +192,9 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { this.hass.language )}
+
+ ${this._config.name || computeStateName(stateObj)} +
@@ -200,7 +203,20 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { >
- ${getSecondaryWeatherAttribute(this.hass, stateObj)} + ${this._config.secondary_info_attribute !== undefined + ? html` + ${this.hass!.localize( + `ui.card.weather.attributes.${this._config.secondary_info_attribute}` + )} + ${stateObj.attributes[ + this._config.secondary_info_attribute + ]} + ${getWeatherUnit( + this.hass, + this._config.secondary_info_attribute + )} + ` + : getSecondaryWeatherAttribute(this.hass, stateObj)}
@@ -231,21 +247,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { ${item.condition !== undefined && item.condition !== null ? html`
- ${item.condition in weatherImages - ? html` - - ` - : item.condition in weatherIcons - ? html` - - ` - : ""} + ${getWeatherStateIcon(item.condition, this)}
` : ""} @@ -286,15 +288,8 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); } - private _attachObserver(): void { - if (typeof ResizeObserver !== "function") { - import("resize-observer").then((modules) => { - modules.install(); - this._attachObserver(); - }); - return; - } - + private async _attachObserver(): Promise { + await installResizeObserver(); this._resizeObserver = new ResizeObserver( debounce(() => this._measureCard(), 250, false) ); @@ -321,201 +316,205 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { } } - static get styles(): CSSResult { - return css` - :host { - display: block; - } + static get styles(): CSSResult[] { + return [ + weatherSVGStyles, + css` + :host { + display: block; + } - ha-card { - cursor: pointer; - padding: 16px; - } + ha-card { + cursor: pointer; + padding: 16px; + } - .content { - display: flex; - flex-wrap: nowrap; - justify-content: space-between; - align-items: center; - } + .content { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + } - .icon-image { - display: flex; - align-items: center; - min-width: 64px; - margin-right: 16px; - } + .icon-image { + display: flex; + align-items: center; + min-width: 64px; + margin-right: 16px; + } - .weather-image, - .weather-icon { - flex: 0 0 64px; - } + .icon-image > * { + flex: 0 0 64px; + height: 64px; + } - .weather-icon { - --mdc-icon-size: 64px; - } + .weather-icon { + --mdc-icon-size: 64px; + } - .info { - display: flex; - justify-content: space-between; - flex-grow: 1; - overflow: hidden; - } + .info { + display: flex; + justify-content: space-between; + flex-grow: 1; + overflow: hidden; + } - .temp-attribute { - text-align: right; - } + .temp-attribute { + text-align: right; + } - .temp-attribute .temp { - position: relative; - margin-right: 24px; - } + .temp-attribute .temp { + position: relative; + margin-right: 24px; + } - .temp-attribute .temp span { - position: absolute; - font-size: 24px; - top: 1px; - } + .temp-attribute .temp span { + position: absolute; + font-size: 24px; + top: 1px; + } - .name, - .temp-attribute .temp { - font-size: 28px; - line-height: 1.2; - } + .state, + .temp-attribute .temp { + font-size: 28px; + line-height: 1.2; + } - .state, - .attribute { - font-size: 14px; - line-height: 1; - } + .name, + .attribute { + font-size: 14px; + line-height: 1; + } - .name-state { - overflow: hidden; - padding-right: 12px; - width: 100%; - } + .name-state { + overflow: hidden; + padding-right: 12px; + width: 100%; + } - .name, - .state { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } + .name, + .state { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } - .attribute { - white-space: nowrap; - } + .attribute { + white-space: nowrap; + } - .forecast { - display: flex; - justify-content: space-around; - padding-top: 16px; - } + .forecast { + display: flex; + justify-content: space-around; + padding-top: 16px; + } - .forecast > div { - text-align: center; - } + .forecast > div { + text-align: center; + } - .forecast .icon, - .forecast .temp { - margin: 4px 0; - } + .forecast .icon, + .forecast .temp { + margin: 4px 0; + } - .forecast .temp { - font-size: 16px; - } + .forecast .temp { + font-size: 16px; + } - .forecast-image-icon { - padding-top: 4px; - padding-bottom: 4px; - } + .forecast-image-icon { + padding-top: 4px; + padding-bottom: 4px; + display: flex; + } - .forecast-image { - width: 40px; - } + .forecast-image-icon > * { + width: 40px; + } - .forecast-icon { - --mdc-icon-size: 40px; - } + .forecast-icon { + --mdc-icon-size: 40px; + } - .attribute, - .templow, - .state { - color: var(--secondary-text-color); - } + .attribute, + .templow, + .name { + color: var(--secondary-text-color); + } - .unavailable { - height: 100px; - display: flex; - justify-content: center; - align-items: center; - font-size: 16px; - padding: 10px 20px; - text-align: center; - } + .unavailable { + height: 100px; + display: flex; + justify-content: center; + align-items: center; + font-size: 16px; + padding: 10px 20px; + text-align: center; + } - /* ============= NARROW ============= */ + /* ============= NARROW ============= */ - :host([narrow]) .icon-image { - min-width: 52px; - } + :host([narrow]) .icon-image { + min-width: 52px; + } - :host([narrow]) .weather-image { - flex: 0 0 52px; - width: 52px; - } + :host([narrow]) .weather-image { + flex: 0 0 52px; + width: 52px; + } - :host([narrow]) .weather-icon { - --mdc-icon-size: 52px; - } + :host([narrow]) .weather-icon { + --mdc-icon-size: 52px; + } - :host([narrow]) .name, - :host([narrow]) .temp-attribute .temp { - font-size: 22px; - } + :host([narrow]) .state, + :host([narrow]) .temp-attribute .temp { + font-size: 22px; + } - :host([narrow]) .temp-attribute .temp { - margin-right: 16px; - } + :host([narrow]) .temp-attribute .temp { + margin-right: 16px; + } - :host([narrow]) .temp span { - top: 1px; - font-size: 16px; - } + :host([narrow]) .temp span { + top: 1px; + font-size: 16px; + } - /* ============= VERY NARROW ============= */ + /* ============= VERY NARROW ============= */ - :host([veryNarrow]) .state, - :host([veryNarrow]) .attribute { - display: none; - } + :host([veryNarrow]) .name, + :host([veryNarrow]) .attribute { + display: none; + } - :host([veryNarrow]) .info { - flex-direction: column; - align-items: flex-start; - } + :host([veryNarrow]) .info { + flex-direction: column; + align-items: flex-start; + } - :host([veryNarrow]) .name-state { - padding-right: 0; - } + :host([veryNarrow]) .name-state { + padding-right: 0; + } - /* ============= VERY VERY NARROW ============= */ + /* ============= VERY VERY NARROW ============= */ - :host([veryVeryNarrow]) .info { - padding-top: 4px; - align-items: center; - } + :host([veryVeryNarrow]) .info { + padding-top: 4px; + align-items: center; + } - :host([veryVeryNarrow]) .content { - flex-wrap: wrap; - justify-content: center; - flex-direction: column; - } + :host([veryVeryNarrow]) .content { + flex-wrap: wrap; + justify-content: center; + flex-direction: column; + } - :host([veryVeryNarrow]) .icon-image { - margin-right: 0; - } - `; + :host([veryVeryNarrow]) .icon-image { + margin-right: 0; + } + `, + ]; } } diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 5dd1c46ac1..3ce3342309 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -34,7 +34,8 @@ export interface EntitiesCardEntityConfig extends EntityConfig { | "last-changed" | "last-triggered" | "position" - | "tilt-position"; + | "tilt-position" + | "brightness"; action_name?: string; service?: string; service_data?: object; @@ -275,4 +276,5 @@ export interface WeatherForecastCardConfig extends LovelaceCardConfig { entity: string; name?: string; show_forecast?: boolean; + secondary_info_attribute?: string; } diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 80d5230bb6..2e80710fcb 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -465,7 +465,8 @@ export const generateLovelaceConfigFromData = async ( }; export const generateLovelaceConfigFromHass = async ( - hass: HomeAssistant + hass: HomeAssistant, + localize?: LocalizeFunc ): Promise => { // We want to keep the registry subscriptions alive after generating the UI // so that we don't serve up stale data after changing areas. @@ -488,6 +489,6 @@ export const generateLovelaceConfigFromHass = async ( deviceEntries, entityEntries, hass.states, - hass.localize + localize || hass.localize ); }; diff --git a/src/panels/lovelace/common/install-resize-observer.ts b/src/panels/lovelace/common/install-resize-observer.ts new file mode 100644 index 0000000000..f675858d99 --- /dev/null +++ b/src/panels/lovelace/common/install-resize-observer.ts @@ -0,0 +1,6 @@ +export const installResizeObserver = async () => { + if (typeof ResizeObserver !== "function") { + const modules = await import("resize-observer"); + modules.install(); + } +}; diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index dcd770421e..e199c1b141 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -10,25 +10,30 @@ import { LitElement, property, TemplateResult, + queryAssignedNodes, } from "lit-element"; -import { LovelaceCardConfig } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { showMoveCardViewDialog } from "../editor/card-editor/show-move-card-view-dialog"; import { swapCard } from "../editor/config-util"; import { confDeleteCard } from "../editor/delete-card"; -import { Lovelace } from "../types"; +import { Lovelace, LovelaceCard } from "../types"; +import { computeCardSize } from "../common/compute-card-size"; @customElement("hui-card-options") export class HuiCardOptions extends LitElement { - public cardConfig?: LovelaceCardConfig; - @property() public hass?: HomeAssistant; @property() public lovelace?: Lovelace; @property() public path?: [number, number]; + @queryAssignedNodes() private _assignedNodes?: NodeListOf; + + public getCardSize() { + return this._assignedNodes ? computeCardSize(this._assignedNodes[0]) : 1; + } + protected render(): TemplateResult { return html` diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index fe154e0775..a33127ce02 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -122,6 +122,11 @@ class HuiGenericEntityRow extends LitElement { ? `${this.hass.localize("ui.card.cover.tilt_position")}: ${ stateObj.attributes.current_tilt_position }` + : this.config.secondary_info === "brightness" && + stateObj.attributes.brightness + ? html`${Math.round( + (stateObj.attributes.brightness / 255) * 100 + )}%` : "")}
` diff --git a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts index e4f9ab4815..3ec4e4397f 100644 --- a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts @@ -22,6 +22,7 @@ const cardConfigStruct = struct({ name: "string?", theme: "string?", show_forecast: "boolean?", + secondary_info_attribute: "string?", }); const includeDomains = ["weather"]; @@ -54,6 +55,10 @@ export class HuiWeatherForecastCardEditor extends LitElement return this._config!.show_forecast || true; } + get _secondary_info_attribute(): string { + return this._config!.secondary_info_attribute || ""; + } + protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; @@ -93,12 +98,26 @@ export class HuiWeatherForecastCardEditor extends LitElement @value-changed=${this._valueChanged} >
- Show forecast +
+ + ${this.hass.localize( + "ui.panel.lovelace.editor.card.weather.show_forecast" + )} +
`; } diff --git a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts index ed2e230f90..3792aef585 100644 --- a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts @@ -31,6 +31,7 @@ import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import "../components/hui-warning"; import { EntityConfig, LovelaceRow } from "./types"; +import { installResizeObserver } from "../common/install-resize-observer"; @customElement("hui-media-player-entity-row") class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { @@ -209,19 +210,13 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { } private _attachObserver(): void { - if (typeof ResizeObserver !== "function") { - import("resize-observer").then((modules) => { - modules.install(); - this._attachObserver(); - }); - return; - } + installResizeObserver().then(() => { + this._resizeObserver = new ResizeObserver(() => + this._debouncedResizeListener() + ); - this._resizeObserver = new ResizeObserver(() => - this._debouncedResizeListener() - ); - - this._resizeObserver.observe(this); + this._resizeObserver.observe(this); + }); } private _computeControlIcon(stateObj: HassEntity): string { diff --git a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts index 2e04013bb1..d0ff194bc2 100644 --- a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts @@ -8,21 +8,32 @@ import { PropertyValues, TemplateResult, } from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; +import { ifDefined } from "lit-html/directives/if-defined"; + import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import "../../../components/entity/state-badge"; import { UNAVAILABLE_STATES } from "../../../data/entity"; import { getSecondaryWeatherAttribute, getWeatherUnit, - weatherIcons, - weatherImages, + getWeatherStateIcon, + weatherSVGStyles, } from "../../../data/weather"; -import { HomeAssistant, WeatherEntity } from "../../../types"; -import { EntitiesCardEntityConfig } from "../cards/types"; +import type { HomeAssistant, WeatherEntity } from "../../../types"; +import type { EntitiesCardEntityConfig } from "../cards/types"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import "../components/hui-warning"; -import { LovelaceRow } from "./types"; +import type { LovelaceRow } from "./types"; +import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { actionHandler } from "../common/directives/action-handler-directive"; +import { hasAction } from "../common/has-action"; +import { computeStateName } from "../../../common/entity/compute_state_name"; +import { ActionHandlerEvent } from "../../../data/lovelace"; +import { handleAction } from "../common/handle-action"; +import { stateIcon } from "../../../common/entity/state_icon"; @customElement("hui-weather-entity-row") class HuiWeatherEntityRow extends LitElement implements LovelaceRow { @@ -61,48 +72,126 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow { `; } - const weatherRowConfig = { - ...this._config, - icon: weatherIcons[stateObj.state], - image: weatherImages[stateObj.state], - }; + const pointer = + (this._config.tap_action && this._config.tap_action.action !== "none") || + (this._config.entity && + !DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity))); + + const weatherStateIcon = getWeatherStateIcon(stateObj.state, this); return html` - -
-
- ${UNAVAILABLE_STATES.includes(stateObj.state) - ? computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.language - ) - : html` - ${stateObj.attributes.temperature} - ${getWeatherUnit(this.hass, "temperature")} - `} -
-
- ${getSecondaryWeatherAttribute(this.hass!, stateObj)} -
+
+ ${weatherStateIcon || + html` + + `} +
+
+ ${this._config.name || computeStateName(stateObj)} +
+
+
+ ${UNAVAILABLE_STATES.includes(stateObj.state) + ? computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.language + ) + : html` + ${stateObj.attributes.temperature} + ${getWeatherUnit(this.hass, "temperature")} + `}
- +
+ ${getSecondaryWeatherAttribute(this.hass!, stateObj)} +
+
`; } - static get styles(): CSSResult { - return css` - .attributes { - display: flex; - flex-direction: column; - justify-content: center; - text-align: right; - } + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } - .secondary { - color: var(--secondary-text-color); - } - `; + static get styles(): CSSResult[] { + return [ + weatherSVGStyles, + css` + :host { + display: flex; + align-items: center; + flex-direction: row; + } + + .info { + margin-left: 16px; + flex: 1 0 60px; + } + + .info, + .info > * { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .icon-image { + display: flex; + align-items: center; + min-width: 40px; + } + + .icon-image > * { + flex: 0 0 40px; + height: 40px; + } + + .weather-icon { + --iron-icon-width: 40px; + --iron-icon-height: 40px; + } + + :host([rtl]) .flex { + margin-left: 0; + margin-right: 16px; + } + + .pointer { + cursor: pointer; + } + + .attributes { + display: flex; + flex-direction: column; + justify-content: center; + text-align: right; + margin-left: 8px; + } + + .secondary { + color: var(--secondary-text-color); + } + `, + ]; } } diff --git a/src/panels/lovelace/ha-panel-lovelace.ts b/src/panels/lovelace/ha-panel-lovelace.ts index 16a0054c92..0e802519ce 100644 --- a/src/panels/lovelace/ha-panel-lovelace.ts +++ b/src/panels/lovelace/ha-panel-lovelace.ts @@ -288,7 +288,8 @@ class LovelacePanel extends LitElement { this._errorMsg = err.message; return; } - conf = await generateLovelaceConfigFromHass(this.hass!); + const localize = await this.hass!.loadBackendTranslation("title"); + conf = await generateLovelaceConfigFromHass(this.hass!, localize); confMode = "generated"; } finally { // Ignore updates for another 2 seconds. @@ -370,8 +371,9 @@ class LovelacePanel extends LitElement { const { config: previousConfig, mode: previousMode } = this.lovelace!; try { // Optimistic update + const localize = await this.hass!.loadBackendTranslation("title"); this._updateLovelace({ - config: await generateLovelaceConfigFromHass(this.hass!), + config: await generateLovelaceConfigFromHass(this.hass!, localize), mode: "generated", editMode: false, }); diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index ffdc503e52..e801a0c751 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -369,100 +369,6 @@ class HUIRoot extends LitElement { `; } - static get styles(): CSSResult[] { - return [ - haStyle, - css` - :host { - --dark-color: #455a64; - --text-dark-color: #fff; - -ms-user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - } - - ha-app-layout { - min-height: 100%; - } - paper-menu-button { - padding: 0; - } - paper-tabs { - margin-left: 12px; - --paper-tabs-selection-bar-color: var(--text-primary-color, #fff); - text-transform: uppercase; - } - .edit-mode { - background-color: var(--dark-color, #455a64); - color: var(--text-dark-color); - } - .edit-mode div[main-title] { - pointer-events: auto; - } - paper-tab.iron-selected .edit-icon { - display: inline-flex; - } - .edit-icon { - color: var(--accent-color); - padding-left: 8px; - vertical-align: middle; - --mdc-theme-text-disabled-on-light: var(--disabled-text-color); - } - .edit-icon.view { - display: none; - } - #add-view { - position: absolute; - height: 44px; - } - #add-view ha-icon { - background-color: var(--accent-color); - border-radius: 5px; - margin-top: 4px; - } - app-toolbar a { - color: var(--text-primary-color, white); - } - mwc-button.warning:not([disabled]) { - color: var(--google-red-500); - } - #view { - min-height: calc(100vh - 112px); - /** - * Since we only set min-height, if child nodes need percentage - * heights they must use absolute positioning so we need relative - * positioning here. - * - * https://www.w3.org/TR/CSS2/visudet.html#the-height-property - */ - position: relative; - display: flex; - } - #view > * { - /** - * The view could get larger than the window in Firefox - * to prevent that we set the max-width to 100% - * flex-grow: 1 and flex-basis: 100% should make sure the view - * stays full width. - * - * https://github.com/home-assistant/home-assistant-polymer/pull/3806 - */ - flex: 1 1 100%; - max-width: 100%; - } - #view.tabs-hidden { - min-height: calc(100vh - 64px); - } - paper-item { - cursor: pointer; - } - .hide-tab { - display: none; - } - `, - ]; - } - protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties); @@ -527,8 +433,10 @@ class HUIRoot extends LitElement { navigate(this, `${this.route?.prefix}/${views[0]?.path || 0}`); newSelectView = 0; } - // On edit mode change, recreate the current view from scratch - force = true; + } + + if (!force) { + huiView.lovelace = this.lovelace; } } @@ -734,6 +642,100 @@ class HUIRoot extends LitElement { // Recalculate to see if we need to adjust content area for tab bar fireEvent(this, "iron-resize"); } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + :host { + --dark-color: #455a64; + --text-dark-color: #fff; + -ms-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + } + + ha-app-layout { + min-height: 100%; + } + paper-menu-button { + padding: 0; + } + paper-tabs { + margin-left: 12px; + --paper-tabs-selection-bar-color: var(--text-primary-color, #fff); + text-transform: uppercase; + } + .edit-mode { + background-color: var(--dark-color, #455a64); + color: var(--text-dark-color); + } + .edit-mode div[main-title] { + pointer-events: auto; + } + paper-tab.iron-selected .edit-icon { + display: inline-flex; + } + .edit-icon { + color: var(--accent-color); + padding-left: 8px; + vertical-align: middle; + --mdc-theme-text-disabled-on-light: var(--disabled-text-color); + } + .edit-icon.view { + display: none; + } + #add-view { + position: absolute; + height: 44px; + } + #add-view ha-icon { + background-color: var(--accent-color); + border-radius: 5px; + margin-top: 4px; + } + app-toolbar a { + color: var(--text-primary-color, white); + } + mwc-button.warning:not([disabled]) { + color: var(--google-red-500); + } + #view { + min-height: calc(100vh - 112px); + /** + * Since we only set min-height, if child nodes need percentage + * heights they must use absolute positioning so we need relative + * positioning here. + * + * https://www.w3.org/TR/CSS2/visudet.html#the-height-property + */ + position: relative; + display: flex; + } + #view > * { + /** + * The view could get larger than the window in Firefox + * to prevent that we set the max-width to 100% + * flex-grow: 1 and flex-basis: 100% should make sure the view + * stays full width. + * + * https://github.com/home-assistant/home-assistant-polymer/pull/3806 + */ + flex: 1 1 100%; + max-width: 100%; + } + #view.tabs-hidden { + min-height: calc(100vh - 64px); + } + paper-item { + cursor: pointer; + } + .hide-tab { + display: none; + } + `, + ]; + } } declare global { diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index 0799e1c7ea..dd5511eac3 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -34,6 +34,7 @@ export interface LovelaceCard extends HTMLElement { hass?: HomeAssistant; isPanel?: boolean; editMode?: boolean; + index?: number; getCardSize(): number; setConfig(config: LovelaceCardConfig): void; } diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index 3c0c303db5..279741e6f8 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -4,8 +4,9 @@ import { property, PropertyValues, TemplateResult, + CSSResult, + css, } from "lit-element"; -// This one is for types import { classMap } from "lit-html/directives/class-map"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { computeRTL } from "../../../common/util/compute_rtl"; @@ -92,7 +93,6 @@ export class HUIView extends LitElement { protected render(): TemplateResult { return html` - ${this.renderStyles()}
${this.lovelace!.editMode @@ -113,82 +113,6 @@ export class HUIView extends LitElement { `; } - protected renderStyles(): TemplateResult { - return html` - - `; - } - protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties); @@ -201,6 +125,7 @@ export class HUIView extends LitElement { } const hassChanged = changedProperties.has("hass"); + let editModeChanged = false; let configChanged = false; @@ -209,7 +134,7 @@ export class HUIView extends LitElement { } else if (changedProperties.has("lovelace")) { const oldLovelace = changedProperties.get("lovelace") as Lovelace; editModeChanged = - !oldLovelace || lovelace.editMode !== oldLovelace.editMode; + oldLovelace && lovelace.editMode !== oldLovelace.editMode; configChanged = !oldLovelace || lovelace.config !== oldLovelace.config; } @@ -221,9 +146,15 @@ export class HUIView extends LitElement { }); } - if (configChanged || editModeChanged || changedProperties.has("columns")) { + if (configChanged) { this._createCards(lovelace.config.views[this.index!]); - } else if (hassChanged) { + } else if (editModeChanged) { + this._switchEditMode(); + } else if (changedProperties.has("columns")) { + this._recreateColumns(); + } + + if (hassChanged && !configChanged) { this._cards.forEach((element) => { element.hass = this.hass; }); @@ -280,38 +211,34 @@ export class HUIView extends LitElement { root.style.display = elements.length > 0 ? "block" : "none"; } - private _createCards(config: LovelaceViewConfig): void { + private _switchEditMode() { + if (this.lovelace!.editMode) { + const wrappedCards = this._cards.map((element) => { + const wrapper = document.createElement("hui-card-options"); + wrapper.hass = this.hass; + wrapper.lovelace = this.lovelace; + wrapper.path = [this.index!, (element as LovelaceCard).index!]; + (element as LovelaceCard).editMode = true; + wrapper.appendChild(element); + return wrapper; + }); + this._createColumns(wrappedCards); + } else { + this._createColumns(this._cards); + } + } + + private _recreateColumns() { + this._createColumns(this._cards); + } + + private _createColumns(elements: HTMLElement[]) { const root = this.shadowRoot!.getElementById("columns")!; while (root.lastChild) { root.removeChild(root.lastChild); } - if (!config || !config.cards || !Array.isArray(config.cards)) { - this._cards = []; - return; - } - - const elements: LovelaceCard[] = []; - const elementsToAppend: HTMLElement[] = []; - config.cards.forEach((cardConfig, cardIndex) => { - const element = this.createCardElement(cardConfig); - elements.push(element); - - if (!this.lovelace!.editMode) { - elementsToAppend.push(element); - return; - } - - const wrapper = document.createElement("hui-card-options"); - wrapper.hass = this.hass; - wrapper.lovelace = this.lovelace; - wrapper.path = [this.index!, cardIndex]; - element.editMode = true; - wrapper.appendChild(element); - elementsToAppend.push(wrapper); - }); - let columns: HTMLElement[][] = []; const columnEntityCount: number[] = []; for (let i = 0; i < this.columns!; i++) { @@ -319,12 +246,11 @@ export class HUIView extends LitElement { columnEntityCount.push(0); } - elements.forEach((el, index) => { - const cardSize = computeCardSize(el); - // Element to append might be the wrapped card when we're editing. - columns[getColumnIndex(columnEntityCount, cardSize)].push( - elementsToAppend[index] + elements.forEach((el) => { + const cardSize = computeCardSize( + (el.tagName === "HUI-CARD-OPTIONS" ? el.firstChild : el) as LovelaceCard ); + columns[getColumnIndex(columnEntityCount, cardSize)].push(el); }); // Remove empty columns @@ -336,8 +262,28 @@ export class HUIView extends LitElement { column.forEach((el) => columnEl.appendChild(el)); root.appendChild(columnEl); }); + } + + private _createCards(config: LovelaceViewConfig): void { + if (!config || !config.cards || !Array.isArray(config.cards)) { + this._cards = []; + return; + } + + const elements: LovelaceCard[] = []; + config.cards.forEach((cardConfig, index) => { + const element = this.createCardElement(cardConfig); + element.index = index; + elements.push(element); + }); this._cards = elements; + + if (this.lovelace!.editMode) { + this._switchEditMode(); + } else { + this._createColumns(this._cards); + } } private _rebuildCard( @@ -361,6 +307,77 @@ export class HUIView extends LitElement { curBadgeEl === badgeElToReplace ? newBadgeEl : curBadgeEl ); } + + static get styles(): CSSResult { + return css` + :host { + display: block; + box-sizing: border-box; + padding: 4px 4px 0; + transform: translateZ(0); + position: relative; + color: var(--primary-text-color); + background: var(--lovelace-background, var(--primary-background-color)); + } + + #badges { + margin: 8px 16px; + font-size: 85%; + text-align: center; + } + + #columns { + display: flex; + flex-direction: row; + justify-content: center; + } + + .column { + flex: 1 0 0; + max-width: 500px; + min-width: 0; + /* on iOS devices the column can become wider when toggling a switch */ + overflow-x: hidden; + } + + .column > * { + display: block; + margin: 4px 4px 8px; + } + + mwc-fab { + position: sticky; + float: right; + bottom: 16px; + right: 16px; + z-index: 1; + } + + mwc-fab.rtl { + float: left; + right: auto; + left: 16px; + } + + @media (max-width: 500px) { + :host { + padding-left: 0; + padding-right: 0; + } + + .column > * { + margin-left: 0; + margin-right: 0; + } + } + + @media (max-width: 599px) { + .column { + max-width: 600px; + } + } + `; + } } declare global { diff --git a/src/state/translations-mixin.ts b/src/state/translations-mixin.ts index 345f0d13d6..215752dfa8 100644 --- a/src/state/translations-mixin.ts +++ b/src/state/translations-mixin.ts @@ -1,5 +1,5 @@ import { atLeastVersion } from "../common/config/version"; -import { computeLocalize } from "../common/translations/localize"; +import { computeLocalize, LocalizeFunc } from "../common/translations/localize"; import { computeRTL } from "../common/util/compute_rtl"; import { debounce } from "../common/util/debounce"; import { @@ -104,29 +104,37 @@ export default >(superClass: T) => this._loadFragmentTranslations(hass.language, hass.panelUrl); } + /** + * Load translations from the backend + * @param language language to fetch + * @param category category to fetch + * @param integration optional, if having to fetch for specific integration + * @param configFlow optional, if having to fetch for all integrations with a config flow + * @param force optional, load even if already cached + */ private async _loadHassTranslations( language: string, category: Parameters[2], integration?: Parameters[3], configFlow?: Parameters[4], force = false - ) { + ): Promise { if ( __BACKWARDS_COMPAT__ && !atLeastVersion(this.hass!.connection.haVersion, 0, 109) ) { if (category !== "state") { - return; + return this.hass!.localize; } const resources = await getHassTranslationsPre109(this.hass!, language); // Ignore the repsonse if user switched languages before we got response if (this.hass!.language !== language) { - return; + return this.hass!.localize; } this._updateResources(language, resources); - return; + return this.hass!.localize; } let alreadyLoaded: LoadedTranslationCategory; @@ -145,12 +153,12 @@ export default >(superClass: T) => if (!force) { if (integration) { if (alreadyLoaded.integrations.includes(integration)) { - return; + return this.hass!.localize; } } else if ( configFlow ? alreadyLoaded.configFlow : alreadyLoaded.setup ) { - return; + return this.hass!.localize; } } @@ -176,10 +184,11 @@ export default >(superClass: T) => // Ignore the repsonse if user switched languages before we got response if (this.hass!.language !== language) { - return; + return this.hass!.localize; } this._updateResources(language, resources); + return this.hass!.localize; } private async _loadFragmentTranslations( @@ -214,9 +223,7 @@ export default >(superClass: T) => // multiple fragments. const resources = { [language]: { - ...(this.hass && - this.hass.resources && - this.hass.resources[language]), + ...this.hass?.resources?.[language], ...data, }, }; diff --git a/src/translations/en.json b/src/translations/en.json index 71557cfd2a..18d2068be9 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1841,7 +1841,8 @@ "no_theme": "No theme", "unit": "Unit", "url": "Url", - "state": "State" + "state": "State", + "secondary_info_attribute": "Secondary Info Attribute" }, "map": { "name": "Map", @@ -1902,7 +1903,8 @@ }, "weather-forecast": { "name": "Weather Forecast", - "description": "The Weather Forecast card displays the weather. Very useful to include on interfaces that people display on the wall." + "description": "The Weather Forecast card displays the weather. Very useful to include on interfaces that people display on the wall.", + "show_forecast": "Show Forecast" } }, "view": { @@ -2186,7 +2188,10 @@ "icons_by": "Icons by", "frontend_version": "Frontend version: {version} - {type}", "custom_uis": "Custom UIs:", - "system_health_error": "System Health component is not loaded. Add 'system_health:' to configuration.yaml" + "system_health_error": "System Health component is not loaded. Add 'system_health:' to configuration.yaml", + "integrations": "Integrations", + "documentation": "Documentation", + "issues": "Issues" }, "logs": { "title": "Logs", diff --git a/src/types.ts b/src/types.ts index 95033824a5..2f7493f4b8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -224,7 +224,7 @@ export interface HomeAssistant { category: Parameters[2], integration?: Parameters[3], configFlow?: Parameters[4] - ): Promise; + ): Promise; } export type LightEntity = HassEntityBase & { diff --git a/translations/frontend/da.json b/translations/frontend/da.json index dd7322b11b..2b66efa7a3 100644 --- a/translations/frontend/da.json +++ b/translations/frontend/da.json @@ -473,6 +473,7 @@ } }, "common": { + "and": "og", "cancel": "Annuller", "close": "Luk", "delete": "Slet", @@ -1180,7 +1181,9 @@ "edit_requires_storage": "Editor er deaktiveret, fordi config er gemt i 'configuration.yaml'.", "elevation": "Højde", "elevation_meters": "meter", + "external_url": "Ekstern webadresse", "imperial_example": "Fahrenheit, pund", + "internal_url": "Intern webadresse", "latitude": "Breddegrad", "location_name": "Navn på din Home Assistant-installation", "longitude": "Længdegrad", diff --git a/translations/frontend/de.json b/translations/frontend/de.json index c4c4c364be..4f0b0eee38 100644 --- a/translations/frontend/de.json +++ b/translations/frontend/de.json @@ -473,6 +473,7 @@ } }, "common": { + "and": "und", "cancel": "Abbrechen", "close": "Schließen", "delete": "Löschen", @@ -1180,7 +1181,9 @@ "edit_requires_storage": "Editor deaktiviert, da die Konfiguration in configuration.yaml gespeichert ist.", "elevation": "Höhe", "elevation_meters": "Meter", + "external_url": "Externe Adresse", "imperial_example": "Fahrenheit, Pfund", + "internal_url": "Interne Adresse", "latitude": "Breitengrad", "location_name": "Name deiner Home Assistant Installation", "longitude": "Längengrad", diff --git a/translations/frontend/en.json b/translations/frontend/en.json index 4fd0fb44fc..302b07c87a 100644 --- a/translations/frontend/en.json +++ b/translations/frontend/en.json @@ -1878,10 +1878,13 @@ "built_using": "Built using", "custom_uis": "Custom UIs:", "developed_by": "Developed by a bunch of awesome people.", + "documentation": "Documentation", "frontend": "frontend-ui", "frontend_version": "Frontend version: {version} - {type}", "home_assistant_logo": "Home Assistant logo", "icons_by": "Icons by", + "integrations": "Integrations", + "issues": "Issues", "license": "Published under the Apache 2.0 license", "path_configuration": "Path to configuration.yaml: {path}", "server": "server", @@ -2072,6 +2075,7 @@ "name": "Name", "no_theme": "No theme", "refresh_interval": "Refresh Interval", + "secondary_info_attribute": "Secondary Info Attribute", "show_icon": "Show Icon?", "show_name": "Show Name?", "show_state": "Show State?", @@ -2162,7 +2166,8 @@ }, "weather-forecast": { "description": "The Weather Forecast card displays the weather. Very useful to include on interfaces that people display on the wall.", - "name": "Weather Forecast" + "name": "Weather Forecast", + "show_forecast": "Show Forecast" } }, "cardpicker": { diff --git a/translations/frontend/es.json b/translations/frontend/es.json index 7690ae77b7..908f22051d 100644 --- a/translations/frontend/es.json +++ b/translations/frontend/es.json @@ -473,6 +473,7 @@ } }, "common": { + "and": "y", "cancel": "Cancelar", "close": "Cerrar", "delete": "Eliminar", @@ -1180,7 +1181,9 @@ "edit_requires_storage": "Editor deshabilitado debido a la configuración almacenada en configuration.yaml.", "elevation": "Altitud", "elevation_meters": "metros", + "external_url": "URL externa", "imperial_example": "Fahrenheit, libras", + "internal_url": "URL interna", "latitude": "Latitud", "location_name": "Nombre de tu instalación de Home Assistant", "longitude": "Longitud", diff --git a/translations/frontend/fi.json b/translations/frontend/fi.json index 959f0eb4f3..4ca8c78362 100644 --- a/translations/frontend/fi.json +++ b/translations/frontend/fi.json @@ -473,6 +473,7 @@ } }, "common": { + "and": "ja", "cancel": "Peruuta", "close": "Sulje", "delete": "Poista", @@ -1180,7 +1181,9 @@ "edit_requires_storage": "Editori on poistettu käytöstä, koska asetuksia on annettu configuration.yaml:ssa.", "elevation": "Korkeus merenpinnasta", "elevation_meters": "metriä", + "external_url": "Ulkoinen URL-osoite", "imperial_example": "Fahrenheit, paunaa", + "internal_url": "Sisäinen URL-osoite", "latitude": "Leveysaste", "location_name": "Home Assistant -järjestelmäsi nimi", "longitude": "Pituusaste", @@ -1247,6 +1250,7 @@ }, "delete": "Poista", "description": "Hallitse yhdistettyjä laitteita.", + "device_info": "Laitteen tiedot", "device_not_found": "Laitetta ei löydy.", "entities": { "add_entities_lovelace": "Lisää Lovelace näkymään", @@ -1602,7 +1606,7 @@ "core": "Lataa ydin uudelleen", "group": "Lataa ryhmät uudelleen", "heading": "Asetusten uudelleenlataus", - "introduction": "Jotkut kotiassistentin osat voidaan ladata uudelleen ilman, että tarvitaan uudelleenkäynnistystä. Painamalla uudelleenlatausta uudet asetukset luetaan yaml tiedostosta.", + "introduction": "Jotkut Home Assistantin osat voidaan ladata uudelleen ilman, että tarvitaan uudelleenkäynnistystä. Painamalla uudelleenlatausta yaml-tiedosto luetaan uudelleen.", "person": "Lataa henkilöt uudelleen", "scene": "Lataa tilanteet uudelleen", "script": "Lataa skriptit uudelleen", @@ -1612,7 +1616,7 @@ "confirm_restart": "Haluatko varmasti käynnistää Home Assistantin uudelleen?", "confirm_stop": "Haluatko varmasti pysäyttää Home Assistantin?", "heading": "Palvelimen hallinta", - "introduction": "Hallitse Home Assistantia... suoraan Home Assistantista", + "introduction": "Hallitse Home Assistantia suoraan käyttöliittymästä.", "restart": "Käynnistä uudelleen", "stop": "Pysäytä" }, diff --git a/translations/frontend/hu.json b/translations/frontend/hu.json index 0f3e72c456..b665183ee2 100644 --- a/translations/frontend/hu.json +++ b/translations/frontend/hu.json @@ -473,6 +473,7 @@ } }, "common": { + "and": "És", "cancel": "Mégse", "close": "Bezárás", "delete": "Törlés", @@ -1180,7 +1181,9 @@ "edit_requires_storage": "A szerkesztő le van tiltva, mert a konfiguráció a configuration.yaml fájlban van tárolva.", "elevation": "Magasság", "elevation_meters": "méter", + "external_url": "Externe URL", "imperial_example": "Fahrenheit, font", + "internal_url": "Érvénytelen URL", "latitude": "Szélesség", "location_name": "A Home Assistant rendszered neve", "longitude": "Hosszúság", @@ -1247,6 +1250,7 @@ }, "delete": "Törlés", "description": "Csatlakoztatott eszközök kezelése", + "device_info": "Eszköz információ", "device_not_found": "Eszköz nem található.", "entities": { "add_entities_lovelace": "Hozzáadás a Lovelace-hez", diff --git a/translations/frontend/it.json b/translations/frontend/it.json index 3a5cd3ab16..a566fb6fde 100644 --- a/translations/frontend/it.json +++ b/translations/frontend/it.json @@ -473,6 +473,7 @@ } }, "common": { + "and": "e", "cancel": "Annulla", "close": "Chiudi", "delete": "Elimina", @@ -1180,7 +1181,9 @@ "edit_requires_storage": "Editor disabilitato perché la configurazione è memorizzata in configuration.yaml.", "elevation": "Altitudine", "elevation_meters": "metri", + "external_url": "URL esterno", "imperial_example": "Fahrenheit, libbre", + "internal_url": "URL interno", "latitude": "Latitudine", "location_name": "Nome della tua installazione di Home Assistant", "longitude": "Longitudine", @@ -1247,6 +1250,7 @@ }, "delete": "Elimina", "description": "Gestisci i dispositivi collegati", + "device_info": "Informazioni sul dispositivo", "device_not_found": "Dispositivo non trovato.", "entities": { "add_entities_lovelace": "Aggiungi a Lovelace", diff --git a/translations/frontend/lb.json b/translations/frontend/lb.json index b819186ab7..3cf3a82567 100644 --- a/translations/frontend/lb.json +++ b/translations/frontend/lb.json @@ -1602,7 +1602,7 @@ "automation": "Automatisme nei lueden", "core": "Standuert and Personnalisatioun néi lueden", "group": "Gruppe nei lueden", - "heading": "YAML Konfiguratioun gëtt frësch gelueden", + "heading": "YAML Konfiguratioun frësch lueden", "introduction": "E puer Deeler vum Home Assistant kënne frësch geluede ginn ouni datt een Neistart néideg ass. Klick op nei luede fir di aktuell Konfiguratioun z'entlueden an di nei Konfiguratioun ze lueden.", "person": "Persoune frësch lueden", "scene": "Szeene néi lueden", diff --git a/translations/frontend/nb.json b/translations/frontend/nb.json index 62cb160a92..6d74a61c2d 100644 --- a/translations/frontend/nb.json +++ b/translations/frontend/nb.json @@ -473,6 +473,7 @@ } }, "common": { + "and": "og", "cancel": "Avbryt", "close": "Lukk", "delete": "Slett", @@ -1180,7 +1181,9 @@ "edit_requires_storage": "Redigering deaktivert da konfigurasjonen er lagret i configuration.yaml.", "elevation": "Høyde", "elevation_meters": "meter", + "external_url": "Ekstern URL", "imperial_example": "Fahrenheit, pund", + "internal_url": "Intern URL", "latitude": "Breddegrad", "location_name": "Navn på installasjonen av Home Assistant", "longitude": "Lengdegrad", diff --git a/translations/frontend/nl.json b/translations/frontend/nl.json index 40f27f533a..f9f250905f 100644 --- a/translations/frontend/nl.json +++ b/translations/frontend/nl.json @@ -473,11 +473,15 @@ } }, "common": { + "and": "en", "cancel": "Annuleren", "close": "Sluiten", "delete": "Verwijderen", "loading": "Bezig met laden", + "next": "Volgende", "no": "Nee", + "previous": "Vorige", + "refresh": "Vernieuwen", "save": "Opslaan", "successfully_deleted": "Succesvol verwijderd", "successfully_saved": "Succesvol opgeslagen", @@ -737,6 +741,10 @@ "triggered": "Geactiveerd {name}" }, "panel": { + "calendar": { + "my_calendars": "Mijn agenda's", + "today": "Vandaag" + }, "config": { "advanced_mode": { "hint_enable": "Ontbrekende configuratie-opties? Schakel geavanceerde modus in", @@ -839,6 +847,9 @@ }, "label": "Apparaat" }, + "not": { + "label": "Niet" + }, "numeric_state": { "above": "Boven", "below": "Onder", @@ -1170,7 +1181,9 @@ "edit_requires_storage": "Editor uitgeschakeld omdat de configuratie is opgeslagen in configuration.yaml", "elevation": "Hoogte", "elevation_meters": "meter", + "external_url": "Externe URL", "imperial_example": "Fahrenheit, ponden", + "internal_url": "Interne URL", "latitude": "Breedtegraad", "location_name": "Naam van Home Assistant installatie", "longitude": "Lengtegraad", @@ -1237,6 +1250,7 @@ }, "delete": "Verwijderen", "description": "Beheer verbonden apparaten", + "device_info": "Apparaat info", "device_not_found": "Apparaat niet gevonden.", "entities": { "add_entities_lovelace": "Voeg toe aan de Lovelace gebruikersinterface", diff --git a/translations/frontend/pl.json b/translations/frontend/pl.json index d58d4c8918..0a92cc0b82 100644 --- a/translations/frontend/pl.json +++ b/translations/frontend/pl.json @@ -473,6 +473,7 @@ } }, "common": { + "and": "i", "cancel": "Anuluj", "close": "Zamknij", "delete": "Usuń", @@ -1180,7 +1181,9 @@ "edit_requires_storage": "Edytor wyłączony, ponieważ konfiguracja jest przechowywana w pliku configuration.yaml.", "elevation": "Wysokość", "elevation_meters": "metry/-ów", + "external_url": "Publiczny adres URL", "imperial_example": "stopnie Fahrenheita, funty", + "internal_url": "Lokalny adres URL", "latitude": "Szerokość geograficzna", "location_name": "Nazwa instalacji Home Assistant", "longitude": "Długość geograficzna", diff --git a/translations/frontend/pt.json b/translations/frontend/pt.json index 261d8d6ba8..518daefd1e 100644 --- a/translations/frontend/pt.json +++ b/translations/frontend/pt.json @@ -331,8 +331,7 @@ "ui": { "auth_store": { "ask": "Deseja continuar com sessão iniciada?", - "confirm": "Guardar login", - "decline": "Não, obrigado" + "decline": "Não" }, "card": { "alarm_control_panel": { @@ -473,6 +472,7 @@ } }, "common": { + "and": "e", "cancel": "Cancelar", "close": "Fechar", "delete": "Apagar", @@ -761,8 +761,8 @@ "editor": { "create": "Criar", "default_name": "Nova área", - "delete": "APAGAR", - "update": "ATUALIZAR" + "delete": "Apagar", + "update": "Atualizar" }, "picker": { "create_area": "Criar área", @@ -1178,7 +1178,9 @@ "edit_requires_storage": "Editor desativado por causa da configuração existente em configuration.yaml.", "elevation": "Elevação", "elevation_meters": "metros", + "external_url": "URL externo", "imperial_example": "Fahrenheit, libras", + "internal_url": "URL interno", "latitude": "Latitude", "location_name": "Nome da instalação do seu Home Assistant", "longitude": "Longitude", @@ -1204,7 +1206,7 @@ "description": "Personalizar as suas entidades", "pick_attribute": "Escolha um atributo para substituir.", "picker": { - "header": "Personalização", + "header": "Personalizações", "introduction": "Ajustar atributos por entidade. Personalizações acrescentadas/editadas terão efeitos imediatos. Remoção de personalizações terão efeito quando a entidade for atualizada." }, "warning": { @@ -1244,6 +1246,7 @@ }, "delete": "Apagar", "description": "Gerir dispositivos ligados", + "device_info": "Informação do dispositivo", "device_not_found": "Dispositivo não encontrado.", "entities": { "add_entities_lovelace": "Adicionar ao Lovelace", @@ -1368,6 +1371,7 @@ "aborted": "Abortado", "close": "Fechar", "created_config": "Configuração criada para {name}.", + "dismiss": "Descartar diálogo", "error_saving_area": "Erro ao salvar a área: {error}", "external_step": { "description": "Para ser concluída, esta etapa exige que visite um sítio web externo.", @@ -1592,8 +1596,8 @@ "automation": "Recarregar automações", "core": "Recarregar localização e personalizações", "group": "Recarregar grupos", - "heading": "A recarregar configuração", - "introduction": "Algumas partes do Home Assistant podem ser recarregadas sem necessidade de reiniciar. Ao carregar em Recarregar configuração irá descarregar a configuração atual e carregar a nova.", + "heading": "A recarregar a configuração YAML", + "introduction": "Algumas partes do Home Assistant podem ser recarregadas sem a necessidade de reiniciar. Ao carregar em Recarregar a configuração irá descartar a configuração atual e carregar a nova.", "person": "Recarregar pessoas", "scene": "Recarregar cenas", "script": "Recarregar scripts", @@ -1661,7 +1665,8 @@ "spinner": "À procura de dispositivos ZHA Zigbee..." }, "add": { - "caption": "Adicionar Dispositivos" + "caption": "Adicionar Dispositivos", + "description": "Adicionar dispositivos à rede Zigbee" }, "caption": "ZHA", "cluster_attributes": { @@ -1709,8 +1714,10 @@ "caption": "Grupos", "create": "Criar grupo", "create_group": "Zigbee Home Automation - Criar grupo", + "create_group_details": "Digite os detalhes necessários para criar um novo grupo zigbee", "creating_group": "Criação de grupo", "description": "Criar e modificar grupos Zigbee", + "group_details": "Aqui estão todos os detalhes do grupo Zigbee selecionado.", "group_id": "ID do grupo", "group_info": "Informações sobre o grupo", "group_name_placeholder": "Nome do Grupo", @@ -1718,6 +1725,7 @@ "group-header": "Zigbee Home Automation - Detalhes do grupo", "groups": "Grupos", "groups-header": "Zigbee Home Automation - Gestão de Grupos", + "header": "Zigbee Home Automation - Gestão de Grupos", "introduction": "Criar e modificar grupos zigbee", "manage_groups": "Gerir Grupos Zigbee", "members": "Membros", @@ -1728,6 +1736,7 @@ "zha_zigbee_groups": "Grupos ZHA Zigbee" }, "header": "Configurar a automação residencial Zigbee", + "introduction": "Aqui é possível configurar o componente ZHA. Ainda não é possível configurar tudo a partir do IU, mas estamos a trabalhar nisso.", "network_management": { "header": "Gestão ", "introduction": "Comandos que afetam toda a rede" @@ -1796,7 +1805,7 @@ "config_parameter": "Parâmetro de configuração", "config_value": "Valor de Configuração", "false": "Falso", - "header": "Configurar opçoes do nó", + "header": "Opções de configuração de nó", "seconds": "Segundos", "set_config_parameter": "Definir o Parâmetro de Configuração", "set_wakeup": "Definir intervalo de acordar", @@ -1973,7 +1982,7 @@ } }, "changed_toast": { - "message": "A configuração do Lovelace foi atualizada, gostaria de atualizar?", + "message": "A configuração do Lovelace foi atualizada para este painel, gostaria de atualizar?", "refresh": "Atualizar" }, "editor": { @@ -2147,7 +2156,7 @@ "header": "Configuração do cartão", "move": "Mover para Vista", "options": "Mais opções", - "pick_card": "Escolha o cartão que deseja adicionar.", + "pick_card": "Que cartão gostaria de adicionar?", "pick_card_view_title": "Que cartão você gostaria de adicionar à sua vista {name}?", "show_code_editor": "Mostrar Editor de Código", "show_visual_editor": "Mostrar Editor Visual", @@ -2185,13 +2194,14 @@ "para_no_id": "Este elemento não possui um ID. Por favor adicione um ID a este elemento em 'ui-lovelace.yaml'." }, "raw_editor": { + "confirm_remove_config_text": "Iremos gerar automaticamente as suas vistas do Lovelace UI com as suas áreas e dispositivos se você remover a sua configuração do Lovelace UI.", "confirm_unsaved_changes": "Existem alterações não guardadas. De certeza de que quer sair?", "confirm_unsaved_comments": "A sua configuração contém comentário(s), eles não serão salvos. Deseja continuar?", "error_invalid_config": "A sua configuração não é válida: {error}", "error_parse_yaml": "Não foi possível analisar o YAML: {error}", "error_remove": "Não foi possível remover a configuração: {error}", "error_save_yaml": "Não é possível salvar o YAML: {error}", - "header": "Editar configuração", + "header": "Editar Configuração", "resources_moved": "Os recursos não devem mais ser adicionados ao editor de configuração do código fonte do Lovelace, mas podem ser adicionados no painel de configuração do Lovelace.", "save": "Guardar", "saved": "Guardada", @@ -2229,7 +2239,7 @@ "refresh": "Atualizar", "reload_resources": "Recarregar recursos" }, - "reload_lovelace": "Recarregar Lovelace", + "reload_lovelace": "Recarregar UI", "reload_resources": { "refresh_body": "É preciso atualizar a página para concluir o carregamento. Deseja atualizar agora?", "refresh_header": "Deseja atualizar?" @@ -2392,6 +2402,7 @@ "mirror": "Espelho", "patio": "Pátio", "right": "Direita", + "temperature_study": "Estudo da Temperatura", "upstairs": "Andar de cima" }, "unit": { diff --git a/translations/frontend/ro.json b/translations/frontend/ro.json index a0d6aa9139..f570ef5f49 100644 --- a/translations/frontend/ro.json +++ b/translations/frontend/ro.json @@ -330,8 +330,8 @@ }, "ui": { "auth_store": { - "ask": "Doriți să salvați aceste date de conectare?", - "confirm": "Salvați datele de conectare", + "ask": "Vrei să rămâi autentificat?", + "confirm": "Da", "decline": "Nu" }, "card": { @@ -459,17 +459,20 @@ } }, "common": { + "and": "Și", "cancel": "Revocare", "close": "Închide", "delete": "Șterge", "loading": "Se încarcă", "next": "Următorul", + "no": "Nu", "previous": "Anterior", "refresh": "Reîmprospătare", "save": "Salvați", "successfully_deleted": "Șters cu succes", "successfully_saved": "Opțiunile salvate cu succes.", - "undo": "Undo" + "undo": "Undo", + "yes": "Da" }, "components": { "area-picker": { @@ -536,12 +539,15 @@ "title": "Opțiuni de sistem pentru {integration}", "update": "Actualizare" }, + "domain_toggler": { + "title": "Comutare domenii" + }, "entity_registry": { "control": "Control", "dismiss": "Ascunde", "editor": { "confirm_delete": "Sigur dorești să ștergi această intrare?", - "delete": "ȘTERGE", + "delete": "Șterge", "enabled_cause": "Dezactivat de către {cause}.", "enabled_description": "Entitățile dezactivate nu vof fi adăugate in Home Assistant", "enabled_label": "Activează entitatea", @@ -550,7 +556,7 @@ "name": "Suprascriere nume", "note": "Notă: este posibil să nu funcționeze încă cu toate integrările.", "unavailable": "Această entitate nu este disponibilă momentan.", - "update": "ACTUALIZARE" + "update": "Actualizare" }, "no_unique_id": "Această entitate nu are un ID unic, prin urmare, setările sale nu pot fi gestionate de la interfața cu utilizatorul.", "related": "În legătură cu", @@ -574,6 +580,7 @@ "time": "Timp" }, "input_number": { + "box": "Introdu textul", "max": "Valoare maximă", "min": "Valoare minimă", "mode": "Mod de afișare", @@ -592,9 +599,12 @@ "min": "Lungime minimă", "mode": "Mod de afișare", "password": "Parola", + "pattern": "Modelul Regex pentru validarea de partea clientului", "text": "Text" }, - "required_error_msg": "Acest câmp este obligatoriu" + "platform_not_loaded": "Integrarea {platform} nu este încărcată. Vă rugăm să-i adăugați configurația dvs. fie adăugând 'default_config:' sau '{platform}:'.", + "required_error_msg": "Acest câmp este obligatoriu", + "yaml_not_editable": "Setările acestei entități nu se pot edita din interfața grafica. Numai entitățile configurate in interfața grafica sunt configurabile din interfața grafica." }, "more_info_control": { "dismiss": "Se respinge dialogul", @@ -653,6 +663,10 @@ } }, "voice_command": { + "did_not_hear": "Home Assistant nu a auzit nimic", + "error": "Oops, a apărut o eroare", + "found": "Am găsit următoarele pentru tine:", + "how_can_i_help": "Cu ce vă pot ajuta?", "label": "Scrieți o întrebare și apăsați „Enter”", "label_voice": "Tastați și apăsați „Enter” sau atingeți microfonul pentru a vorbi" }, @@ -723,11 +737,12 @@ }, "description": "Privire de ansamblu asupra tuturor zonelor din casa ta.", "editor": { - "create": "CREAȚI", - "delete": "ȘTERGEȚI", - "update": "ACTUALIZAȚI" + "create": "Creează", + "delete": "Șterge", + "update": "Actualizare" }, "picker": { + "create_area": "Creare zonă", "header": "Zone", "integrations_page": "Pagina de integrari", "introduction": "Zonele sunt folosite pentru a organiza unde sunt dispozitivele. Aceste informații vor fi utilizate de către Home Assistant pentru a vă ajuta să vă organizați interfața, permisiunile și integrarea cu alte sisteme.", @@ -851,7 +866,7 @@ "triggers": { "add": "Adăugați acțiune", "delete": "Ștergeți", - "delete_confirm": "Sigur doriți să ștergeți?", + "delete_confirm": "Sigur dorești să ștergi această intrare?", "duplicate": "Dublura", "header": "Declanșatoare", "introduction": "Declanșatoarele sunt cele ce încep procesarea unei reguli de automatizare. Este posibil să specificați mai multe declanșatoare pentru aceeași regulă. Odată ce începe declanșarea, Home Assistant o să valideze condițiile, dacă este cazul, și va apela acțiunea. \n\n [Aflați mai multe despre declanșatoare.] (https://home-assistant.io/docs/automation/trigger/)", @@ -994,14 +1009,22 @@ } }, "customize": { + "attributes_customize": "Următoarele atribute sunt deja setate în customize.yaml", "attributes_not_set": "Următoarele atribute nu au fost setate. Setați-le dacă doriți.", + "attributes_outside": "Următoarele atribute sunt personalizate din afara customize.yaml", "attributes_override": "Poți să le suprascrii dacă vrei.", + "attributes_set": "Următoarele atribute ale entității sunt setate programatic.", "caption": "Personalizare", "description": "Personalizați-vă entitățile", + "different_include": "Posibil printr-un domeniu, un glob sau alta includere", "pick_attribute": "Alegeți un atribut pentru suprascriere", "picker": { "header": "Personalizări", "introduction": "Atributele per entitate. Particularizările adăugate/editate vor avea efect imediat. Particularizările eliminate vor avea efect atunci când entitatea este actualizată." + }, + "warning": { + "include_link": "include customize.yaml", + "include_sentence": "Se pare că configurația dvs..yaml nu corespunde" } }, "devices": { @@ -1172,7 +1195,7 @@ "ignore": { "confirm_delete_ignore": "Acest lucru va face ca integrarea să apară din nou în integrările descoperite atunci când este descoperită. Acest lucru ar putea necesita o repornire sau să dureze ceva timp.", "confirm_delete_ignore_title": "Încetați să ignorați {name} ?", - "confirm_ignore": "Sunteți sigur că nu doriți să configurați această integrare? Puteți anula acest lucru făcând clic pe „Afișați integrări ignorate” din meniul de preaplin din partea dreaptă sus.", + "confirm_ignore": "Sunteți sigur că nu doriți să configurați această integrare? Puteți anula acest lucru făcând clic pe „Afișați integrări ignorate” din meniul overflow din partea dreaptă sus.", "confirm_ignore_title": "Ignorați descoperirea {name} ?", "hide_ignored": "Ascundeți integrările ignorate", "ignore": "Ignora", @@ -1194,6 +1217,7 @@ "lovelace": { "caption": "Panouri de bord Lovelace", "dashboards": { + "cant_edit_default": "Tabloul de bord standard Lovelace nu poate fi editat din UI. Îl puteți ascunde setând un alt tablou de bord ca implicit.", "cant_edit_yaml": "Tablourile de bord definite în YAML nu pot fi editate din UI. Schimbați-le în configuration.yaml.", "caption": "Tablouri de bord", "conf_mode": { @@ -1207,13 +1231,13 @@ "delete": "Șterge", "dismiss": "Închide", "edit_dashboard": "Editează tabloul de bord", - "icon": "Pictograma barei laterale", + "icon": "Pictograma", "new_dashboard": "Adăugați un nou tablou de bord", "remove_default": "Eliminare ca implicit pe acest dispozitiv", "require_admin": "Doar administrator", "set_default": "Setare ca implicită pe acest dispozitiv", "show_sidebar": "Afișați în bara laterală", - "title": "Titlul barei laterale", + "title": "Titlu", "title_required": "Titlul este necesar.", "update": "Actualizare", "url": "Url", @@ -1229,7 +1253,7 @@ "sidebar": "Afișați în bara laterală", "title": "Titlu" }, - "open": "Deschide tabloul de bord" + "open": "Deschide" } }, "resources": { @@ -1313,6 +1337,7 @@ }, "learn_more": "Aflați mai multe despre scene", "no_scenes": "Nu am putut găsi nici o scenă editabilă", + "only_editable": "Numai scenele definite în scene.yaml sunt editabile.", "pick_scene": "Alege scena pentru a edita", "show_info_scene": "Afișează informații despre scenă" } @@ -1547,7 +1572,13 @@ "developer-tools": { "tabs": { "events": { + "alert_event_type": "Tipul de eveniment este un câmp obligatoriu", "available_events": "Evenimente disponibile", + "fire_event": "Declansare eveniment", + "listening_to": "Ascultand", + "start_listening": "Incepe sa asculti", + "stop_listening": "Nu mai asculta", + "subscribe_to": "Eveniment la care să vă abonați", "title": "Evenimente" }, "info": { @@ -1703,6 +1734,9 @@ "history-graph": { "description": "Fișa History Graph vă permite să afișați un grafic pentru fiecare dintre entitățile listate." }, + "iframe": { + "name": "Pagină web" + }, "light": { "description": "Cardul Light vă permite să schimbați luminozitatea luminii." }, @@ -1835,6 +1869,7 @@ "title": "Entități neutilizate" }, "views": { + "confirm_delete": "Ștergeți vizualizarea?", "confirm_delete_existing_cards": "Ștergerea acestei vizualizări va elimina și cardurile", "confirm_delete_text": "Sigur doriți să ștergeți vizualizarea „{name}”?" }, diff --git a/translations/frontend/sv.json b/translations/frontend/sv.json index e623659206..48ae93c4eb 100644 --- a/translations/frontend/sv.json +++ b/translations/frontend/sv.json @@ -473,11 +473,15 @@ } }, "common": { + "and": "och", "cancel": "Avbryt", "close": "Stäng", "delete": "Radera", "loading": "Läser in", + "next": "Nästa", "no": "Nej", + "previous": "Föregående", + "refresh": "Uppdatera", "save": "Spara", "successfully_deleted": "Har raderats", "successfully_saved": "Inställningar sparades", @@ -736,6 +740,10 @@ "triggered": "Utlöst {name}" }, "panel": { + "calendar": { + "my_calendars": "Mina Kalendrar", + "today": "Idag" + }, "config": { "advanced_mode": { "hint_enable": "Saknas konfigurationsalternativ? Aktivera avancerat läge på", @@ -838,6 +846,9 @@ }, "label": "Enhet" }, + "not": { + "label": "Inte" + }, "numeric_state": { "above": "Över", "below": "Under", @@ -1169,7 +1180,9 @@ "edit_requires_storage": "Redigeraren är inaktiverad eftersom konfigurationen lagras i configuration.yaml.", "elevation": "Höjd över havet", "elevation_meters": "meter", + "external_url": "Extern URL", "imperial_example": "Fahrenheit, pounds", + "internal_url": "Intern URL", "latitude": "Latitud", "location_name": "Namn på din Home Assistant-installation", "longitude": "Longitud", @@ -1236,6 +1249,7 @@ }, "delete": "Ta bort", "description": "Hantera anslutna enheter", + "device_info": "Enhetsinformation", "device_not_found": "Enheten hittades inte.", "entities": { "add_entities_lovelace": "Lägg till i Lovelace", @@ -2095,21 +2109,27 @@ "name": "Markdown" }, "media-control": { + "description": "Mediakontrolkortet används för att visa mediaspelarenheter på ett gränssnitt med lättanvända kontroller.", "name": "Mediaspelare" }, "picture-elements": { + "description": "Bildlementkortet är en av de mest mångsidiga typerna av kort. Korten låter dig placera ikoner eller text och till och med tjänster! På en bild baserad på koordinater.", "name": "Bildelement" }, "picture-entity": { + "description": "Bildentitetskortet visar en enhet i form av en bild. I stället för bilder från URL kan den också visa bilden av kameraenheter.", "name": "Bildentitet" }, "picture-glance": { + "description": "Picture Glance-kortet visar en bild och motsvarande enhetstillstånd som en ikon. Enheterna på höger sida tillåter att växla åtgärder, andra visar dialogrutan för mer information.", "name": "Bildblick" }, "picture": { + "description": "Bildkortet låter dig ställa in en bild som ska användas för att navigera till olika banor i ditt gränssnitt eller för att ringa en tjänst.", "name": "Bild" }, "plant-status": { + "description": "Plant Status-kortet är för alla de härliga botanikerna där ute.", "name": "Växtstatus" }, "sensor": { @@ -2125,6 +2145,7 @@ "name": "Termostat" }, "vertical-stack": { + "description": "Med Vertical Stack-kortet kan du gruppera flera kort så att de alltid sitter i samma kolumn.", "name": "Vertikal trave" }, "weather-forecast": { diff --git a/translations/frontend/zh-Hant.json b/translations/frontend/zh-Hant.json index b294c2d257..f1d15ae5a3 100644 --- a/translations/frontend/zh-Hant.json +++ b/translations/frontend/zh-Hant.json @@ -473,6 +473,7 @@ } }, "common": { + "and": "及", "cancel": "取消", "close": "關閉", "delete": "刪除", @@ -1180,7 +1181,9 @@ "edit_requires_storage": "由於 configuration.yaml 內已儲存設定,編輯功能已關閉。", "elevation": "海拔", "elevation_meters": "公尺", + "external_url": "外部 URL", "imperial_example": "華氏、磅", + "internal_url": "內部 URL", "latitude": "緯度", "location_name": "Home Assistant 安裝名稱", "longitude": "經度", diff --git a/yarn.lock b/yarn.lock index 69e66b6ce9..c33003fb3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2613,10 +2613,10 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@thomasloven/round-slider@0.3.7": - version "0.3.7" - resolved "https://registry.yarnpkg.com/@thomasloven/round-slider/-/round-slider-0.3.7.tgz#3f8f16f90296e1062d932f5ea8ebf244aa7e58f6" - integrity sha512-rIdEvyLt4YNahpAp1Ibk7qOn9mdgP3Qo2gORyojHqaBTV+t29N1zlTo/G0SbKTLDUtSGDslQWD3/nAdD3yBOYA== +"@thomasloven/round-slider@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@thomasloven/round-slider/-/round-slider-0.4.1.tgz#42ddd28abb25c378dce35c4c0ccdd72ea63b3b25" + integrity sha512-Z6jrXG5vowKQkOwdsyGDLi8ZT9lUfcYjFsaQe8djhDE8+x41GYp5lkJ4uCwT787A8WcODbtQfYtuxPOlZcizTw== dependencies: lit-element "^2.2.1"
- Issues + ${this.hass.localize( + "ui.panel.developer-tools.tabs.info.issues" + )}