Merge pull request #5840 from home-assistant/dev

This commit is contained in:
Bram Kragten 2020-05-12 22:24:40 +02:00 committed by GitHub
commit de1ffe67b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1489 additions and 895 deletions

View File

@ -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() {

View File

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

View File

@ -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",

View File

@ -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`<slot name="icon"></slot>` : ""}
${!this.narrow || this.active
? html` <span class="name">${this.name}</span> `
? html`<span class="name">${this.name}</span>`
: ""}
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
</div>
@ -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);

View File

@ -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) =>

View File

@ -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<string>([
"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<string>([
"partlycloudy",
"cloudy",
"fog",
"windy",
"windy-variant",
"hail",
"rainy",
"snowy",
"snowy-rainy",
"pouring",
"lightning",
"lightning-rainy",
]);
const rainStates = new Set<string>(["hail", "rainy", "pouring"]);
const windyStates = new Set<string>(["windy", "windy-variant"]);
const snowyStates = new Set<string>(["snowy", "snowy-rainy"]);
const lightningStates = new Set<string>(["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`
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 17 17"
>
${
state === "sunny"
? svg`
<path
class="sun"
d="m 14.39303,8.4033507 c 0,3.3114723 -2.684145,5.9956173 -5.9956169,5.9956173 -3.3114716,0 -5.9956168,-2.684145 -5.9956168,-5.9956173 0,-3.311471 2.6841452,-5.995617 5.9956168,-5.995617 3.3114719,0 5.9956169,2.684146 5.9956169,5.995617"
/>
`
: ""
}
${
state === "clear-night"
? svg`
<path
class="moon"
d="m 13.502891,11.382935 c -1.011285,1.859223 -2.976664,3.121381 -5.2405751,3.121381 -3.289929,0 -5.953329,-2.663833 -5.953329,-5.9537625 0,-2.263911 1.261724,-4.228856 3.120948,-5.240575 -0.452782,0.842738 -0.712753,1.806363 -0.712753,2.832381 0,3.289928 2.663833,5.9533275 5.9533291,5.9533275 1.026017,0 1.989641,-0.259969 2.83238,-0.712752"
/>
`
: ""
}
${
state === "partlycloudy"
? svg`
<path
class="sun"
d="m14.981 4.2112c0 1.9244-1.56 3.4844-3.484 3.4844-1.9244 0-3.4844-1.56-3.4844-3.4844s1.56-3.484 3.4844-3.484c1.924 0 3.484 1.5596 3.484 3.484"
/>
`
: ""
}
${
cloudyStates.has(state)
? svg`
<path
class="cloud-back"
d="m3.8863 5.035c-0.54892 0.16898-1.04 0.46637-1.4372 0.8636-0.63077 0.63041-1.0206 1.4933-1.0206 2.455 0 1.9251 1.5589 3.4682 3.4837 3.4682h6.9688c1.9251 0 3.484-1.5981 3.484-3.5232 0-1.9251-1.5589-3.5232-3.484-3.5232h-1.0834c-0.25294-1.6916-1.6986-2.9083-3.4463-2.9083-1.7995 0-3.2805 1.4153-3.465 3.1679"
/>
<path
class="cloud-front"
d="m4.1996 7.6995c-0.33902 0.10407-0.64276 0.28787-0.88794 0.5334-0.39017 0.38982-0.63147 0.92322-0.63147 1.5176 0 1.1896 0.96414 2.1431 2.1537 2.1431h4.3071c1.1896 0 2.153-0.98742 2.153-2.1777 0-1.1896-0.96344-2.1777-2.153-2.1777h-0.66992c-0.15593-1.0449-1.0499-1.7974-2.1297-1.7974-1.112 0-2.0274 0.87524-2.1417 1.9586"
/>
`
: ""
}
${
rainStates.has(state)
? svg`
<path
class="rain"
d="m5.2852 14.734c-0.22401 0.24765-0.57115 0.2988-0.77505 0.11395-0.20391-0.1845-0.18732-0.53481 0.036689-0.78281 0.14817-0.16298 0.59126-0.32914 0.87559-0.42369 0.12453-0.04092 0.22684 0.05186 0.19791 0.17956-0.065617 0.2921-0.18732 0.74965-0.33514 0.91299"
/>
<path
class="rain"
d="m11.257 14.163c-0.22437 0.24765-0.57115 0.2988-0.77505 0.11395-0.2039-0.1845-0.18768-0.53481 0.03669-0.78281 0.14817-0.16298 0.59126-0.32914 0.8756-0.42369 0.12453-0.04092 0.22684 0.05186 0.19791 0.17956-0.06562 0.2921-0.18732 0.74965-0.33514 0.91299"
/>
<path
class="rain"
d="m8.432 15.878c-0.15452 0.17039-0.3937 0.20567-0.53446 0.07867-0.14041-0.12735-0.12876-0.36865 0.025753-0.53975 0.10195-0.11218 0.40711-0.22684 0.60325-0.29175 0.085725-0.02858 0.15628 0.03563 0.13652 0.12382-0.045508 0.20108-0.12912 0.51647-0.23107 0.629"
/>
<path
class="rain"
d="m7.9991 14.118c-0.19226 0.21237-0.49001 0.25612-0.66499 0.09737-0.17462-0.15804-0.16051-0.45861 0.03175-0.67098 0.12665-0.14005 0.50729-0.28293 0.75071-0.36336 0.10689-0.03563 0.19473 0.0441 0.17004 0.15346-0.056092 0.25082-0.16051 0.64347-0.28751 0.78352"
/>
`
: ""
}
${
state === "pouring"
? svg`
<path
class="rain"
d="m10.648 16.448c-0.19226 0.21449-0.49001 0.25894-0.66499 0.09878-0.17498-0.16016-0.16087-0.4639 0.03175-0.67874 0.12665-0.14146 0.50694-0.2854 0.75071-0.36724 0.10689-0.03563 0.19473 0.0448 0.17004 0.15558-0.05645 0.25365-0.16051 0.65017-0.28751 0.79163"
/>
<path
class="rain"
d="m5.9383 16.658c-0.22437 0.25012-0.5715 0.30162-0.77505 0.11501-0.20391-0.18627-0.18768-0.54046 0.036689-0.79093 0.14817-0.1651 0.59126-0.33267 0.87559-0.42827 0.12418-0.04127 0.22648 0.05221 0.19791 0.18168-0.065617 0.29528-0.18732 0.75741-0.33514 0.92251"
/>
`
: ""
}
${
windyStates.has(state)
? svg`
<path
class="cloud-back"
d="m 13.59616,15.30968 c 0,0 -0.09137,-0.0071 -0.250472,-0.0187 -0.158045,-0.01235 -0.381353,-0.02893 -0.64382,-0.05715 -0.262466,-0.02716 -0.564444,-0.06385 -0.877358,-0.124531 -0.156986,-0.03034 -0.315383,-0.06844 -0.473781,-0.111478 -0.157691,-0.04551 -0.313266,-0.09842 -0.463902,-0.161219 l -0.267406,-0.0949 c -0.09984,-0.02646 -0.205669,-0.04904 -0.305153,-0.06738 -0.193322,-0.02716 -0.3838218,-0.03316 -0.5640912,-0.02011 -0.3626556,0.02611 -0.6847417,0.119239 -0.94615,0.226483 -0.2617611,0.108656 -0.4642556,0.230364 -0.600075,0.324203 -0.1358195,0.09419 -0.2049639,0.160514 -0.2049639,0.160514 0,0 0.089958,-0.01623 0.24765,-0.04445 0.1559278,-0.02575 0.3764139,-0.06174 0.6367639,-0.08714 0.2596444,-0.02646 0.5591527,-0.0441 0.8678333,-0.02328 0.076905,0.0035 0.1538111,0.01658 0.2321278,0.02293 0.077611,0.01058 0.1534581,0.02893 0.2314221,0.04022 0.07267,0.01834 0.1397,0.03986 0.213078,0.05644 l 0.238125,0.08925 c 0.09207,0.03281 0.183444,0.07055 0.275872,0.09878 0.09243,0.0261 0.185208,0.05327 0.277636,0.07161 0.184856,0.0388 0.367947,0.06174 0.543983,0.0702 0.353131,0.01905 0.678745,-0.01341 0.951442,-0.06456 0.27305,-0.05292 0.494595,-0.123119 0.646642,-0.181681 0.152047,-0.05785 0.234597,-0.104069 0.234597,-0.104069"
/>
<path
class="cloud-back"
d="m 4.7519154,13.905801 c 0,0 0.091369,-0.0032 0.2511778,-0.0092 0.1580444,-0.0064 0.3820583,-0.01446 0.6455833,-0.03281 0.2631722,-0.01729 0.5662083,-0.04269 0.8812389,-0.09137 0.1576916,-0.02434 0.3175,-0.05609 0.4776611,-0.09384 0.1591027,-0.03951 0.3167944,-0.08643 0.4699,-0.14358 l 0.2702277,-0.08467 c 0.1008945,-0.02222 0.2074334,-0.04127 0.3072695,-0.05574 0.1943805,-0.01976 0.3848805,-0.0187 0.5651499,0.0014 0.3608917,0.03951 0.67945,0.144639 0.936625,0.261761 0.2575278,0.118534 0.4554364,0.247297 0.5873754,0.346781 0.132291,0.09913 0.198966,0.168275 0.198966,0.168275 0,0 -0.08925,-0.01976 -0.245886,-0.05397 C 9.9423347,14.087088 9.7232597,14.042988 9.4639681,14.00736 9.2057347,13.97173 8.9072848,13.94245 8.5978986,13.95162 c -0.077258,7.06e-4 -0.1541638,0.01058 -0.2328333,0.01411 -0.077964,0.0078 -0.1545166,0.02328 -0.2331861,0.03175 -0.073025,0.01588 -0.1404055,0.03422 -0.2141361,0.04798 l -0.2420055,0.08008 c -0.093486,0.02963 -0.1859139,0.06421 -0.2794,0.0889 C 7.3028516,14.23666 7.2093653,14.2603 7.116232,14.27512 6.9303181,14.30722 6.7465209,14.3231 6.5697792,14.32486 6.2166487,14.33046 5.8924459,14.28605 5.6218654,14.224318 5.3505793,14.161565 5.1318571,14.082895 4.9822793,14.01869 4.8327015,13.95519 4.7519154,13.905801 4.7519154,13.905801"
/>
`
: ""
}
${
snowyStates.has(state)
? svg`
<path
class="rain"
d="m 8.4319893,15.348341 c 0,0.257881 -0.209197,0.467079 -0.467078,0.467079 -0.258586,0 -0.46743,-0.209198 -0.46743,-0.467079 0,-0.258233 0.208844,-0.467431 0.46743,-0.467431 0.257881,0 0.467078,0.209198 0.467078,0.467431"
/>
<path
class="rain"
d="m 11.263878,14.358553 c 0,0.364067 -0.295275,0.659694 -0.659695,0.659694 -0.364419,0 -0.6596937,-0.295627 -0.6596937,-0.659694 0,-0.364419 0.2952747,-0.659694 0.6596937,-0.659694 0.36442,0 0.659695,0.295275 0.659695,0.659694"
/>
<path
class="rain"
d="m 5.3252173,13.69847 c 0,0.364419 -0.295275,0.660047 -0.659695,0.660047 -0.364067,0 -0.659694,-0.295628 -0.659694,-0.660047 0,-0.364067 0.295627,-0.659694 0.659694,-0.659694 0.36442,0 0.659695,0.295627 0.659695,0.659694"
/>
`
: ""
}
${
lightningStates.has(state)
? svg`
<path
class="sun"
d="m 9.9252695,10.935875 -1.6483986,2.341014 1.1170184,0.05929 -1.2169864,2.02141 3.0450261,-2.616159 H 9.8864918 L 10.97937,11.294651 10.700323,10.79794 h -0.508706 l -0.2663475,0.137936"
/>
`
: ""
}
</svg>`;
};
export const getWeatherStateIcon = (
state: string,
element: HTMLElement
): TemplateResult | undefined => {
const userDefinedIcon = getComputedStyle(element).getPropertyValue(
`--weather-icon-${state}`
);
if (userDefinedIcon) {
return html`
<div
style="background-size: cover;${styleMap({
"background-image": userDefinedIcon,
})}"
></div>
`;
}
if (weatherSVGs.has(state)) {
return html`${getWeatherStateSVG(state)}`;
}
if (state in weatherIcons) {
return html`
<ha-icon class="weather-icon" .icon=${weatherIcons[state]}></ha-icon>
`;
}
return undefined;
};

View File

@ -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<ZHADevice[]> =>
): Promise<ZHADeviceEndpoint[]> =>
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<ZHAGroup> =>
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<ZHAGroup> =>
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<ZHAGroup> =>
hass.callWS({
type: "zha/group/add",

View File

@ -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 &&

View File

@ -74,7 +74,7 @@ class HassTabsSubpage extends LitElement {
html`
<ha-tab
.hass=${this.hass}
@activated=${this._tabTapped}
@click=${this._tabTapped}
.path=${page.path}
.active=${page === activeTab}
.narrow=${this.narrow}

