3
.gitignore
vendored
@ -31,3 +31,6 @@ src/cast/dev_const.ts
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
yarn-error.log
|
||||
|
||||
#asdf
|
||||
.tool-versions
|
||||
|
@ -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
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 805 B After Width: | Height: | Size: 803 B |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 160 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 781 B After Width: | Height: | Size: 375 B |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
@ -16,7 +16,8 @@ class DemoCard extends PolymerElement {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
#card {
|
||||
width: 400px;
|
||||
max-width: 400px;
|
||||
width: 100vw;
|
||||
}
|
||||
pre {
|
||||
width: 400px;
|
||||
|
@ -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 |
BIN
public/static/images/weather/cloudy.png
Normal file
After Width: | Height: | Size: 715 B |
BIN
public/static/images/weather/lightning-rainy.png
Normal file
After Width: | Height: | Size: 910 B |
BIN
public/static/images/weather/lightning.png
Normal file
After Width: | Height: | Size: 822 B |
BIN
public/static/images/weather/night.png
Normal file
After Width: | Height: | Size: 465 B |
BIN
public/static/images/weather/partly-cloudy.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
public/static/images/weather/pouring.png
Normal file
After Width: | Height: | Size: 862 B |
BIN
public/static/images/weather/rainy.png
Normal file
After Width: | Height: | Size: 818 B |
BIN
public/static/images/weather/snowy.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/static/images/weather/sunny.png
Normal file
After Width: | Height: | Size: 678 B |
BIN
public/static/images/weather/windy.png
Normal file
After Width: | Height: | Size: 817 B |
2
setup.py
@ -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",
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
};
|
||||
|
23
src/data/lovelace_custom_cards.ts
Normal 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
@ -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] || "";
|
||||
}
|
||||
};
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
`;
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -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];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
243
src/panels/lovelace/cards/hui-entity-card.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
: [];
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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(() => {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
@ -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");
|
||||
|
@ -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",
|
||||
|
@ -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 {
|
||||
|