Merge pull request #5399 from home-assistant/dev

20200401.0
This commit is contained in:
Bram Kragten 2020-04-01 19:40:40 +02:00 committed by GitHub
commit da80a3896d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
150 changed files with 2408 additions and 921 deletions

3
.gitignore vendored
View File

@ -31,3 +31,6 @@ src/cast/dev_const.ts
# Secrets
.lokalise_token
yarn-error.log
#asdf
.tool-versions

View File

@ -50,15 +50,8 @@ stages:
- template: templates/azp-job-wheels.yaml@azure
parameters:
builderVersion: '$(versionWheels)'
builderApk: 'build-base'
wheelsLocal: true
wheelsRequirement: 'requirement.txt'
preBuild:
- task: NodeTool@0
displayName: "Use Node $(versionNode)"
inputs:
versionSpec: "$(versionNode)"
- script: |
set -e
yarn install
script/build_frontend
sleep 240
echo "home-assistant-frontend==$(Build.SourceBranchName)" > requirement.txt

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 805 B

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 B

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -16,7 +16,8 @@ class DemoCard extends PolymerElement {
color: var(--primary-color);
}
#card {
width: 400px;
max-width: 400px;
width: 100vw;
}
pre {
width: 400px;

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="36" height="36" viewBox="0 0 24 24"><path fill="#505050" d="M19,3A2,2 0 0,1 21,5V11H19V13H19L17,13V15H15V17H13V19H11V21H5C3.89,21 3,20.1 3,19V5A2,2 0 0,1 5,3H19M21,15V19A2,2 0 0,1 19,21H19L15,21V19H17V17H19V15H21M19,8.5A0.5,0.5 0 0,0 18.5,8H5.5A0.5,0.5 0 0,0 5,8.5V15.5A0.5,0.5 0 0,0 5.5,16H11V15H13V13H15V11H17V9H19V8.5Z" /></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="36" height="36" version="1.1" viewBox="0 0 24 24"><path fill="#505050" d="M19,3A2,2 0 0,1 21,5V11H19V13H19L17,13V15H15V17H13V19H11V21H5C3.89,21 3,20.1 3,19V5A2,2 0 0,1 5,3H19M21,15V19A2,2 0 0,1 19,21H19L15,21V19H17V17H19V15H21M19,8.5A0.5,0.5 0 0,0 18.5,8H5.5A0.5,0.5 0 0,0 5,8.5V15.5A0.5,0.5 0 0,0 5.5,16H11V15H13V13H15V11H17V9H19V8.5Z"/></svg>

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

View File

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

View File

@ -60,6 +60,7 @@ export class StateBadge extends LitElement {
const iconStyle: Partial<CSSStyleDeclaration> = {
color: "",
filter: "",
display: "",
};
const hostStyle: Partial<CSSStyleDeclaration> = {
backgroundImage: "",
@ -76,7 +77,7 @@ export class StateBadge extends LitElement {
}
hostStyle.backgroundImage = `url(${imageUrl})`;
iconStyle.display = "none";
} else {
} else if (stateObj.state === "on") {
if (stateObj.attributes.hs_color && this.stateColor !== false) {
const hue = stateObj.attributes.hs_color[0];
const sat = stateObj.attributes.hs_color[1];

View File

@ -55,7 +55,8 @@ export const fetchRecent = (
entityId,
startTime,
endTime,
skipInitialState = false
skipInitialState = false,
significantChangesOnly?: boolean
): Promise<HassEntity[][]> => {
let url = "history/period";
if (startTime) {
@ -68,6 +69,9 @@ export const fetchRecent = (
if (skipInitialState) {
url += "&skip_initial_state";
}
if (significantChangesOnly !== undefined) {
url += `&significant_changes_only=${Number(significantChangesOnly)}`;
}
return hass.callApi("GET", url);
};

View File

@ -0,0 +1,23 @@
export interface CustomCardEntry {
type: string;
name?: string;
description?: string;
preview?: boolean;
}
export interface CustomCardsWindow {
customCards?: CustomCardEntry[];
}
export const CUSTOM_TYPE_PREFIX = "custom:";
const customCardsWindow = window as CustomCardsWindow;
if (!("customCards" in customCardsWindow)) {
customCardsWindow.customCards = [];
}
export const customCards = customCardsWindow.customCards!;
export const getCustomCardEntry = (type: string) =>
customCards.find((card) => card.type === type);

80
src/data/weather.ts Normal file
View File

@ -0,0 +1,80 @@
import { HomeAssistant } from "../types";
export const weatherImages = {
"clear-night": "/static/images/weather/night.png",
cloudy: "/static/images/weather/cloudy.png",
fog: "/static/images/weather/cloudy.png",
hail: "/static/images/weather/rainy.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",
snowy: "/static/images/weather/snowy.png",
"snowy-rainy": "/static/images/weather/rainy.png",
sunny: "/static/images/weather/sunny.png",
windy: "/static/images/weather/windy.png",
"windy-variant": "/static/images/weather/windy.png",
};
export const weatherIcons = {
exceptional: "hass:alert-circle-outline",
};
export const cardinalDirections = [
"N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW",
"N",
];
const getWindBearingText = (degree: string): string => {
const degreenum = parseInt(degree, 10);
if (isFinite(degreenum)) {
// tslint:disable-next-line: no-bitwise
return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
}
return degree;
};
export const getWindBearing = (bearing: string): string => {
if (bearing != null) {
return getWindBearingText(bearing);
}
return "";
};
export const getWeatherUnit = (
hass: HomeAssistant,
measure: string
): string => {
const lengthUnit = hass.config.unit_system.length || "";
switch (measure) {
case "pressure":
return lengthUnit === "km" ? "hPa" : "inHg";
case "wind_speed":
return `${lengthUnit}/h`;
case "length":
return lengthUnit;
case "precipitation":
return lengthUnit === "km" ? "mm" : "in";
case "humidity":
case "precipitation_probability":
return "%";
default:
return hass.config.unit_system[measure] || "";
}
};

View File

@ -31,7 +31,7 @@ export class HuiPersistentNotificationItem extends LitElement {
return html`
<notification-item-template>
<span slot="header">
${this.notification.title || this.notification.notification_id}
${this.notification.title}
</span>
<ha-markdown content="${this.notification.message}"></ha-markdown>

View File

@ -116,13 +116,9 @@ class OnboardingCoreConfig extends LitElement {
@value-changed=${this._handleChange}
>
<span slot="suffix">
${this._unitSystem === "metric"
? this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)
: this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_feet"
)}
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
</span>
</paper-input>
</div>

View File

@ -102,13 +102,9 @@ class ConfigCoreForm extends LitElement {
@value-changed=${this._handleChange}
>
<span slot="suffix">
${this._unitSystem === "metric"
? this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)
: this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_feet"
)}
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
</span>
</paper-input>
</div>

View File

@ -97,6 +97,15 @@ export class HaConfigDevicePage extends LitElement {
)
);
private _computeArea = memoizeOne((areas, device):
| AreaRegistryEntry
| undefined => {
if (!areas || !device || !device.area_id) {
return undefined;
}
return areas.find((area) => area.area_id === device.area_id);
});
private _batteryEntity = memoizeOne((entities: EntityRegistryEntry[]):
| EntityRegistryEntry
| undefined => findBatteryEntity(this.hass, entities));
@ -132,7 +141,7 @@ export class HaConfigDevicePage extends LitElement {
const batteryState = batteryEntity
? this.hass.states[batteryEntity.entity_id]
: undefined;
const areaName = this._computeAreaName(this.areas, device);
const area = this._computeArea(this.areas, device);
return html`
<hass-tabs-subpage
@ -165,12 +174,16 @@ export class HaConfigDevicePage extends LitElement {
: html`
<div>
<h1>${computeDeviceName(device, this.hass)}</h1>
${areaName
? this.hass.localize(
"ui.panel.config.integrations.config_entry.area",
"area",
areaName
)
${area
? html`
<a href="/config/areas/area/${area.area_id}"
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.area",
"area",
area.name || "Unnamed Area"
)}</a
>
`
: ""}
</div>
`
@ -437,13 +450,6 @@ export class HaConfigDevicePage extends LitElement {
return state ? computeStateName(state) : null;
}
private _computeAreaName(areas, device): string | undefined {
if (!areas || !device || !device.area_id) {
return undefined;
}
return areas.find((area) => area.area_id === device.area_id).name;
}
private _onImageLoad(ev) {
ev.target.style.display = "inline-block";
}
@ -648,6 +654,10 @@ export class HaConfigDevicePage extends LitElement {
a {
text-decoration: none;
color: var(--primary-color);
}
ha-card a {
color: var(--primary-text-color);
}
`;

View File

@ -200,6 +200,15 @@ export class HaConfigManagerDashboard extends LitElement {
href="/config/integrations/config_entry/${item.entry_id}"
>
<paper-item data-index=${idx}>
<img
src="https://brands.home-assistant.io/${item.domain}/icon.png"
srcset="
https://brands.home-assistant.io/${item.domain}/icon@2x.png 2x
"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
<paper-item-body two-line>
<div>
${this.hass.localize(
@ -342,6 +351,14 @@ export class HaConfigManagerDashboard extends LitElement {
return states;
}
private _onImageLoad(ev) {
ev.target.style.visibility = "initial";
}
private _onImageError(ev) {
ev.target.style.visibility = "hidden";
}
static get styles(): CSSResult {
return css`
mwc-button {
@ -355,6 +372,9 @@ export class HaConfigManagerDashboard extends LitElement {
cursor: pointer;
margin: 8px;
}
.configured {
padding-bottom: 24px;
}
.configured a {
color: var(--primary-text-color);
text-decoration: none;
@ -375,6 +395,10 @@ export class HaConfigManagerDashboard extends LitElement {
.overflow {
width: 56px;
}
img {
width: 50px;
margin-right: 16px;
}
`;
}
}

View File

@ -1,8 +1,7 @@
import "@material/mwc-button";
import "@polymer/paper-spinner/paper-spinner";
import "../../../components/ha-switch";
import "../../../components/ha-dialog";
import "../../../resources/ha-style";
import {
LitElement,
html,
@ -10,6 +9,8 @@ import {
customElement,
property,
PropertyValues,
CSSResult,
css,
} from "lit-element";
import { HomeAssistant } from "../../../types";
import { PolymerChangedEvent } from "../../../polymer-types";
@ -17,11 +18,12 @@ import { AddUserDialogParams } from "./show-dialog-add-user";
import {
User,
SYSTEM_GROUP_ID_USER,
GROUPS,
createUser,
deleteUser,
SYSTEM_GROUP_ID_ADMIN,
} from "../../../data/user";
import { createAuthForUser } from "../../../data/auth";
import { haStyleDialog } from "../../../resources/styles";
@customElement("dialog-add-user")
export class DialogAddUser extends LitElement {
@ -33,14 +35,14 @@ export class DialogAddUser extends LitElement {
@property() private _name?: string;
@property() private _username?: string;
@property() private _password?: string;
@property() private _group?: string;
@property() private _isAdmin?: boolean;
public showDialog(params: AddUserDialogParams) {
this._params = params;
this._name = "";
this._username = "";
this._password = "";
this._group = SYSTEM_GROUP_ID_USER;
this._isAdmin = false;
this._error = undefined;
this._loading = false;
}
@ -106,25 +108,10 @@ export class DialogAddUser extends LitElement {
@value-changed=${this._passwordChanged}
error-message="Required"
></paper-input>
<ha-paper-dropdown-menu
.label=${this.hass.localize("ui.panel.config.users.editor.group")}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._group}
@iron-select=${this._handleGroupChange}
attr-for-selected="group-id"
>
${GROUPS.map(
(groupId) => html`
<paper-item group-id=${groupId}>
${this.hass.localize(`groups.${groupId}`)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
${this._group === SYSTEM_GROUP_ID_USER
<ha-switch .checked=${this._isAdmin} @change=${this._adminChanged}>
${this.hass.localize("ui.panel.config.users.editor.admin")}
</ha-switch>
${!this._isAdmin
? html`
<br />
The users group is a work in progress. The user will be unable
@ -191,8 +178,8 @@ export class DialogAddUser extends LitElement {
this._password = ev.detail.value;
}
private async _handleGroupChange(ev): Promise<void> {
this._group = ev.detail.item.getAttribute("group-id");
private async _adminChanged(ev): Promise<void> {
this._isAdmin = ev.target.checked;
}
private async _createUser(ev) {
@ -207,7 +194,7 @@ export class DialogAddUser extends LitElement {
let user: User;
try {
const userResponse = await createUser(this.hass, this._name, [
this._group!,
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
]);
user = userResponse.user;
} catch (err) {
@ -233,6 +220,20 @@ export class DialogAddUser extends LitElement {
this._params!.userAddedCallback(user);
this._close();
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
ha-switch {
margin-top: 8px;
}
`,
];
}
}
declare global {

View File

@ -10,20 +10,22 @@ import {
TemplateResult,
css,
} from "lit-element";
import "../../../components/entity/ha-entities-picker";
import "../../../components/user/ha-user-picker";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { UserDetailDialogParams } from "./show-dialog-user-detail";
import "../../../components/ha-switch";
import { createCloseHeading } from "../../../components/ha-dialog";
import { GROUPS, SYSTEM_GROUP_ID_USER } from "../../../data/user";
import {
SYSTEM_GROUP_ID_ADMIN,
SYSTEM_GROUP_ID_USER,
} from "../../../data/user";
@customElement("dialog-user-detail")
class DialogUserDetail extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _name!: string;
@property() private _group?: string;
@property() private _isAdmin?: boolean;
@property() private _error?: string;
@property() private _params?: UserDetailDialogParams;
@property() private _submitting: boolean = false;
@ -32,7 +34,7 @@ class DialogUserDetail extends LitElement {
this._params = params;
this._error = undefined;
this._name = params.entry.name || "";
this._group = params.entry.group_ids[0];
this._isAdmin = params.entry.group_ids[0] === SYSTEM_GROUP_ID_ADMIN;
await this.updateComplete;
}
@ -55,31 +57,55 @@ class DialogUserDetail extends LitElement {
<div class="error">${this._error}</div>
`
: ""}
<div class="secondary">
${this.hass.localize("ui.panel.config.users.editor.id")}: ${user.id}
</div>
<div>
${user.is_owner
? html`
<span class="state"
>${this.hass.localize(
"ui.panel.config.users.editor.owner"
)}</span
>
`
: ""}
${user.system_generated
? html`
<span class="state">
${this.hass.localize(
"ui.panel.config.users.editor.system_generated"
)}
</span>
`
: ""}
${user.is_active
? html`
<span class="state"
>${this.hass.localize(
"ui.panel.config.users.editor.active"
)}</span
>
`
: ""}
</div>
<div class="form">
<paper-input
.value=${this._name}
.disabled=${user.system_generated}
@value-changed=${this._nameChanged}
label="${this.hass!.localize("ui.panel.config.user.editor.name")}"
label="${this.hass!.localize(
"ui.panel.config.users.editor.name"
)}"
></paper-input>
<ha-paper-dropdown-menu
.label=${this.hass.localize("ui.panel.config.users.editor.group")}
<ha-switch
.disabled=${user.system_generated}
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._group}
@iron-select=${this._handleGroupChange}
attr-for-selected="group-id"
>
${GROUPS.map(
(groupId) => html`
<paper-item group-id=${groupId}>
${this.hass.localize(`groups.${groupId}`)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
${this._group === SYSTEM_GROUP_ID_USER
${this.hass.localize("ui.panel.config.users.editor.admin")}
</ha-switch>
${!this._isAdmin
? html`
<br />
The users group is a work in progress. The user will be unable
@ -88,34 +114,6 @@ class DialogUserDetail extends LitElement {
limit access to administrators.
`
: ""}
<table>
<tr>
<td>
${this.hass.localize("ui.panel.config.users.editor.id")}
</td>
<td>${user.id}</td>
</tr>
<tr>
<td>
${this.hass.localize("ui.panel.config.users.editor.owner")}
</td>
<td>${user.is_owner}</td>
</tr>
<tr>
<td>
${this.hass.localize("ui.panel.config.users.editor.active")}
</td>
<td>${user.is_active}</td>
</tr>
<tr>
<td>
${this.hass.localize(
"ui.panel.config.users.editor.system_generated"
)}
</td>
<td>${user.system_generated}</td>
</tr>
</table>
</div>
</div>
@ -129,21 +127,33 @@ class DialogUserDetail extends LitElement {
</mwc-button>
${user.system_generated
? html`
<paper-tooltip position="right"
>${this.hass.localize(
<paper-tooltip position="right">
${this.hass.localize(
"ui.panel.config.users.editor.system_generated_users_not_removable"
)}</paper-tooltip
>
)}
</paper-tooltip>
`
: ""}
</div>
<div slot="primaryAction">
<mwc-button
@click=${this._updateEntry}
.disabled=${!this._name ||
this._submitting ||
user.system_generated}
>
${this.hass!.localize("ui.panel.config.users.editor.update_user")}
</mwc-button>
${user.system_generated
? html`
<paper-tooltip position="left">
${this.hass.localize(
"ui.panel.config.users.editor.system_generated_users_not_editable"
)}
</paper-tooltip>
`
: ""}
</div>
<mwc-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!this._name}
>
${this.hass!.localize("ui.panel.config.users.editor.update_user")}
</mwc-button>
</ha-dialog>
`;
}
@ -153,8 +163,8 @@ class DialogUserDetail extends LitElement {
this._name = ev.detail.value;
}
private async _handleGroupChange(ev): Promise<void> {
this._group = ev.detail.item.getAttribute("group-id");
private async _adminChanged(ev): Promise<void> {
this._isAdmin = ev.target.checked;
}
private async _updateEntry() {
@ -162,7 +172,9 @@ class DialogUserDetail extends LitElement {
try {
await this._params!.updateEntry({
name: this._name.trim(),
group_ids: [this._group!],
group_ids: [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
],
});
this._close();
} catch (err) {
@ -194,8 +206,24 @@ class DialogUserDetail extends LitElement {
ha-dialog {
--mdc-dialog-min-width: 500px;
}
table {
width: 100%;
.form {
padding-top: 16px;
}
.secondary {
color: var(--secondary-text-color);
}
.state {
background-color: rgba(var(--rgb-primary-text-color), 0.15);
border-radius: 16px;
padding: 4px 8px;
margin-top: 8px;
display: inline-block;
}
.state:not(:first-child) {
margin-left: 8px;
}
ha-switch {
margin-top: 8px;
}
`,
];

View File

@ -157,7 +157,7 @@ export class HaConfigUsers extends LitElement {
showAddUserDialog(this, {
userAddedCallback: async (user: User) => {
if (user) {
this._users = { ...this._users, ...user };
this._users = [...this._users, user];
}
},
});

View File

@ -6,6 +6,7 @@ import {
css,
CSSResult,
PropertyValues,
query,
} from "lit-element";
import "../../../layouts/hass-subpage";
@ -18,7 +19,7 @@ import {
addGroup,
ZHAGroup,
} from "../../../data/zha";
import "./zha-devices-data-table";
import { ZHADevicesDataTable } from "./zha-devices-data-table";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import { navigate } from "../../../common/navigate";
import { PolymerChangedEvent } from "../../../polymer-types";
@ -34,6 +35,8 @@ export class ZHAAddGroupPage extends LitElement {
@property() public devices: ZHADevice[] = [];
@property() private _processingAdd: boolean = false;
@property() private _groupName: string = "";
@query("zha-devices-data-table")
private _zhaDevicesDataTable!: ZHADevicesDataTable;
private _firstUpdatedCalled: boolean = false;
private _selectedDevicesToAdd: string[] = [];
@ -130,6 +133,7 @@ export class ZHAAddGroupPage extends LitElement {
this._selectedDevicesToAdd = [];
this._processingAdd = false;
this._groupName = "";
this._zhaDevicesDataTable.clearSelection();
navigate(this, `/config/zha/group/${group.group_id}`, true);
}

View File

@ -9,10 +9,14 @@ import {
TemplateResult,
property,
customElement,
query,
} from "lit-element";
import { HomeAssistant } from "../../../types";
// tslint:disable-next-line
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import {
DataTableColumnContainer,
HaDataTable,
} from "../../../components/data-table/ha-data-table";
// tslint:disable-next-line
import { Cluster } from "../../../data/zha";
import { formatAsPaddedHex } from "./functions";
@ -27,6 +31,7 @@ export class ZHAClustersDataTable extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow = false;
@property() public clusters: Cluster[] = [];
@query("ha-data-table") private _dataTable!: HaDataTable;
private _clusters = memoizeOne((clusters: Cluster[]) => {
let outputClusters: ClusterRowData[] = clusters;
@ -77,6 +82,10 @@ export class ZHAClustersDataTable extends LitElement {
}
);
public clearSelection() {
this._dataTable.clearSelection();
}
protected render(): TemplateResult {
return html`
<ha-data-table

View File

@ -9,10 +9,14 @@ import {
TemplateResult,
property,
customElement,
query,
} from "lit-element";
import { HomeAssistant } from "../../../types";
// tslint:disable-next-line
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import {
DataTableColumnContainer,
HaDataTable,
} from "../../../components/data-table/ha-data-table";
// tslint:disable-next-line
import { ZHADevice } from "../../../data/zha";
import { showZHADeviceInfoDialog } from "../../../dialogs/zha-device-info-dialog/show-dialog-zha-device-info";
@ -27,6 +31,7 @@ export class ZHADevicesDataTable extends LitElement {
@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;
@ -89,6 +94,10 @@ export class ZHADevicesDataTable extends LitElement {
}
);
public clearSelection() {
this._dataTable.clearSelection();
}
protected render(): TemplateResult {
return html`
<ha-data-table

View File

@ -16,6 +16,7 @@ import {
property,
PropertyValues,
TemplateResult,
query,
} from "lit-element";
import {
@ -26,7 +27,7 @@ import {
Cluster,
fetchClustersForZhaNode,
} from "../../../data/zha";
import "./zha-clusters-data-table";
import { ZHAClustersDataTable } from "./zha-clusters-data-table";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { ItemSelectedEvent } from "./types";
@ -47,6 +48,8 @@ export class ZHAGroupBindingControl extends LitElement {
@property() private _clusters: Cluster[] = [];
private _groupToBind?: ZHAGroup;
private _clustersToBind?: Cluster[];
@query("zha-devices-data-table")
private _zhaClustersDataTable!: ZHAClustersDataTable;
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedDevice")) {
@ -187,6 +190,7 @@ export class ZHAGroupBindingControl extends LitElement {
this._groupToBind!.group_id,
this._clustersToBind!
);
this._zhaClustersDataTable.clearSelection();
}
}
@ -198,6 +202,7 @@ export class ZHAGroupBindingControl extends LitElement {
this._groupToBind!.group_id,
this._clustersToBind!
);
this._zhaClustersDataTable.clearSelection();
}
}

View File

@ -9,10 +9,14 @@ import {
TemplateResult,
property,
customElement,
query,
} from "lit-element";
import { HomeAssistant } from "../../../types";
// tslint:disable-next-line
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import {
DataTableColumnContainer,
HaDataTable,
} from "../../../components/data-table/ha-data-table";
// tslint:disable-next-line
import { ZHAGroup, ZHADevice } from "../../../data/zha";
import { formatAsPaddedHex } from "./functions";
@ -29,6 +33,7 @@ export class ZHAGroupsDataTable extends LitElement {
@property() public narrow = false;
@property() public groups: ZHAGroup[] = [];
@property() public selectable = false;
@query("ha-data-table") private _dataTable!: HaDataTable;
private _groups = memoizeOne((groups: ZHAGroup[]) => {
let outputGroups: GroupRowData[] = groups;
@ -98,6 +103,10 @@ export class ZHAGroupsDataTable extends LitElement {
}
);
public clearSelection() {
this._dataTable.clearSelection();
}
protected render(): TemplateResult {
return html`
<ha-data-table

View File

@ -82,7 +82,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
}
this._config = {
theme: "default",
hold_action: { action: "more-info" },
double_tap_action: { action: "none" },
show_icon: true,

View File

@ -30,7 +30,12 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard {
throw new Error("No card configured.");
}
if (this._element && this._element.parentElement) {
this.removeChild(this._element);
}
this._element = createCardElement(config.card) as LovelaceCard;
this.appendChild(this._element);
}
public getCardSize(): number {

View File

@ -93,7 +93,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
public setConfig(config: EntitiesCardConfig): void {
const entities = processConfigEntities(config.entities);
this._config = { theme: "default", ...config };
this._config = config;
this._configEntities = entities;
if (config.show_header_toggle === undefined) {
// Default value is show toggle if we can at least toggle 2 entities.

View File

@ -0,0 +1,243 @@
import {
html,
LitElement,
PropertyValues,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateIcon } from "../../../common/entity/state_icon";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../components/hui-warning";
import {
LovelaceCard,
LovelaceCardEditor,
LovelaceHeaderFooter,
} from "../types";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { EntityCardConfig } from "./types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { actionHandler } from "../common/directives/action-handler-directive";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { findEntities } from "../common/find-entites";
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
import { UNKNOWN, UNAVAILABLE } from "../../../data/entity";
import { HuiErrorCard } from "./hui-error-card";
@customElement("hui-entity-card")
export class HuiEntityCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(
/* webpackChunkName: "hui-entity-card-editor" */ "../editor/config-elements/hui-entity-card-editor"
);
return document.createElement("hui-entity-card-editor");
}
public static getStubConfig(
hass: HomeAssistant,
entities: string[],
entitiesFill: string[]
) {
const includeDomains = ["sensor", "light", "switch"];
const maxEntities = 1;
const foundEntities = findEntities(
hass,
maxEntities,
entities,
entitiesFill,
includeDomains
);
return {
entity: foundEntities[0] || "",
};
}
@property() public hass?: HomeAssistant;
@property() private _config?: EntityCardConfig;
private _footerElement?: HuiErrorCard | LovelaceHeaderFooter;
public setConfig(config: EntityCardConfig): void {
if (config.entity && !isValidEntityId(config.entity)) {
throw new Error("Invalid Entity");
}
this._config = config;
if (this._config.footer) {
this._footerElement = createHeaderFooterElement(this._config.footer);
} else if (this._footerElement) {
this._footerElement = undefined;
}
}
public getCardSize(): number {
return 1 + (this._config?.footer ? 1 : 0);
}
protected render(): TemplateResult {
if (!this._config || !this.hass) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
<hui-warning
>${this.hass.localize(
"ui.panel.lovelace.warning.entity_not_found",
"entity",
this._config.entity
)}</hui-warning
>
`;
}
const showUnit = this._config.attribute
? this._config.attribute in stateObj.attributes
: stateObj.state !== UNKNOWN && stateObj.state !== UNAVAILABLE;
return html`
<ha-card>
<div
@action=${this._handleClick}
.actionHandler=${actionHandler()}
tabindex="0"
>
<div class="header">
<div class="name">
${this._config.name || computeStateName(stateObj)}
</div>
<div class="icon">
<ha-icon
.icon=${this._config.icon || stateIcon(stateObj)}
></ha-icon>
</div>
</div>
<div class="info">
<span class="value"
>${"attribute" in this._config
? stateObj.attributes[this._config.attribute!] ||
this.hass.localize("state.default.unknown")
: this.hass.localize(`state.default.${stateObj.state}`) ||
this.hass.localize(
`state.${this._config.entity.split(".")[0]}.${
stateObj.state
}`
) ||
stateObj.state}</span
>${showUnit
? html`
<span class="measurement"
>${this._config.unit ||
(this._config.attribute
? ""
: stateObj.attributes.unit_of_measurement)}</span
>
`
: ""}
</div>
</div>
${this._footerElement}
</ha-card>
`;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
// Side Effect used to update footer hass while keeping optimizations
if (this._footerElement) {
this._footerElement.hass = this.hass;
}
return hasConfigOrEntityChanged(this, changedProps);
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| EntityCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
}
}
private _handleClick(): void {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
}
static get styles(): CSSResult {
return css`
ha-card > div {
cursor: pointer;
}
.header {
display: flex;
padding: 8px 16px 0;
justify-content: space-between;
}
.name {
color: var(--secondary-text-color);
line-height: 40px;
font-weight: 500;
font-size: 16px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.icon {
color: var(--state-icon-color, #44739e);
line-height: 40px;
}
.info {
padding: 0px 16px 16px;
margin-top: -4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 28px;
}
.value {
font-size: 28px;
margin-right: 4px;
}
.measurement {
font-size: 18px;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-entity-card": HuiEntityCard;
}
}

View File

@ -82,7 +82,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
if (!isValidEntityId(config.entity)) {
throw new Error("Invalid Entity");
}
this._config = { min: 0, max: 100, theme: "default", ...config };
this._config = { min: 0, max: 100, ...config };
}
public connectedCallback(): void {

View File

@ -74,7 +74,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
}
public setConfig(config: GlanceCardConfig): void {
this._config = { theme: "default", state_color: true, ...config };
this._config = { state_color: true, ...config };
const entities = processConfigEntities<GlanceConfigEntity>(config.entities);
for (const entity of entities) {

View File

@ -70,7 +70,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
throw new Error("Entities need to be an array");
}
this._config = { theme: "default", ...config };
this._config = config;
this._configEntities = config.entities
? processConfigEntities(config.entities)
: [];

View File

@ -25,12 +25,15 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant, LightEntity } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { toggleEntity } from "../common/entity/toggle-entity";
import { LightCardConfig } from "./types";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { SUPPORT_BRIGHTNESS } from "../../../data/light";
import { findEntities } from "../common/find-entites";
import { UNAVAILABLE } from "../../../data/entity";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
@customElement("hui-light-card")
export class HuiLightCard extends LitElement implements LovelaceCard {
@ -74,7 +77,10 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
throw new Error("Specify an entity from within the light domain.");
}
this._config = { theme: "default", ...config };
this._config = {
...config,
tap_action: { action: "toggle" },
};
}
protected render(): TemplateResult {
@ -143,7 +149,11 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
filter: this._computeBrightness(stateObj),
color: this._computeColor(stateObj),
})}
@click=${this._handleClick}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
tabindex="0"
></paper-icon-button>
</div>
@ -222,7 +232,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
}
private _computeBrightness(stateObj: LightEntity): string {
if (!stateObj.attributes.brightness) {
if (stateObj.state === "off" || !stateObj.attributes.brightness) {
return "";
}
const brightness = stateObj.attributes.brightness;
@ -230,7 +240,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
}
private _computeColor(stateObj: LightEntity): string {
if (!stateObj.attributes.hs_color) {
if (stateObj.state === "off" || !stateObj.attributes.hs_color) {
return "";
}
const [hue, sat] = stateObj.attributes.hs_color;
@ -240,8 +250,8 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
private _handleClick() {
toggleEntity(this.hass!, this._config!.entity!);
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
private _handleMoreInfo() {

View File

@ -1,5 +1,13 @@
import "@polymer/paper-icon-button/paper-icon-button";
import { Layer, Marker, Circle, Map } from "leaflet";
import {
Layer,
Marker,
Circle,
Map,
CircleMarker,
Polyline,
LatLngTuple,
} from "leaflet";
import {
LitElement,
TemplateResult,
@ -10,7 +18,6 @@ import {
CSSResult,
customElement,
} from "lit-element";
import "../../map/ha-entity-marker";
import {
@ -32,6 +39,9 @@ import { MapCardConfig } from "./types";
import { classMap } from "lit-html/directives/class-map";
import { findEntities } from "../common/find-entites";
import { HassEntity } from "home-assistant-js-websocket";
import { fetchRecent } from "../../../data/history";
@customElement("hui-map-card")
class HuiMapCard extends LitElement implements LovelaceCard {
public static async getConfigElement() {
@ -66,6 +76,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
@property({ type: Boolean, reflect: true })
public editMode = false;
@property()
private _history?: HassEntity[][];
private _date?: Date;
@property()
private _config?: MapCardConfig;
private _configEntities?: EntityConfig[];
@ -86,7 +100,24 @@ class HuiMapCard extends LitElement implements LovelaceCard {
);
private _mapItems: Array<Marker | Circle> = [];
private _mapZones: Array<Marker | Circle> = [];
private _mapPaths: Array<Polyline | CircleMarker> = [];
private _connected = false;
private _colorDict: { [key: string]: string } = {};
private _colorIndex: number = 0;
private _colors: string[] = [
"#0288D1",
"#00AA00",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
];
public setConfig(config: MapCardConfig): void {
if (!config) {
@ -112,6 +143,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this._configEntities = config.entities
? processConfigEntities(config.entities)
: [];
this._cleanupHistory();
}
public getCardSize(): number {
@ -223,7 +256,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("hass")) {
if (changedProps.has("hass") || changedProps.has("_history")) {
this._drawEntities();
this._fitMap();
}
@ -233,6 +266,15 @@ class HuiMapCard extends LitElement implements LovelaceCard {
) {
this.updateMap(changedProps.get("_config") as MapCardConfig);
}
if (this._config!.hours_to_show && this._configEntities?.length) {
const minute = 60000;
if (changedProps.has("_config")) {
this._getHistory();
} else if (Date.now() - this._date!.getTime() >= minute) {
this._getHistory();
}
}
}
private get _mapEl(): HTMLDivElement {
@ -285,9 +327,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return;
}
const bounds = this.Leaflet.latLngBounds(
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : []
);
const bounds = this.Leaflet.featureGroup(this._mapItems).getBounds();
this._leafletMap.fitBounds(bounds.pad(0.5));
if (zoom && this._leafletMap.getZoom() > zoom) {
@ -295,6 +335,18 @@ class HuiMapCard extends LitElement implements LovelaceCard {
}
}
private _getColor(entityId: string) {
let color;
if (this._colorDict[entityId]) {
color = this._colorDict[entityId];
} else {
color = this._colors[this._colorIndex];
this._colorIndex = (this._colorIndex + 1) % this._colors.length;
this._colorDict[entityId] = color;
}
return color;
}
private _drawEntities(): void {
const hass = this.hass;
const map = this._leafletMap;
@ -314,6 +366,11 @@ class HuiMapCard extends LitElement implements LovelaceCard {
}
const mapZones: Layer[] = (this._mapZones = []);
if (this._mapPaths) {
this._mapPaths.forEach((marker) => marker.remove());
}
const mapPaths: Layer[] = (this._mapPaths = []);
const allEntities = this._configEntities!.concat();
// Calculate visible geo location sources
@ -331,6 +388,60 @@ class HuiMapCard extends LitElement implements LovelaceCard {
}
}
// DRAW history
if (this._config!.hours_to_show && this._history) {
for (const entityStates of this._history) {
if (entityStates?.length <= 1) {
continue;
}
const entityId = entityStates[0].entity_id;
// filter location data from states and remove all invalid locations
const path = entityStates.reduce(
(accumulator: LatLngTuple[], state) => {
const latitude = state.attributes.latitude;
const longitude = state.attributes.longitude;
if (latitude && longitude) {
accumulator.push([latitude, longitude] as LatLngTuple);
}
return accumulator;
},
[]
) as LatLngTuple[];
// DRAW HISTORY
for (
let markerIndex = 0;
markerIndex < path.length - 1;
markerIndex++
) {
const opacityStep = 0.8 / (path.length - 2);
const opacity = 0.2 + markerIndex * opacityStep;
// DRAW history path dots
mapPaths.push(
Leaflet.circleMarker(path[markerIndex], {
radius: 3,
color: this._getColor(entityId),
opacity,
interactive: false,
})
);
// DRAW history path lines
const line = [path[markerIndex], path[markerIndex + 1]];
mapPaths.push(
Leaflet.polyline(line, {
color: this._getColor(entityId),
opacity,
interactive: false,
})
);
}
}
}
// DRAW entities
for (const entity of allEntities) {
const entityId = entity.entity;
const stateObj = hass.states[entityId];
@ -414,6 +525,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
entity-id="${entityId}"
entity-name="${entityName}"
entity-picture="${entityPicture || ""}"
entity-color="${this._getColor(entityId)}"
></ha-entity-marker>
`,
iconSize: [48, 48],
@ -428,7 +540,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#0288D1",
color: this._getColor(entityId),
radius: gpsAccuracy,
})
);
@ -437,6 +549,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this._mapItems.forEach((marker) => map.addLayer(marker));
this._mapZones.forEach((marker) => map.addLayer(marker));
this._mapPaths.forEach((marker) => map.addLayer(marker));
}
private _attachObserver(): void {
@ -455,6 +568,62 @@ class HuiMapCard extends LitElement implements LovelaceCard {
}
}
private async _getHistory(): Promise<void> {
this._date = new Date();
if (!this._configEntities) {
return;
}
const entityIds = this._configEntities!.map((entity) => entity.entity).join(
","
);
const endTime = new Date();
const startTime = new Date();
startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
const skipInitialState = false;
const significantChangesOnly = false;
const stateHistory = await fetchRecent(
this.hass,
entityIds,
startTime,
endTime,
skipInitialState,
significantChangesOnly
);
if (stateHistory.length < 1) {
return;
}
this._history = stateHistory;
}
private _cleanupHistory() {
if (!this._history) {
return;
}
if (this._config!.hours_to_show! <= 0) {
this._history = undefined;
} else {
// remove unused entities
const configEntityIds = this._configEntities?.map(
(configEntity) => configEntity.entity
);
this._history = this._history!.reduce(
(accumulator: HassEntity[][], entityStates) => {
const entityId = entityStates[0].entity_id;
if (configEntityIds?.includes(entityId)) {
accumulator.push(entityStates);
}
return accumulator;
},
[]
) as HassEntity[][];
}
}
static get styles(): CSSResult {
return css`
:host([ispanel]) ha-card {

View File

@ -119,7 +119,10 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
{
template: this._config.content,
entity_ids: this._config.entity_id,
variables: { config: this._config },
variables: {
config: this._config,
user: this._hass.user!.name,
},
}
);
this._unsubRenderTemplate.catch(() => {

View File

@ -212,7 +212,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
throw new Error("Specify an entity from within the media_player domain.");
}
this._config = { theme: "default", ...config };
this._config = config;
}
public connectedCallback(): void {

View File

@ -1,174 +1,15 @@
import {
html,
svg,
LitElement,
PropertyValues,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import "@polymer/paper-spinner/paper-spinner";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateIcon } from "../../../common/entity/state_icon";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { fetchRecent } from "../../../data/history";
import { SensorCardConfig } from "./types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entites";
import { customElement } from "lit-element";
import { HassEntity } from "home-assistant-js-websocket/dist/types";
const strokeWidth = 5;
const average = (items): number => {
return (
items.reduce((sum, entry) => sum + parseFloat(entry.state), 0) /
items.length
);
};
const lastValue = (items): number => {
return parseFloat(items[items.length - 1].state) || 0;
};
const midPoint = (
_Ax: number,
_Ay: number,
_Bx: number,
_By: number
): number[] => {
const _Zx = (_Ax - _Bx) / 2 + _Bx;
const _Zy = (_Ay - _By) / 2 + _By;
return [_Zx, _Zy];
};
const getPath = (coords: number[][]): string => {
let next;
let Z;
const X = 0;
const Y = 1;
let path = "";
let last = coords.filter(Boolean)[0];
path += `M ${last[X]},${last[Y]}`;
for (const coord of coords) {
next = coord;
Z = midPoint(last[X], last[Y], next[X], next[Y]);
path += ` ${Z[X]},${Z[Y]}`;
path += ` Q${next[X]},${next[Y]}`;
last = next;
}
path += ` ${next[X]},${next[Y]}`;
return path;
};
const calcPoints = (
history: any,
hours: number,
width: number,
detail: number,
min: number,
max: number
): number[][] => {
const coords = [] as number[][];
const height = 80;
let yRatio = (max - min) / height;
yRatio = yRatio !== 0 ? yRatio : height;
let xRatio = width / (hours - (detail === 1 ? 1 : 0));
xRatio = isFinite(xRatio) ? xRatio : width;
const first = history.filter(Boolean)[0];
let last = [average(first), lastValue(first)];
const getCoords = (item, i, offset = 0, depth = 1) => {
if (depth > 1 && item) {
return item.forEach((subItem, index) =>
getCoords(subItem, i, index, depth - 1)
);
}
const x = xRatio * (i + offset / 6);
if (item) {
last = [average(item), lastValue(item)];
}
const y =
height + strokeWidth / 2 - ((item ? last[0] : last[1]) - min) / yRatio;
return coords.push([x, y]);
};
for (let i = 0; i < history.length; i += 1) {
getCoords(history[i], i, 0, detail);
}
if (coords.length === 1) {
coords[1] = [width, coords[0][1]];
}
coords.push([width, coords[coords.length - 1][1]]);
return coords;
};
const coordinates = (
history: any,
hours: number,
width: number,
detail: number
): number[][] => {
history.forEach((item) => (item.state = Number(item.state)));
history = history.filter((item) => !Number.isNaN(item.state));
const min = Math.min.apply(
Math,
history.map((item) => item.state)
);
const max = Math.max.apply(
Math,
history.map((item) => item.state)
);
const now = new Date().getTime();
const reduce = (res, item, point) => {
const age = now - new Date(item.last_changed).getTime();
let key = Math.abs(age / (1000 * 3600) - hours);
if (point) {
key = (key - Math.floor(key)) * 60;
key = Number((Math.round(key / 10) * 10).toString()[0]);
} else {
key = Math.floor(key);
}
if (!res[key]) {
res[key] = [];
}
res[key].push(item);
return res;
};
history = history.reduce((res, item) => reduce(res, item, false), []);
if (detail > 1) {
history = history.map((entry) =>
entry.reduce((res, item) => reduce(res, item, true), [])
);
}
return calcPoints(history, hours, width, detail, min, max);
};
import { LovelaceCardEditor } from "../types";
import { HomeAssistant } from "../../../types";
import { SensorCardConfig, EntityCardConfig } from "./types";
import { GraphHeaderFooterConfig } from "../header-footer/types";
import { findEntities } from "../common/find-entites";
import { HuiEntityCard } from "./hui-entity-card";
@customElement("hui-sensor-card")
class HuiSensorCard extends LitElement implements LovelaceCard {
class HuiSensorCard extends HuiEntityCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(
/* webpackChunkName: "hui-sensor-card-editor" */ "../editor/config-elements/hui-sensor-card-editor"
@ -202,304 +43,30 @@ class HuiSensorCard extends LitElement implements LovelaceCard {
return { type: "sensor", entity: foundEntities[0] || "", graph: "line" };
}
@property() public hass?: HomeAssistant;
@property() private _config?: SensorCardConfig;
@property() private _history?: any;
private _date?: Date;
public setConfig(config: SensorCardConfig): void {
if (!config.entity || config.entity.split(".")[0] !== "sensor") {
throw new Error("Specify an entity from within the sensor domain.");
}
const cardConfig = {
detail: 1,
theme: "default",
hours_to_show: 24,
...config,
const { graph, detail, hours_to_show, ...cardConfig } = config;
const entityCardConfig: EntityCardConfig = {
...cardConfig,
type: "entity",
};
cardConfig.hours_to_show = Number(cardConfig.hours_to_show);
cardConfig.detail =
cardConfig.detail === 1 || cardConfig.detail === 2
? cardConfig.detail
: 1;
if (graph === "line") {
const footerConfig: GraphHeaderFooterConfig = {
type: "graph",
entity: config.entity,
detail: detail || 1,
hours_to_show: hours_to_show || 24,
};
this._config = cardConfig;
}
public getCardSize(): number {
return 3;
}
protected render(): TemplateResult {
if (!this._config || !this.hass) {
return html``;
entityCardConfig.footer = footerConfig;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
<hui-warning
>${this.hass.localize(
"ui.panel.lovelace.warning.entity_not_found",
"entity",
this._config.entity
)}</hui-warning
>
`;
}
let graph;
if (stateObj && this._config.graph === "line") {
if (!stateObj.attributes.unit_of_measurement) {
return html`
<hui-warning
>Entity: ${this._config.entity} - Has no Unit of Measurement and
therefore can not display a line graph.</hui-warning
>
`;
} else if (!this._history) {
graph = svg`
<svg width="100%" height="100%" viewBox="0 0 500 100"></svg>
`;
} else {
graph = svg`
<svg width="100%" height="100%" viewBox="0 0 500 100">
<g>
<mask id="fill">
<path
class='fill'
fill='white'
d="${this._history} L 500, 100 L 0, 100 z"
/>
</mask>
<rect height="100%" width="100%" id="fill-rect" fill="var(--accent-color)" mask="url(#fill)"></rect>
<mask id="line">
<path
fill="none"
stroke="var(--accent-color)"
stroke-width="${strokeWidth}"
stroke-linecap="round"
stroke-linejoin="round"
d=${this._history}
></path>
</mask>
<rect height="100%" width="100%" id="rect" fill="var(--accent-color)" mask="url(#line)"></rect>
</g>
</svg>
`;
}
} else {
graph = "";
}
return html`
<ha-card
@action=${this._handleClick}
.actionHandler=${actionHandler()}
tabindex="0"
>
<div class="flex header">
<div class="name">
<span>${this._config.name || computeStateName(stateObj)}</span>
</div>
<div class="icon">
<ha-icon
.icon="${this._config.icon || stateIcon(stateObj)}"
></ha-icon>
</div>
</div>
<div class="flex info">
<span id="value">${stateObj.state}</span>
<span id="measurement"
>${this._config.unit ||
stateObj.attributes.unit_of_measurement}</span
>
</div>
<div class="graph"><div>${graph}</div></div>
</ha-card>
`;
}
protected firstUpdated(): void {
this._date = new Date();
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_history")) {
return true;
}
return hasConfigOrEntityChanged(this, changedProps);
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| SensorCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
}
if (this._config.graph === "line") {
const minute = 60000;
if (changedProps.has("_config")) {
this._getHistory();
} else if (Date.now() - this._date!.getTime() >= minute) {
this._getHistory();
}
}
}
private _handleClick(): void {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
}
private async _getHistory(): Promise<void> {
const endTime = new Date();
const startTime = new Date();
startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
const stateHistory = await fetchRecent(
this.hass,
this._config!.entity,
startTime,
endTime
);
if (stateHistory.length < 1 || stateHistory[0].length < 1) {
return;
}
const coords = coordinates(
stateHistory[0],
this._config!.hours_to_show!,
500,
this._config!.detail!
);
this._history = getPath(coords);
this._date = new Date();
}
static get styles(): CSSResult {
return css`
:host {
display: flex;
flex-direction: column;
}
ha-card {
display: flex;
flex-direction: column;
flex: 1;
position: relative;
cursor: pointer;
overflow: hidden;
}
ha-card:focus {
outline: none;
background: var(--divider-color);
}
.flex {
display: flex;
}
.header {
margin: 8px 16px 0;
justify-content: space-between;
}
.name {
align-items: center;
display: flex;
min-width: 0;
opacity: 0.8;
position: relative;
}
.name > span {
display: block;
display: -webkit-box;
font-size: 1.2rem;
font-weight: 500;
max-height: 1.4rem;
top: 2px;
opacity: 0.8;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-wrap: break-word;
word-break: break-all;
}
.icon {
color: var(--paper-item-icon-color, #44739e);
line-height: 40px;
}
.info {
flex-wrap: wrap;
margin: 0 16px 16px;
}
#value {
display: inline-block;
font-size: 2rem;
font-weight: 400;
line-height: 1em;
margin-right: 4px;
}
#measurement {
align-self: flex-end;
display: inline-block;
font-size: 1.3rem;
line-height: 1.2em;
margin-top: 0.1em;
opacity: 0.6;
vertical-align: bottom;
}
.graph {
align-self: flex-end;
margin: auto;
margin-bottom: 0px;
position: relative;
width: 100%;
overflow: hidden;
}
.graph > div {
align-self: flex-end;
margin: auto 0px;
display: flex;
}
.fill {
opacity: 0.1;
}
`;
super.setConfig(entityCardConfig);
}
}

View File

@ -87,7 +87,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
throw new Error("Specify an entity from within the climate domain.");
}
this._config = { theme: "default", ...config };
this._config = config;
}
public connectedCallback(): void {

View File

@ -22,6 +22,11 @@ export interface EmptyStateCardConfig extends LovelaceCardConfig {
title?: string;
}
export interface EntityCardConfig extends LovelaceCardConfig {
attribute?: string;
unit?: string;
}
export interface EntitiesCardEntityConfig extends EntityConfig {
type?: string;
secondary_info?: "entity-id" | "last-changed";
@ -133,6 +138,9 @@ export interface LightCardConfig extends LovelaceCardConfig {
name?: string;
theme?: string;
icon?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export interface MapCardConfig extends LovelaceCardConfig {
@ -141,6 +149,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
aspect_ratio?: string;
default_zoom?: number;
entities?: Array<EntityConfig | string>;
hours_to_show?: number;
geo_location_sources?: string[];
dark_mode?: boolean;
}

View File

@ -1,12 +0,0 @@
import { HomeAssistant } from "../../../types";
import { CallServiceConfig } from "../entity-rows/types";
export const callService = (
config: CallServiceConfig,
hass: HomeAssistant
): void => {
const entityId = config.entity;
const [domain, service] = config.service.split(".", 2);
const serviceData = { entity_id: entityId, ...config.service_data };
hass.callService(domain, service, serviceData);
};

View File

@ -30,26 +30,19 @@ export class HuiConditionalBase extends UpdatingElement {
throw new Error("Conditions are invalid.");
}
if (this._element && this._element.parentElement) {
this.removeChild(this._element);
}
this._config = config;
this.style.display = "none";
}
protected update(): void {
if (!this._element || !this.hass) {
if (!this._element || !this.hass || !this._config) {
return;
}
const visible =
this._config && checkConditionsMet(this._config.conditions, this.hass);
const visible = checkConditionsMet(this._config.conditions, this.hass);
if (visible) {
this._element.hass = this.hass;
if (!this._element.parentElement) {
this.appendChild(this._element);
}
}
this.style.setProperty("display", visible ? "" : "none");

View File

@ -1,4 +1,5 @@
import "../cards/hui-entities-card";
import "../cards/hui-entity-card";
import "../cards/hui-button-card";
import "../cards/hui-entity-button-card";
import "../cards/hui-glance-card";
@ -16,6 +17,7 @@ import {
} from "./create-element-base";
const ALWAYS_LOADED_TYPES = new Set([
"entity",
"entities",
"button",
"entity-button",

View File

@ -17,8 +17,8 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
import { CUSTOM_TYPE_PREFIX } from "../../../data/lovelace_custom_cards";
const CUSTOM_TYPE_PREFIX = "custom:";
const TIMEOUT = 2000;
interface CreateElementConfigTypes {

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