View File

@ -60,6 +60,7 @@ import type {
import { HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-svg-icon";
import { mdiPlus } from "@mdi/js";
import { LocalizeFunc } from "../../../common/translations/localize";
interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
localized_title?: string;
@ -121,7 +122,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._deviceRegistryEntries = entries;
}),
subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => {
const translationsPromisses: Promise<void>[] = [];
const translationsPromisses: Promise<LocalizeFunc>[] = [];
flowsInProgress.forEach((flow) => {
// To render title placeholders
if (flow.context.title_placeholders) {

View File

@ -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")}
</div>
<zha-devices-data-table
<zha-device-endpoint-data-table
.hass=${this.hass}
.devices=${this.devices}
.deviceEndpoints=${this.deviceEndpoints}
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleAddSelectionChanged}
>
</zha-devices-data-table>
</zha-device-endpoint-data-table>
<div class="paper-dialog-buttons">
<mwc-button
@ -121,7 +121,7 @@ export class ZHAAddGroupPage extends LitElement {
}
private async _fetchData() {
this.devices = await fetchGroupableDevices(this.hass!);
this.deviceEndpoints = await fetchGroupableDevices(this.hass!);
}
private _handleAddSelectionChanged(
@ -132,11 +132,11 @@ export class ZHAAddGroupPage extends LitElement {
private async _createGroup(): Promise<void> {
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 = "";

View File

@ -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%",
},
}
);

View File

@ -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`
<div
class="mdc-data-table__cell table-cell-text"
@click=${this._handleClicked}
style="cursor: pointer;"
>
${name}
</div>
`,
},
endpoint_id: {
title: "Endpoint",
sortable: true,
filterable: true,
},
}
: {
name: {
title: "Name",
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
<div
class="mdc-data-table__cell table-cell-text"
@click=${this._handleClicked}
style="cursor: pointer;"
>
${name}
</div>
`,
},
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`<div
style="overflow: hidden; text-overflow: ellipsis;"
>
${entity.name || entity.original_name}
</div>`
)}
<div>And ${entities.length - 2} more...</div>`
: entities.map(
(entity) =>
html`<div
style="overflow: hidden; text-overflow: ellipsis;"
>
${entity.name || entity.original_name}
</div>`
)
: "This endpoint has no associated entities"}
`,
},
}
);
public clearSelection() {
this._dataTable.clearSelection();
}
protected render(): TemplateResult {
return html`
<ha-data-table
.columns=${this._columns(this.narrow)}
.data=${this._deviceEndpoints(this.deviceEndpoints)}
.selectable=${this.selectable}
auto-height
></ha-data-table>
`;
}
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;
}
}

View File

@ -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`
<div @click=${this._handleClicked} style="cursor: pointer;">
${name}
</div>
`,
},
}
: {
name: {
title: "Name",
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
<div @click=${this._handleClicked} style="cursor: pointer;">
${name}
</div>
`,
},
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`
<ha-data-table
.columns=${this._columns(this.narrow)}
.data=${this._devices(this.devices)}
.selectable=${this.selectable}
auto-height
></ha-data-table>
`;
}
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;
}
}

View File

@ -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`
<hass-subpage .header=${this.group.name}>
<ha-icon-button
@ -122,13 +123,13 @@ export class ZHAGroupPage extends LitElement {
${this.hass.localize("ui.panel.config.zha.groups.members")}
</div>
${members.length
? members.map(
${this.group.members.length
? this.group.members.map(
(member) => html`
<zha-device-card
class="card"
.hass=${this.hass}
.device=${member}
.device=${member.device}
.narrow=${this.narrow}
.showActions=${false}
.showEditableInfo=${false}
@ -140,7 +141,7 @@ export class ZHAGroupPage extends LitElement {
This group has no members
</p>
`}
${members.length
${this.group.members.length
? html`
<div class="header">
${this.hass.localize(
@ -148,14 +149,15 @@ export class ZHAGroupPage extends LitElement {
)}
</div>
<zha-devices-data-table
<zha-device-endpoint-data-table
id="removeMembers"
.hass=${this.hass}
.devices=${members}
.deviceEndpoints=${this.group.members}
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleRemoveSelectionChanged}
>
</zha-devices-data-table>
</zha-device-endpoint-data-table>
<div class="paper-dialog-buttons">
<mwc-button
@ -182,14 +184,15 @@ export class ZHAGroupPage extends LitElement {
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
</div>
<zha-devices-data-table
<zha-device-endpoint-data-table
id="addMembers"
.hass=${this.hass}
.devices=${this._filteredDevices}
.deviceEndpoints=${this._filteredDeviceEndpoints}
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleAddSelectionChanged}
>
</zha-devices-data-table>
</zha-device-endpoint-data-table>
<div class="paper-dialog-buttons">
<mwc-button
@ -218,16 +221,22 @@ export class ZHAGroupPage extends LitElement {
if (this.groupId !== null && this.groupId !== undefined) {
this.group = await fetchGroup(this.hass!, this.groupId);
}
this.devices = await fetchGroupableDevices(this.hass!);
this.deviceEndpoints = await fetchGroupableDevices(this.hass!);
// filter the groupable devices so we only show devices that aren't already in the group
this._filterDevices();
}
private _filterDevices() {
// filter the groupable devices so we only show devices that aren't already in the group
this._filteredDevices = this.devices.filter((device) => {
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<void> {
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<void> {
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;
}

View File

@ -34,7 +34,11 @@ class IntegrationsCard extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Integrations">
<ha-card
.header=${this.hass.localize(
"ui.panel.developer-tools.tabs.info.integrations"
)}
>
<table class="card-content">
<tbody>
${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"
)}
</a>
</td>
${!manifest.is_built_in
? ""
: html`
${manifest.is_built_in || manifest.issue_tracker
? html`
<td>
<a
href=${integrationIssuesUrl(domain)}
href=${integrationIssuesUrl(
domain,
manifest
)}
target="_blank"
rel="noreferrer"
>
Issues
${this.hass.localize(
"ui.panel.developer-tools.tabs.info.issues"
)}
</a>
</td>
`}
`
: ""}
`}
</tr>
`;

View File

@ -84,15 +84,19 @@ class DialogSystemLogDetail extends LitElement {
target="_blank"
rel="noreferrer"
>documentation</a
>${!this._manifest.is_built_in
? ""
: html`,
>${this._manifest.is_built_in ||
this._manifest.issue_tracker
? html`,
<a
href=${integrationIssuesUrl(integration)}
href=${integrationIssuesUrl(
integration,
this._manifest
)}
target="_blank"
rel="noreferrer"
>issues</a
>`})
>`
: ""})
`}
`
: ""}

View File

@ -231,7 +231,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
stateLabel === "triggered" ||
!stateLabel
? ""
: stateLabel;
: this._stateDisplay(state);
}
private _actionDisplay(state: string): string {

View File

@ -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`
<ha-card
@click="${this._handleClick}"
@click=${this._handleClick}
tabindex="0"
style=${styleMap({
"--base-unit": this._baseUnit,
"--round-slider-bar-color": sliderBarColor,
})}
>
<div class="container">
<div class="gauge-a"></div>
<div
class="gauge-c"
style=${styleMap({
transform: `rotate(${this._translateTurn(state)}turn)`,
"background-color": this._computeSeverity(state),
})}
></div>
<div class="gauge-b"></div>
</div>
<round-slider
readonly
arcLength="180"
startAngle="180"
.value=${state}
.min=${this._config.min}
.max=${this._config.max}
></round-slider>
<div class="gauge-data">
<div id="percent">
<div class="percent">
${stateObj.state}
${this._config.unit ||
stateObj.attributes.unit_of_measurement ||
""}
</div>
<div id="name">
<div class="name">
${this._config.name || computeStateName(stateObj)}
</div>
</div>
@ -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<void> {
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;
}
`;
}

View File

@ -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<void> {
await installResizeObserver();
this._resizeObserver = new ResizeObserver(
debounce(() => this._measureCard(), 250, false)
);

View File

@ -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`
<ha-card
@action=${this._handleAction}
@ -166,25 +175,16 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
>
<div class="content">
<div class="icon-image">
${stateObj.state in weatherImages
? html`
<img
class="weather-image"
src="${weatherImages[stateObj.state]}"
/>
`
: html`
<ha-icon
class="weather-icon"
.icon=${weatherIcons[stateObj.state] || stateIcon(stateObj)}
></ha-icon>
`}
${weatherStateIcon ||
html`
<ha-icon
class="weather-icon"
.icon=${stateIcon(stateObj)}
></ha-icon>
`}
</div>
<div class="info">
<div class="name-state">
<div class="name">
${this._config.name || computeStateName(stateObj)}
</div>
<div class="state">
${computeStateDisplay(
this.hass.localize,
@ -192,6 +192,9 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
this.hass.language
)}
</div>
<div class="name">
${this._config.name || computeStateName(stateObj)}
</div>
</div>
<div class="temp-attribute">
<div class="temp">
@ -200,7 +203,20 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
>
</div>
<div class="attribute">
${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)}
</div>
</div>
</div>
@ -231,21 +247,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
${item.condition !== undefined && item.condition !== null
? html`
<div class="forecast-image-icon">
${item.condition in weatherImages
? html`
<img
class="forecast-image"
src="${weatherImages[item.condition]}"
/>
`
: item.condition in weatherIcons
? html`
<ha-icon
class="forecast-icon"
.icon=${weatherIcons[item.condition]}
></ha-icon>
`
: ""}
${getWeatherStateIcon(item.condition, this)}
</div>
`
: ""}
@ -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<void> {
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;
}
`,
];
}
}

View File

@ -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;
}

View File

@ -465,7 +465,8 @@ export const generateLovelaceConfigFromData = async (
};
export const generateLovelaceConfigFromHass = async (
hass: HomeAssistant
hass: HomeAssistant,
localize?: LocalizeFunc
): Promise<LovelaceConfig> => {
// 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
);
};

View File

@ -0,0 +1,6 @@
export const installResizeObserver = async () => {
if (typeof ResizeObserver !== "function") {
const modules = await import("resize-observer");
modules.install();
}
};

View File

@ -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<LovelaceCard>;
public getCardSize() {
return this._assignedNodes ? computeCardSize(this._assignedNodes[0]) : 1;
}
protected render(): TemplateResult {
return html`
<slot></slot>

View File

@ -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
)}%`
: "")}
</div>
`

View File

@ -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}
></hui-theme-select-editor>
</div>
<ha-switch
.checked=${this._config!.show_forecast !== false}
.configValue=${"show_forecast"}
@change=${this._valueChanged}
>Show forecast</ha-switch
>
<div class="side-by-side">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.secondary_info_attribute"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._secondary_info_attribute}
.configValue=${"secondary_info_attribute"}
@value-changed=${this._valueChanged}
></paper-input>
<ha-switch
.checked=${this._config!.show_forecast !== false}
.configValue=${"show_forecast"}
@change=${this._valueChanged}
>${this.hass.localize(
"ui.panel.lovelace.editor.card.weather.show_forecast"
)}</ha-switch
>
</div>
</div>
`;
}

View File

@ -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 {

View File

@ -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`
<hui-generic-entity-row .hass=${this.hass} .config=${weatherRowConfig}>
<div class="attributes">
<div>
${UNAVAILABLE_STATES.includes(stateObj.state)
? computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.language
)
: html`
${stateObj.attributes.temperature}
${getWeatherUnit(this.hass, "temperature")}
`}
</div>
<div class="secondary">
${getSecondaryWeatherAttribute(this.hass!, stateObj)}
</div>
<div
class="icon-image${classMap({
pointer,
})}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
tabindex=${ifDefined(pointer ? "0" : undefined)}
>
${weatherStateIcon ||
html`
<ha-icon class="weather-icon" .icon=${stateIcon(stateObj)}></ha-icon>
`}
</div>
<div
class="info ${classMap({
pointer,
})}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
>
${this._config.name || computeStateName(stateObj)}
</div>
<div class="attributes">
<div>
${UNAVAILABLE_STATES.includes(stateObj.state)
? computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.language
)
: html`
${stateObj.attributes.temperature}
${getWeatherUnit(this.hass, "temperature")}
`}
</div>
</hui-generic-entity-row>
<div class="secondary">
${getSecondaryWeatherAttribute(this.hass!, stateObj)}
</div>
</div>
`;
}
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);
}
`,
];
}
}

View File

@ -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,
});

View File

@ -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 {

View File

@ -34,6 +34,7 @@ export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant;
isPanel?: boolean;
editMode?: boolean;
index?: number;
getCardSize(): number;
setConfig(config: LovelaceCardConfig): void;
}

View File

@ -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()}
<div id="badges"></div>
<div id="columns"></div>
${this.lovelace!.editMode
@ -113,82 +113,6 @@ export class HUIView extends LitElement {
`;
}
protected renderStyles(): TemplateResult {
return html`
<style>
: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;
}
}
</style>
`;
}
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 {

View File

@ -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 <T extends Constructor<HassBaseEl>>(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<typeof getHassTranslations>[2],
integration?: Parameters<typeof getHassTranslations>[3],
configFlow?: Parameters<typeof getHassTranslations>[4],
force = false
) {
): Promise<LocalizeFunc> {
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 <T extends Constructor<HassBaseEl>>(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 <T extends Constructor<HassBaseEl>>(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 <T extends Constructor<HassBaseEl>>(superClass: T) =>
// multiple fragments.
const resources = {
[language]: {
...(this.hass &&
this.hass.resources &&
this.hass.resources[language]),
...this.hass?.resources?.[language],
...data,
},
};

View File

@ -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",

View File

@ -224,7 +224,7 @@ export interface HomeAssistant {
category: Parameters<typeof getHassTranslations>[2],
integration?: Parameters<typeof getHassTranslations>[3],
configFlow?: Parameters<typeof getHassTranslations>[4]
): Promise<void>;
): Promise<LocalizeFunc>;
}
export type LightEntity = HassEntityBase & {

View File

@ -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",

View File

@ -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",

View File

@ -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": {

View File

@ -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",

View File

@ -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ä"
},

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": {

View File

@ -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}”?"
},

View File

@ -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": {

View File

@ -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": "經度",

View File

@ -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"