Merge pull request #2917 from home-assistant/dev

20190312.0
This commit is contained in:
Paulus Schoutsen 2019-03-12 11:28:16 -07:00 committed by GitHub
commit cd94442455
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
151 changed files with 4546 additions and 2517 deletions

View File

@ -13,6 +13,10 @@
**Last working Home Assistant release (if known):**
**UI (States or Lovelace UI?):**
<!--
- Frontend -> Developer tools -> Info
-->
**Browser and Operating System:**
<!--

View File

@ -1,6 +1,6 @@
const webpack = require("webpack");
const path = require("path");
const BabelMinifyPlugin = require("babel-minify-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
module.exports.resolve = {
extensions: [".ts", ".js", ".json", ".tsx"],
@ -29,32 +29,15 @@ module.exports.plugins = [
),
];
module.exports.optimization = {
module.exports.optimization = (latestBuild) => ({
minimizer: [
// Took options from Polymer build tool
// https://github.com/Polymer/tools/blob/master/packages/build/src/js-transform.ts
new BabelMinifyPlugin(
{
// Disable the minify-constant-folding plugin because it has a bug relating
// to invalid substitution of constant values into export specifiers:
// https://github.com/babel/minify/issues/820
evaluate: false,
// TODO(aomarks) Find out why we disabled this plugin.
simplifyComparisons: false,
// Prevent removal of things that babel thinks are unreachable, but sometimes
// gets wrong: https://github.com/Polymer/tools/issues/724
deadcode: false,
// Disable the simplify plugin because it can eat some statements preceeding
// loops. https://github.com/babel/minify/issues/824
simplify: false,
// This is breaking ES6 output. https://github.com/Polymer/tools/issues/261
mangle: false,
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
terserOptions: {
ecma: latestBuild ? undefined : 5,
},
{}
),
}),
],
};
});

View File

@ -39,7 +39,7 @@ module.exports = {
},
],
},
optimization: webpackBase.optimization,
optimization: webpackBase.optimization(latestBuild),
plugins: [
new webpack.DefinePlugin({
__DEV__: false,
@ -74,6 +74,7 @@ module.exports = {
new WorkboxPlugin.GenerateSW({
swDest: "service_worker_es5.js",
importWorkboxFrom: "local",
include: [],
}),
].filter(Boolean),
resolve: webpackBase.resolve,

View File

@ -7,6 +7,7 @@ const isProd = process.env.NODE_ENV === "production";
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
const buildPath = path.resolve(__dirname, "dist");
const publicPath = isProd ? "./" : "http://localhost:8080/";
const latestBuild = true;
module.exports = {
mode: isProd ? "production" : "development",
@ -16,7 +17,7 @@ module.exports = {
entry: "./src/entrypoint.js",
module: {
rules: [
babelLoaderConfig({ latestBuild: true }),
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
@ -32,7 +33,7 @@ module.exports = {
},
],
},
optimization: webpackBase.optimization,
optimization: webpackBase.optimization(latestBuild),
plugins: [
new CopyWebpackPlugin([
"public",
@ -51,15 +52,6 @@ module.exports = {
to: "static/images/leaflet/",
},
]),
isProd &&
new UglifyJsPlugin({
extractComments: true,
sourceMap: true,
uglifyOptions: {
// Disabling because it broke output
mangle: false,
},
}),
].filter(Boolean),
resolve: webpackBase.resolve,
output: {

View File

@ -1,11 +1,12 @@
const CompressionPlugin = require("compression-webpack-plugin");
const config = require("./config.js");
const { babelLoaderConfig } = require("../config/babel.js");
const { minimizer } = require("../config/babel.js");
const webpackBase = require("../config/webpack.js");
const isProdBuild = process.env.NODE_ENV === "production";
const isCI = process.env.CI === "true";
const chunkFilename = isProdBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const latestBuild = false;
module.exports = {
mode: isProdBuild ? "production" : "development",
@ -15,7 +16,7 @@ module.exports = {
},
module: {
rules: [
babelLoaderConfig({ latestBuild: false }),
babelLoaderConfig({ latestBuild }),
{
test: /\.(html)$/,
use: {
@ -27,9 +28,7 @@ module.exports = {
},
],
},
optimization: {
minimizer,
},
optimization: webpackBase.optimization(latestBuild),
plugins: [
isProdBuild &&
!isCI &&

View File

@ -73,7 +73,8 @@
"deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0",
"fecha": "^3.0.0",
"home-assistant-js-websocket": "^3.2.4",
"hls.js": "^0.12.3",
"home-assistant-js-websocket": "^3.3.0",
"intl-messageformat": "^2.2.0",
"jquery": "^3.3.1",
"js-yaml": "^3.12.0",
@ -107,12 +108,12 @@
"@gfx/zopfli": "^1.0.9",
"@types/chai": "^4.1.7",
"@types/codemirror": "^0.0.71",
"@types/hls.js": "^0.12.2",
"@types/leaflet": "^1.4.3",
"@types/memoize-one": "^4.1.0",
"@types/mocha": "^5.2.5",
"babel-eslint": "^10",
"babel-loader": "^8.0.4",
"babel-minify-webpack-plugin": "^0.3.1",
"chai": "^4.2.0",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.5.2",
@ -146,6 +147,7 @@
"reify": "^0.18.1",
"require-dir": "^1.0.0",
"sinon": "^7.1.1",
"terser-webpack-plugin": "^1.2.3",
"ts-mocha": "^2.0.0",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0",

View File

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

View File

@ -78,6 +78,7 @@ export const DOMAINS_TOGGLE = new Set([
"light",
"switch",
"group",
"automation",
]);
/** Temperature units. */

View File

@ -53,7 +53,7 @@ class StateBadge extends LitElement {
};
if (stateObj) {
// hide icon if we have entity picture
if (stateObj.attributes.entity_picture) {
if (stateObj.attributes.entity_picture && !this.overrideIcon) {
hostStyle.backgroundImage =
"url(" + stateObj.attributes.entity_picture + ")";
iconStyle.display = "none";

View File

@ -22,6 +22,7 @@ import { DEFAULT_PANEL } from "../common/const";
const computeUrl = (urlPath) => `/${urlPath}`;
const computePanels = (hass: HomeAssistant) => {
const isAdmin = hass.user.is_admin;
const panels = hass.panels;
const sortValue = {
map: 1,
@ -30,9 +31,9 @@ const computePanels = (hass: HomeAssistant) => {
};
const result: Panel[] = [];
Object.keys(panels).forEach((key) => {
if (panels[key].title) {
result.push(panels[key]);
Object.values(panels).forEach((panel) => {
if (panel.title && (panel.component_name !== "config" || isAdmin)) {
result.push(panel);
}
});
@ -129,62 +130,66 @@ class HaSidebar extends LitElement {
: html``}
</paper-listbox>
<div>
<div class="divider"></div>
${!hass.user.is_admin
? ""
: html`
<div>
<div class="divider"></div>
<div class="subheader">
${hass.localize("ui.sidebar.developer_tools")}
</div>
<div class="subheader">
${hass.localize("ui.sidebar.developer_tools")}
</div>
<div class="dev-tools">
<a href="/dev-service" tabindex="-1">
<paper-icon-button
icon="hass:remote"
alt="${hass.localize("panel.dev-services")}"
title="${hass.localize("panel.dev-services")}"
></paper-icon-button>
</a>
<a href="/dev-state" tabindex="-1">
<paper-icon-button
icon="hass:code-tags"
alt="${hass.localize("panel.dev-states")}"
title="${hass.localize("panel.dev-states")}"
></paper-icon-button>
</a>
<a href="/dev-event" tabindex="-1">
<paper-icon-button
icon="hass:radio-tower"
alt="${hass.localize("panel.dev-events")}"
title="${hass.localize("panel.dev-events")}"
></paper-icon-button>
</a>
<a href="/dev-template" tabindex="-1">
<paper-icon-button
icon="hass:file-xml"
alt="${hass.localize("panel.dev-templates")}"
title="${hass.localize("panel.dev-templates")}"
></paper-icon-button>
</a>
${isComponentLoaded(hass, "mqtt")
? html`
<a href="/dev-mqtt" tabindex="-1">
<div class="dev-tools">
<a href="/dev-service" tabindex="-1">
<paper-icon-button
icon="hass:altimeter"
alt="${hass.localize("panel.dev-mqtt")}"
title="${hass.localize("panel.dev-mqtt")}"
icon="hass:remote"
alt="${hass.localize("panel.dev-services")}"
title="${hass.localize("panel.dev-services")}"
></paper-icon-button>
</a>
`
: html``}
<a href="/dev-info" tabindex="-1">
<paper-icon-button
icon="hass:information-outline"
alt="${hass.localize("panel.dev-info")}"
title="${hass.localize("panel.dev-info")}"
></paper-icon-button>
</a>
</div>
</div>
<a href="/dev-state" tabindex="-1">
<paper-icon-button
icon="hass:code-tags"
alt="${hass.localize("panel.dev-states")}"
title="${hass.localize("panel.dev-states")}"
></paper-icon-button>
</a>
<a href="/dev-event" tabindex="-1">
<paper-icon-button
icon="hass:radio-tower"
alt="${hass.localize("panel.dev-events")}"
title="${hass.localize("panel.dev-events")}"
></paper-icon-button>
</a>
<a href="/dev-template" tabindex="-1">
<paper-icon-button
icon="hass:file-xml"
alt="${hass.localize("panel.dev-templates")}"
title="${hass.localize("panel.dev-templates")}"
></paper-icon-button>
</a>
${isComponentLoaded(hass, "mqtt")
? html`
<a href="/dev-mqtt" tabindex="-1">
<paper-icon-button
icon="hass:altimeter"
alt="${hass.localize("panel.dev-mqtt")}"
title="${hass.localize("panel.dev-mqtt")}"
></paper-icon-button>
</a>
`
: html``}
<a href="/dev-info" tabindex="-1">
<paper-icon-button
icon="hass:information-outline"
alt="${hass.localize("panel.dev-info")}"
title="${hass.localize("panel.dev-info")}"
></paper-icon-button>
</a>
</div>
</div>
`}
`;
}

View File

@ -8,7 +8,7 @@ import {
customElement,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { User } from "../../data/auth";
import { User } from "../../data/user";
import { CurrentUser } from "../../types";
const computeInitials = (name: string) => {

View File

@ -15,7 +15,7 @@ import {
} from "lit-element";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { User, fetchUsers } from "../../data/auth";
import { User, fetchUsers } from "../../data/user";
import compare from "../../common/string/compare";
class HaEntityPicker extends LitElement {

View File

@ -1,26 +1,9 @@
import { HomeAssistant } from "../types";
export interface AuthProvider {
name: string;
id: string;
type: string;
}
interface Credential {
export interface Credential {
type: string;
}
export interface User {
id: string;
name: string;
is_owner: boolean;
is_active: boolean;
system_generated: boolean;
group_ids: string[];
credentials: Credential[];
}
export const fetchUsers = async (hass: HomeAssistant) =>
hass.callWS<User[]>({
type: "config/auth/list",
});

View File

@ -1,12 +1,37 @@
import { HomeAssistant } from "../types";
import { HomeAssistant, CameraEntity } from "../types";
export interface CameraThumbnail {
content_type: string;
content: string;
}
export interface Stream {
url: string;
}
export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
`/api/camera_proxy_stream/${entity.entity_id}?token=${
entity.attributes.access_token
}`;
export const fetchThumbnail = (hass: HomeAssistant, entityId: string) =>
hass.callWS<CameraThumbnail>({
type: "camera_thumbnail",
entity_id: entityId,
});
export const fetchStreamUrl = (
hass: HomeAssistant,
entityId: string,
format?: "hls"
) => {
const data = {
type: "camera/stream",
entity_id: entityId,
};
if (format) {
// @ts-ignore
data.format = format;
}
return hass.callWS<Stream>(data);
};

View File

@ -1,5 +1,45 @@
import { HomeAssistant } from "../types";
export interface EntityFilter {
include_domains: string[];
include_entities: string[];
exclude_domains: string[];
exclude_entities: string[];
}
interface CloudStatusBase {
logged_in: boolean;
cloud: "disconnected" | "connecting" | "connected";
}
interface CertificateInformation {
common_name: string;
expire_date: string;
fingerprint: string;
}
export type CloudStatusLoggedIn = CloudStatusBase & {
email: string;
google_entities: EntityFilter;
google_domains: string[];
alexa_entities: EntityFilter;
alexa_domains: string[];
prefs: {
google_enabled: boolean;
alexa_enabled: boolean;
google_allow_unlock: boolean;
cloudhooks: { [webhookId: string]: CloudWebhook };
};
remote_domain: string | undefined;
remote_connected: boolean;
remote_certificate: undefined | CertificateInformation;
};
export type CloudStatus = CloudStatusBase | CloudStatusLoggedIn;
export interface SubscriptionInfo {
human_description: string;
}
export interface CloudWebhook {
webhook_id: string;
cloudhook_id: string;
@ -18,3 +58,29 @@ export const deleteCloudhook = (hass: HomeAssistant, webhookId: string) =>
type: "cloud/cloudhook/delete",
webhook_id: webhookId,
});
export const connectCloudRemote = (hass: HomeAssistant) =>
hass.callWS({
type: "cloud/remote/connect",
});
export const disconnectCloudRemote = (hass: HomeAssistant) =>
hass.callWS({
type: "cloud/remote/disconnect",
});
export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) =>
hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" });
export const updateCloudPref = (
hass: HomeAssistant,
prefs: {
google_enabled?: boolean;
alexa_enabled?: boolean;
google_allow_unlock?: boolean;
}
) =>
hass.callWS({
type: "cloud/update_prefs",
...prefs,
});

View File

@ -10,10 +10,12 @@ export interface DeviceRegistryEntry {
sw_version?: string;
hub_device_id?: string;
area_id?: string;
name_by_user?: string;
}
export interface DeviceRegistryEntryMutableParams {
area_id: string;
area_id?: string;
name_by_user?: string;
}
export const fetchDeviceRegistry = (hass: HomeAssistant) =>

36
src/data/frontend.ts Normal file
View File

@ -0,0 +1,36 @@
import { HomeAssistant } from "../types";
declare global {
// tslint:disable-next-line
interface FrontendUserData {}
}
export type ValidUserDataKey = keyof FrontendUserData;
export const fetchFrontendUserData = async <
UserDataKey extends ValidUserDataKey
>(
hass: HomeAssistant,
key: UserDataKey
): Promise<FrontendUserData[UserDataKey] | null> => {
const result = await hass.callWS<{
value: FrontendUserData[UserDataKey] | null;
}>({
type: "frontend/get_user_data",
key,
});
return result.value;
};
export const saveFrontendUserData = async <
UserDataKey extends ValidUserDataKey
>(
hass: HomeAssistant,
key: UserDataKey,
value: FrontendUserData[UserDataKey]
): Promise<void> =>
hass.callWS<void>({
type: "frontend/set_user_data",
key,
value,
});

24
src/data/onboarding.ts Normal file
View File

@ -0,0 +1,24 @@
import { handleFetchPromise } from "../util/hass-call-api";
export interface OnboardingStep {
step: string;
done: boolean;
}
interface UserStepResponse {
auth_code: string;
}
export const onboardUserStep = async (params: {
client_id: string;
name: string;
username: string;
password: string;
}) =>
handleFetchPromise<UserStepResponse>(
fetch("/api/onboarding/users", {
method: "POST",
credentials: "same-origin",
body: JSON.stringify(params),
})
);

31
src/data/translation.ts Normal file
View File

@ -0,0 +1,31 @@
import { HomeAssistant } from "../types";
import { fetchFrontendUserData, saveFrontendUserData } from "./frontend";
export interface FrontendTranslationData {
language: string;
}
declare global {
interface FrontendUserData {
language: FrontendTranslationData;
}
}
export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass, "language");
export const saveTranslationPreferences = (
hass: HomeAssistant,
data: FrontendTranslationData
) => saveFrontendUserData(hass, "language", data);
export const getHassTranslations = async (
hass: HomeAssistant,
language: string
): Promise<{}> => {
const result = await hass.callWS<{ resources: {} }>({
type: "frontend/get_translations",
language,
});
return result.resources;
};

49
src/data/user.ts Normal file
View File

@ -0,0 +1,49 @@
import { HomeAssistant } from "../types";
import { Credential } from "./auth";
export const SYSTEM_GROUP_ID_ADMIN = "system-admin";
export const SYSTEM_GROUP_ID_USER = "system-users";
export const SYSTEM_GROUP_ID_READ_ONLY = "system-read-only";
export interface User {
id: string;
name: string;
is_owner: boolean;
is_active: boolean;
system_generated: boolean;
group_ids: string[];
credentials: Credential[];
}
interface UpdateUserParams {
name?: User["name"];
group_ids?: User["group_ids"];
}
export const fetchUsers = async (hass: HomeAssistant) =>
hass.callWS<User[]>({
type: "config/auth/list",
});
export const createUser = async (hass: HomeAssistant, name: string) =>
hass.callWS<{ user: User }>({
type: "config/auth/create",
name,
});
export const updateUser = async (
hass: HomeAssistant,
userId: string,
params: UpdateUserParams
) =>
hass.callWS<{ user: User }>({
...params,
type: "config/auth/update",
user_id: userId,
});
export const deleteUser = async (hass: HomeAssistant, userId: string) =>
hass.callWS<void>({
type: "config/auth/delete",
user_id: userId,
});

View File

@ -1,12 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
export interface ZHADeviceEntity extends HassEntity {
device_info?: {
identifiers: any[];
};
}
export interface ZHAEntityReference extends HassEntity {
name: string;
}
@ -20,6 +14,8 @@ export interface ZHADevice {
quirk_class: string;
entities: ZHAEntityReference[];
manufacturer_code: number;
device_reg_id: string;
user_given_name: string;
}
export interface Attribute {
@ -78,6 +74,37 @@ export const fetchDevices = (hass: HomeAssistant): Promise<ZHADevice[]> =>
type: "zha/devices",
});
export const fetchBindableDevices = (
hass: HomeAssistant,
ieeeAddress: string
): Promise<ZHADevice[]> =>
hass.callWS({
type: "zha/devices/bindable",
ieee: ieeeAddress,
});
export const bindDevices = (
hass: HomeAssistant,
sourceIEEE: string,
targetIEEE: string
): Promise<void> =>
hass.callWS({
type: "zha/devices/bind",
source_ieee: sourceIEEE,
target_ieee: targetIEEE,
});
export const unbindDevices = (
hass: HomeAssistant,
sourceIEEE: string,
targetIEEE: string
): Promise<void> =>
hass.callWS({
type: "zha/devices/unbind",
source_ieee: sourceIEEE,
target_ieee: targetIEEE,
});
export const readAttributeValue = (
hass: HomeAssistant,
data: ReadAttributeServiceData

View File

@ -1,85 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import computeStateName from "../../../common/entity/compute_state_name";
import emptyImageBase64 from "../../../common/empty_image_base64";
import EventsMixin from "../../../mixins/events-mixin";
/*
* @appliesMixin EventsMixin
*/
class MoreInfoCamera extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
:host {
max-width: 640px;
}
.camera-image {
width: 100%;
}
</style>
<img
class="camera-image"
src="[[computeCameraImageUrl(hass, stateObj, isVisible)]]"
on-load="imageLoaded"
alt="[[_computeStateName(stateObj)]]"
/>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
stateObj: {
type: Object,
},
isVisible: {
type: Boolean,
value: true,
},
};
}
connectedCallback() {
super.connectedCallback();
this.isVisible = true;
}
disconnectedCallback() {
this.isVisible = false;
super.disconnectedCallback();
}
imageLoaded() {
this.fire("iron-resize");
}
_computeStateName(stateObj) {
return computeStateName(stateObj);
}
computeCameraImageUrl(hass, stateObj, isVisible) {
if (hass.demo) {
return "/demo/webcam.jpg";
}
if (stateObj && isVisible) {
return (
"/api/camera_proxy_stream/" +
stateObj.entity_id +
"?token=" +
stateObj.attributes.access_token
);
}
// Return an empty image if no stateObj (= dialog not open) or in cleanup mode.
return emptyImageBase64;
}
}
customElements.define("more-info-camera", MoreInfoCamera);

View File

@ -0,0 +1,132 @@
import { property, UpdatingElement, PropertyValues } from "lit-element";
import computeStateName from "../../../common/entity/compute_state_name";
import { HomeAssistant, CameraEntity } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { fetchStreamUrl, computeMJPEGStreamUrl } from "../../../data/camera";
type HLSModule = typeof import("hls.js");
class MoreInfoCamera extends UpdatingElement {
@property() public hass?: HomeAssistant;
@property() public stateObj?: CameraEntity;
public disconnectedCallback() {
super.disconnectedCallback();
this._teardownPlayback();
}
protected updated(changedProps: PropertyValues) {
if (!changedProps.has("stateObj")) {
return;
}
const oldState = changedProps.get("stateObj") as this["stateObj"];
const oldEntityId = oldState ? oldState.entity_id : undefined;
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
// Same entity, ignore.
if (curEntityId === oldEntityId) {
return;
}
// Tear down if we have something and we need to build it up
if (oldEntityId) {
this._teardownPlayback();
}
if (curEntityId) {
this._startPlayback();
}
}
private async _startPlayback(): Promise<void> {
if (!this.stateObj) {
return;
}
const videoEl = document.createElement("video");
videoEl.style.width = "100%";
videoEl.autoplay = true;
videoEl.controls = true;
videoEl.muted = true;
// tslint:disable-next-line
let Hls: HLSModule | undefined;
let hlsSupported =
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
if (!hlsSupported) {
Hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
hlsSupported = Hls.isSupported();
}
if (hlsSupported) {
try {
const { url } = await fetchStreamUrl(
this.hass!,
this.stateObj.entity_id
);
if (Hls) {
this._renderHLSPolyfill(videoEl, Hls, url);
} else {
this._renderHLSNative(videoEl, url);
}
return;
} catch (err) {
// Fails if entity doesn't support it. In that case we go
// for mjpeg.
}
}
this._renderMJPEG();
}
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
videoEl.src = url;
await new Promise((resolve) =>
videoEl.addEventListener("loadedmetadata", resolve)
);
videoEl.play();
}
private async _renderHLSPolyfill(
videoEl: HTMLVideoElement,
// tslint:disable-next-line
Hls: HLSModule,
url: string
) {
const hls = new Hls();
await new Promise((resolve) => {
hls.on(Hls.Events.MEDIA_ATTACHED, resolve);
hls.attachMedia(videoEl);
});
hls.loadSource(url);
this.appendChild(videoEl);
videoEl.addEventListener("loadeddata", () =>
fireEvent(this, "iron-resize")
);
}
private _renderMJPEG() {
const img = document.createElement("img");
img.style.width = "100%";
img.addEventListener("load", () => fireEvent(this, "iron-resize"));
img.src = __DEMO__
? "/demo/webcamp.jpg"
: computeMJPEGStreamUrl(this.stateObj!);
img.alt = computeStateName(this.stateObj!);
this.appendChild(img);
}
private _teardownPlayback(): any {
while (this.lastChild) {
this.removeChild(this.lastChild);
}
}
}
customElements.define("more-info-camera", MoreInfoCamera);

View File

@ -1,3 +0,0 @@
import "../components/ha-iconset-svg";
import "../resources/roboto";
import "../onboarding/ha-onboarding";

View File

@ -0,0 +1,10 @@
import "../components/ha-iconset-svg";
import "../resources/ha-style";
import "../resources/roboto";
import "../onboarding/ha-onboarding";
declare global {
interface Window {
stepsPromise: Promise<Response>;
}
}

View File

@ -7,7 +7,7 @@ import { demoPanels } from "./demo_panels";
import { getEntity, Entity } from "./entity";
import { HomeAssistant } from "../types";
import { HassEntities } from "home-assistant-js-websocket";
import { getActiveTranslation } from "../util/hass-translation";
import { getLocalLanguage } from "../util/hass-translation";
import { translationMetadata } from "../resources/translations-metadata";
const ensureArray = <T>(val: T | T[]): T[] =>
@ -87,6 +87,8 @@ export const provideHass = (
);
});
const localLanguage = getLocalLanguage();
const hassObj: MockHomeAssistant = {
// Home Assistant properties
auth: {} as any,
@ -128,13 +130,15 @@ export const provideHass = (
user: {
credentials: [],
id: "abcd",
is_admin: true,
is_owner: true,
mfa_modules: [],
name: "Demo User",
},
panelUrl: "lovelace",
language: getActiveTranslation(),
language: localLanguage,
selectedLanguage: localLanguage,
resources: null as any,
localize: () => "",

View File

@ -12,7 +12,7 @@ import LocalizeMixin from "../../mixins/localize-mixin";
import EventsMixin from "../../mixins/events-mixin";
import { getState } from "../../util/ha-pref-storage";
import { getActiveTranslation } from "../../util/hass-translation";
import { getLocalLanguage } from "../../util/hass-translation";
import { fetchWithAuth } from "../../util/fetch-with-auth";
import hassCallApi from "../../util/hass-call-api";
import { subscribePanels } from "../../data/ws-panels";
@ -49,7 +49,7 @@ export default (superClass) =>
user: null,
panelUrl: this._panelUrl,
language: getActiveTranslation(),
language: getLocalLanguage(),
// If resources are already loaded, don't discard them
resources: (this.hass && this.hass.resources) || null,
localize: () => "",

View File

@ -24,7 +24,7 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
super.firstUpdated(changedProps);
this.addEventListener("hass-dock-sidebar", (ev) => {
this._updateHass({ dockedSidebar: ev.detail.dock });
storeState(this.hass);
storeState(this.hass!);
});
}
};

View File

@ -1,32 +1,42 @@
import applyThemesOnElement from "../../common/dom/apply_themes_on_element";
import { storeState } from "../../util/ha-pref-storage";
import { subscribeThemes } from "../../data/ws-themes";
import { Constructor, LitElement } from "lit-element";
import { HassBaseEl } from "./hass-base-mixin";
import { HASSDomEvent } from "../../common/dom/fire_event";
export default (superClass) =>
declare global {
// for add event listener
interface HTMLElementEventMap {
settheme: HASSDomEvent<string>;
}
}
export default (superClass: Constructor<LitElement & HassBaseEl>) =>
class extends superClass {
firstUpdated(changedProps) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.addEventListener("settheme", (ev) => {
this._updateHass({ selectedTheme: ev.detail });
this._applyTheme();
storeState(this.hass);
storeState(this.hass!);
});
}
hassConnected() {
protected hassConnected() {
super.hassConnected();
subscribeThemes(this.hass.connection, (themes) => {
subscribeThemes(this.hass!.connection, (themes) => {
this._updateHass({ themes });
this._applyTheme();
});
}
_applyTheme() {
private _applyTheme() {
applyThemesOnElement(
document.documentElement,
this.hass.themes,
this.hass.selectedTheme,
this.hass!.themes,
this.hass!.selectedTheme,
true
);
}

View File

@ -1,11 +1,17 @@
import { translationMetadata } from "../../resources/translations-metadata";
import { getTranslation } from "../../util/hass-translation";
import { storeState } from "../../util/ha-pref-storage";
import {
getTranslation,
getLocalLanguage,
getUserLanguage,
} from "../../util/hass-translation";
import { Constructor, LitElement } from "lit-element";
import { HassBaseEl } from "./hass-base-mixin";
import { computeLocalize } from "../../common/translations/localize";
import { computeRTL } from "../../common/util/compute_rtl";
import { HomeAssistant } from "../../types";
import { saveFrontendUserData } from "../../data/frontend";
import { storeState } from "../../util/ha-pref-storage";
import { getHassTranslations } from "../../data/translation";
/*
* superClass needs to contain `this.hass` and `this._updateHass`.
@ -16,60 +22,86 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.addEventListener("hass-language-select", (e) =>
this._selectLanguage(e)
this._selectLanguage((e as CustomEvent).detail.language, true)
);
this._loadResources();
this._loadCoreTranslations(getLocalLanguage());
}
protected hassConnected() {
super.hassConnected();
this._loadBackendTranslations();
this.style.direction = computeRTL(this.hass!) ? "rtl" : "ltr";
getUserLanguage(this.hass!).then((language) => {
if (language && this.hass!.language !== language) {
// We just get language from backend, no need to save back
this._selectLanguage(language, false);
}
});
this._applyTranslations(this.hass!);
}
protected hassReconnected() {
super.hassReconnected();
this._loadBackendTranslations();
this._applyTranslations(this.hass!);
}
protected panelUrlChanged(newPanelUrl) {
super.panelUrlChanged(newPanelUrl);
this._loadTranslationFragment(newPanelUrl);
// this may be triggered before hassConnected
this._loadFragmentTranslations(
this.hass ? this.hass.language : getLocalLanguage(),
newPanelUrl
);
}
private async _loadBackendTranslations() {
const hass = this.hass;
if (!hass || !hass.language) {
private _selectLanguage(language: string, saveToBackend: boolean) {
if (!this.hass) {
// should not happen, do it to avoid use this.hass!
return;
}
const language = hass.selectedLanguage || hass.language;
// update selectedLanguage so that it can be saved to local storage
this._updateHass({ language, selectedLanguage: language });
storeState(this.hass);
if (saveToBackend) {
saveFrontendUserData(this.hass, "language", { language });
}
const { resources } = await hass.callWS({
type: "frontend/get_translations",
language,
});
this._applyTranslations(this.hass);
}
// If we've switched selected languages just ignore this response
if ((hass.selectedLanguage || hass.language) !== language) {
private _applyTranslations(hass: HomeAssistant) {
this.style.direction = computeRTL(hass) ? "rtl" : "ltr";
this._loadCoreTranslations(hass.language);
this._loadHassTranslations(hass.language);
this._loadFragmentTranslations(hass.language, hass.panelUrl);
}
private async _loadHassTranslations(language: string) {
const resources = await getHassTranslations(this.hass!, language);
// Ignore the repsonse if user switched languages before we got response
if (this.hass!.language !== language) {
return;
}
this._updateResources(language, resources);
}
private _loadTranslationFragment(panelUrl) {
private async _loadFragmentTranslations(
language: string,
panelUrl: string
) {
if (translationMetadata.fragments.includes(panelUrl)) {
this._loadResources(panelUrl);
const result = await getTranslation(panelUrl, language);
this._updateResources(result.language, result.data);
}
}
private async _loadResources(fragment?) {
const result = await getTranslation(fragment);
private async _loadCoreTranslations(language: string) {
const result = await getTranslation(null, language);
this._updateResources(result.language, result.data);
}
private _updateResources(language, data) {
private _updateResources(language: string, data: any) {
// Update the language in hass, and update the resources with the newly
// loaded resources. This merges the new data on top of the old data for
// this language, so that the full translation set can be loaded across
@ -88,14 +120,4 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
}
this._updateHass(changes);
}
private _selectLanguage(event) {
const language: string = event.detail.language;
this._updateHass({ language, selectedLanguage: language });
this.style.direction = computeRTL(this.hass!) ? "rtl" : "ltr";
storeState(this.hass);
this._loadResources();
this._loadBackendTranslations();
this._loadTranslationFragment(this.hass!.panelUrl);
}
};

View File

@ -4,7 +4,7 @@ import {
PropertyDeclarations,
PropertyValues,
} from "lit-element";
import { getActiveTranslation } from "../util/hass-translation";
import { getLocalLanguage } from "../util/hass-translation";
import { localizeLiteBaseMixin } from "./localize-lite-base-mixin";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
@ -35,7 +35,8 @@ export const litLocalizeLiteMixin = <T extends LitElement>(
super();
// This will prevent undefined errors if called before connected to DOM.
this.localize = empty;
this.language = getActiveTranslation();
// Use browser language setup before login.
this.language = getLocalLanguage();
}
public connectedCallback(): void {

View File

@ -35,7 +35,10 @@ export const localizeLiteBaseMixin = (superClass) =>
}
private async _updateResources() {
const { language, data } = await getTranslation(this.translationFragment);
const { language, data } = await getTranslation(
this.translationFragment,
this.language
);
this.resources = {
[language]: data,
};

View File

@ -2,7 +2,7 @@
* Lite mixin to add localization without depending on the Hass object.
*/
import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin";
import { getActiveTranslation } from "../util/hass-translation";
import { getLocalLanguage } from "../util/hass-translation";
import { localizeLiteBaseMixin } from "./localize-lite-base-mixin";
import { computeLocalize } from "../common/translations/localize";
@ -16,7 +16,8 @@ export const localizeLiteMixin = dedupingMixin(
return {
language: {
type: String,
value: getActiveTranslation(),
// Use browser language setup before login.
value: getLocalLanguage(),
},
resources: Object,
// The fragment to load.

View File

@ -1,175 +0,0 @@
import "@polymer/polymer/lib/elements/dom-if";
import "@polymer/polymer/lib/elements/dom-repeat";
import "@polymer/paper-input/paper-input";
import "@material/mwc-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { localizeLiteMixin } from "../mixins/localize-lite-mixin";
class HaOnboarding extends localizeLiteMixin(PolymerElement) {
static get template() {
return html`
<style>
.error {
color: red;
}
.action {
margin: 32px 0;
text-align: center;
}
</style>
<p>
[[localize('ui.panel.page-onboarding.intro')]]
</p>
<p>
[[localize('ui.panel.page-onboarding.user.intro')]]
</p>
<template is='dom-if' if='[[_errorMsg]]'>
<p class='error'>[[_computeErrorMsg(localize, _errorMsg)]]</p>
</template>
<form>
<paper-input
autofocus
label="[[localize('ui.panel.page-onboarding.user.data.name')]]"
value='{{_name}}'
required
auto-validate
autocapitalize='on'
error-message="[[localize('ui.panel.page-onboarding.user.required_field')]]"
on-blur='_maybePopulateUsername'
></paper-input>
<paper-input
label="[[localize('ui.panel.page-onboarding.user.data.username')]]"
value='{{_username}}'
required
auto-validate
autocapitalize='none'
error-message="[[localize('ui.panel.page-onboarding.user.required_field')]]"
></paper-input>
<paper-input
label="[[localize('ui.panel.page-onboarding.user.data.password')]]"
value='{{_password}}'
required
type='password'
auto-validate
error-message="[[localize('ui.panel.page-onboarding.user.required_field')]]"
></paper-input>
<template is='dom-if' if='[[!_loading]]'>
<p class='action'>
<mwc-button raised on-click='_submitForm'>
[[localize('ui.panel.page-onboarding.user.create_account')]]
</mwc-button>
</p>
</template>
</div>
</form>
`;
}
static get properties() {
return {
_name: String,
_username: String,
_password: String,
_loading: {
type: Boolean,
value: false,
},
translationFragment: {
type: String,
value: "page-onboarding",
},
_errorMsg: String,
};
}
async ready() {
super.ready();
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
this._submitForm();
}
});
try {
const response = await window.stepsPromise;
if (response.status === 404) {
// We don't load the component when onboarding is done
document.location = "/";
return;
}
const steps = await response.json();
if (steps.every((step) => step.done)) {
// Onboarding is done!
document.location = "/";
}
} catch (err) {
alert("Something went wrong loading loading onboarding, try refreshing");
}
}
_maybePopulateUsername() {
if (this._username) return;
const parts = this._name.split(" ");
if (parts.length) {
this._username = parts[0].toLowerCase();
}
}
async _submitForm() {
if (!this._name || !this._username || !this._password) {
this._errorMsg = "required_fields";
return;
}
this._errorMsg = "";
try {
const response = await fetch("/api/onboarding/users", {
method: "POST",
credentials: "same-origin",
body: JSON.stringify({
name: this._name,
username: this._username,
password: this._password,
}),
});
if (!response.ok) {
// eslint-disable-next-line
throw {
message: `Bad response from server: ${response.status}`,
};
}
document.location = "/";
} catch (err) {
// eslint-disable-next-line
console.error(err);
this.setProperties({
_loading: false,
_errorMsg: err.message,
});
}
}
_computeErrorMsg(localize, errorMsg) {
return (
localize(`ui.panel.page-onboarding.user.error.${errorMsg}`) || errorMsg
);
}
}
customElements.define("ha-onboarding", HaOnboarding);

View File

@ -0,0 +1,230 @@
import "@polymer/paper-input/paper-input";
import "@material/mwc-button";
import {
LitElement,
CSSResult,
css,
html,
PropertyValues,
property,
customElement,
TemplateResult,
} from "lit-element";
import { genClientId } from "home-assistant-js-websocket";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { OnboardingStep, onboardUserStep } from "../data/onboarding";
import { PolymerChangedEvent } from "../polymer-types";
@customElement("ha-onboarding")
class HaOnboarding extends litLocalizeLiteMixin(LitElement) {
public translationFragment = "page-onboarding";
@property() private _name = "";
@property() private _username = "";
@property() private _password = "";
@property() private _passwordConfirm = "";
@property() private _loading = false;
@property() private _errorMsg?: string = undefined;
protected render(): TemplateResult | void {
return html`
<p>
${this.localize("ui.panel.page-onboarding.intro")}
</p>
<p>
${this.localize("ui.panel.page-onboarding.user.intro")}
</p>
${
this._errorMsg
? html`
<p class="error">
${this.localize(
`ui.panel.page-onboarding.user.error.${this._errorMsg}`
) || this._errorMsg}
</p>
`
: ""
}
<form>
<paper-input
autofocus
name="name"
label="${this.localize("ui.panel.page-onboarding.user.data.name")}"
.value=${this._name}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='on'
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
@blur=${this._maybePopulateUsername}
></paper-input>
<paper-input
name="username"
label="${this.localize("ui.panel.page-onboarding.user.data.username")}"
value=${this._username}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='none'
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
></paper-input>
<paper-input
name="password"
label="${this.localize("ui.panel.page-onboarding.user.data.password")}"
value=${this._password}
@value-changed=${this._handleValueChanged}
required
type='password'
auto-validate
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
></paper-input>
<paper-input
name="passwordConfirm"
label="${this.localize(
"ui.panel.page-onboarding.user.data.password_confirm"
)}"
value=${this._passwordConfirm}
@value-changed=${this._handleValueChanged}
required
type='password'
.invalid=${this._password !== "" &&
this._passwordConfirm !== "" &&
this._passwordConfirm !== this._password}
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.error.password_not_match"
)}"
></paper-input>
<p class="action">
<mwc-button
raised
@click=${this._submitForm}
.disabled=${this._loading}
>
${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button>
</p>
</div>
</form>
`;
}
protected async firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
this._submitForm();
}
});
try {
const response = await window.stepsPromise;
if (response.status === 404) {
// We don't load the component when onboarding is done
document.location.href = "/";
return;
}
const steps: OnboardingStep[] = await response.json();
if (steps.every((step) => step.done)) {
// Onboarding is done!
document.location.href = "/";
}
} catch (err) {
alert("Something went wrong loading loading onboarding, try refreshing");
}
}
private _handleValueChanged(ev: PolymerChangedEvent<string>): void {
const name = (ev.target as any).name;
this[`_${name}`] = ev.detail.value;
}
private _maybePopulateUsername(): void {
if (this._username) {
return;
}
const parts = this._name.split(" ");
if (parts.length) {
this._username = parts[0].toLowerCase();
}
}
private async _submitForm(): Promise<void> {
if (!this._name || !this._username || !this._password) {
this._errorMsg = "required_fields";
return;
}
if (this._password !== this._passwordConfirm) {
this._errorMsg = "password_not_match";
return;
}
this._loading = true;
this._errorMsg = "";
try {
const clientId = genClientId();
const { auth_code } = await onboardUserStep({
client_id: clientId,
name: this._name,
username: this._username,
password: this._password,
});
const state = btoa(
JSON.stringify({
hassUrl: `${location.protocol}//${location.host}`,
clientId,
})
);
document.location.href = `/?auth_callback=1&code=${encodeURIComponent(
auth_code
)}&state=${state}`;
} catch (err) {
// tslint:disable-next-line
console.error(err);
this._loading = false;
this._errorMsg = err.message;
}
}
static get styles(): CSSResult {
return css`
.error {
color: red;
}
.action {
margin: 32px 0;
text-align: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-onboarding": HaOnboarding;
}
}

View File

@ -123,7 +123,7 @@ class DialogAreaDetail extends LitElement {
}
this._params = undefined;
} catch (err) {
this._error = err;
this._error = err.message || "Unknown error";
} finally {
this._submitting = false;
}

View File

@ -86,11 +86,11 @@ class HaConfigAreaRegistry extends LitElement {
? html`
<div class="empty">
${this.hass.localize(
"ui.panel.config.area_registry.picker.no_areas"
"ui.panel.config.area_registry.no_areas"
)}
<mwc-button @click=${this._createArea}>
${this.hass.localize(
"ui.panel.config.area_registry.picker.create_area"
"ui.panel.config.area_registry.create_area"
)}
</mwc-button>
</div>
@ -104,7 +104,7 @@ class HaConfigAreaRegistry extends LitElement {
?is-wide=${this.isWide}
icon="hass:plus"
title="${this.hass.localize(
"ui.panel.config.area_registry.picker.create_area"
"ui.panel.config.area_registry.create_area"
)}"
@click=${this._createArea}
class="${classMap({

View File

@ -3,6 +3,8 @@ import {
LitElement,
PropertyDeclarations,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
@ -12,9 +14,8 @@ import { PaperToggleButtonElement } from "@polymer/paper-toggle-button/paper-tog
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
import { updatePref } from "./data";
import { CloudStatusLoggedIn } from "./types";
import "./cloud-exposed-entities";
import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud";
export class CloudAlexaPref extends LitElement {
public hass?: HomeAssistant;
@ -35,7 +36,6 @@ export class CloudAlexaPref extends LitElement {
const enabled = this.cloudStatus!.prefs.alexa_enabled;
return html`
${this.renderStyle()}
<paper-card heading="Alexa">
<paper-toggle-button
.checked="${enabled}"
@ -51,7 +51,7 @@ export class CloudAlexaPref extends LitElement {
</li>
<li>
<a
href="https://www.home-assistant.io/cloud/alexa/"
href="https://www.nabucasa.com/config/amazon_alexa/"
target="_blank"
>
Config documentation
@ -80,25 +80,23 @@ export class CloudAlexaPref extends LitElement {
private async _toggleChanged(ev) {
const toggle = ev.target as PaperToggleButtonElement;
try {
await updatePref(this.hass!, { alexa_enabled: toggle.checked! });
await updateCloudPref(this.hass!, { alexa_enabled: toggle.checked! });
fireEvent(this, "ha-refresh-cloud-status");
} catch (err) {
toggle.checked = !toggle.checked;
}
}
private renderStyle(): TemplateResult {
return html`
<style>
a {
color: var(--primary-color);
}
paper-card > paper-toggle-button {
position: absolute;
right: 8px;
top: 16px;
}
</style>
static get styles(): CSSResult {
return css`
a {
color: var(--primary-color);
}
paper-card > paper-toggle-button {
position: absolute;
right: 8px;
top: 16px;
}
`;
}
}

View File

@ -12,12 +12,12 @@ import "../../../components/entity/ha-state-icon";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
import { EntityFilter } from "./types";
import computeStateName from "../../../common/entity/compute_state_name";
import {
FilterFunc,
generateFilter,
} from "../../../common/entity/entity_filter";
import { EntityFilter } from "../../../data/cloud";
export class CloudExposedEntities extends LitElement {
public hass?: HomeAssistant;

View File

@ -3,6 +3,8 @@ import {
LitElement,
PropertyDeclarations,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
@ -13,9 +15,8 @@ import "../../../components/buttons/ha-call-api-button";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
import { updatePref } from "./data";
import { CloudStatusLoggedIn } from "./types";
import "./cloud-exposed-entities";
import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud";
export class CloudGooglePref extends LitElement {
public hass?: HomeAssistant;
@ -36,7 +37,6 @@ export class CloudGooglePref extends LitElement {
const { google_enabled, google_allow_unlock } = this.cloudStatus.prefs;
return html`
${this.renderStyle()}
<paper-card heading="Google Assistant">
<paper-toggle-button
id="google_enabled"
@ -58,7 +58,7 @@ export class CloudGooglePref extends LitElement {
</li>
<li>
<a
href="https://www.home-assistant.io/cloud/google_assistant/"
href="https://www.nabucasa.com/config/google_assistant/"
target="_blank"
>
Config documentation
@ -103,37 +103,35 @@ export class CloudGooglePref extends LitElement {
private async _toggleChanged(ev) {
const toggle = ev.target as PaperToggleButtonElement;
try {
await updatePref(this.hass!, { [toggle.id]: toggle.checked! });
await updateCloudPref(this.hass!, { [toggle.id]: toggle.checked! });
fireEvent(this, "ha-refresh-cloud-status");
} catch (err) {
toggle.checked = !toggle.checked;
}
}
private renderStyle(): TemplateResult {
return html`
<style>
a {
color: var(--primary-color);
}
paper-card > paper-toggle-button {
position: absolute;
right: 8px;
top: 16px;
}
ha-call-api-button {
color: var(--primary-color);
font-weight: 500;
}
.unlock {
display: flex;
flex-direction: row;
padding-top: 16px;
}
.unlock > div {
flex: 1;
}
</style>
static get styles(): CSSResult {
return css`
a {
color: var(--primary-color);
}
paper-card > paper-toggle-button {
position: absolute;
right: 8px;
top: 16px;
}
ha-call-api-button {
color: var(--primary-color);
font-weight: 500;
}
.unlock {
display: flex;
flex-direction: row;
padding-top: 16px;
}
.unlock > div {
flex: 1;
}
`;
}
}

View File

@ -0,0 +1,148 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
CSSResult,
css,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-toggle-button/paper-toggle-button";
import "@polymer/paper-item/paper-item-body";
// tslint:disable-next-line
import { PaperToggleButtonElement } from "@polymer/paper-toggle-button/paper-toggle-button";
import "../../../components/buttons/ha-call-api-button";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
import {
connectCloudRemote,
disconnectCloudRemote,
CloudStatusLoggedIn,
} from "../../../data/cloud";
import format_date_time from "../../../common/datetime/format_date_time";
@customElement("cloud-remote-pref")
export class CloudRemotePref extends LitElement {
public hass?: HomeAssistant;
public cloudStatus?: CloudStatusLoggedIn;
static get properties(): PropertyDeclarations {
return {
hass: {},
cloudStatus: {},
};
}
protected render(): TemplateResult | void {
if (!this.cloudStatus) {
return html``;
}
const {
remote_connected,
remote_domain,
remote_certificate,
} = this.cloudStatus;
return html`
<paper-card heading="Remote Control">
<paper-toggle-button
.checked="${remote_connected}"
@change="${this._toggleChanged}"
></paper-toggle-button>
<div class="card-content">
Home Assistant Cloud provides you with a secure remote connection to
your instance while away from home. Your instance
${remote_connected ? "is" : "will be"} available at
<a href="https://${remote_domain}" target="_blank">
https://${remote_domain}</a
>.
${!remote_certificate
? ""
: html`
<div class="data-row">
<paper-item-body two-line>
Certificate expiration date
<div secondary>Will be automatically renewed</div>
</paper-item-body>
<div class="data-value">
${format_date_time(
new Date(remote_certificate.expire_date),
this.hass!.language
)}
</div>
</div>
<div class="data-row">
<paper-item-body>
Certificate fingerprint
</paper-item-body>
<div class="data-value">
${remote_certificate.fingerprint}
</div>
</div>
`}
</div>
<div class="card-actions">
<a href="https://www.nabucasa.com/config/remote/" target="_blank">
<mwc-button>Learn how it works</mwc-button>
</a>
</div>
</paper-card>
`;
}
private async _toggleChanged(ev) {
const toggle = ev.target as PaperToggleButtonElement;
try {
if (toggle.checked) {
await connectCloudRemote(this.hass!);
} else {
await disconnectCloudRemote(this.hass!);
}
fireEvent(this, "ha-refresh-cloud-status");
} catch (err) {
toggle.checked = !toggle.checked;
}
}
static get styles(): CSSResult {
return css`
.data-row {
display: flex;
}
.data-value {
padding: 16px 0;
}
a {
color: var(--primary-color);
}
paper-card > paper-toggle-button {
position: absolute;
right: 8px;
top: 16px;
}
ha-call-api-button {
color: var(--primary-color);
font-weight: 500;
}
.unlock {
display: flex;
flex-direction: row;
padding-top: 16px;
}
.unlock > div {
flex: 1;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"cloud-remote-pref": CloudRemotePref;
}
}

View File

@ -17,8 +17,8 @@ import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { HomeAssistant } from "../../../types";
import { WebhookDialogParams } from "./types";
import { haStyle } from "../../../resources/styles";
import { WebhookDialogParams } from "./show-cloud-webhook-manage-dialog";
const inputLabel = "Public URL Click to copy to clipboard";
@ -60,10 +60,17 @@ export class CloudWebhookManageDialog extends LitElement {
@blur="${this._restoreLabel}"
></paper-input>
<p>
If you no longer want to use this webhook, you can
<button class="link" @click="${this._disableWebhook}">
disable it</button
>.
${cloudhook.managed
? html`
This webhook is managed by an integration and cannot be
disabled.
`
: html`
If you no longer want to use this webhook, you can
<button class="link" @click="${this._disableWebhook}">
disable it</button
>.
`}
</p>
</div>

View File

@ -10,23 +10,15 @@ import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-spinner/paper-spinner";
import "../../../components/ha-card";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant, WebhookError } from "../../../types";
import { WebhookDialogParams, CloudStatusLoggedIn } from "./types";
import { Webhook, fetchWebhooks } from "../../../data/webhook";
import {
createCloudhook,
deleteCloudhook,
CloudWebhook,
CloudStatusLoggedIn,
} from "../../../data/cloud";
declare global {
// for fire event
interface HASSDomEvents {
"manage-cloud-webhook": WebhookDialogParams;
}
}
import { showManageCloudhookDialog } from "./show-cloud-webhook-manage-dialog";
export class CloudWebhooks extends LitElement {
public hass?: HomeAssistant;
@ -138,14 +130,13 @@ export class CloudWebhooks extends LitElement {
private _showDialog(webhookId: string) {
const webhook = this._localHooks!.find(
(ent) => ent.webhook_id === webhookId
);
)!;
const cloudhook = this._cloudHooks![webhookId];
const params: WebhookDialogParams = {
webhook: webhook!,
showManageCloudhookDialog(this, {
webhook,
cloudhook,
disableHook: () => this._disableWebhook(webhookId),
};
fireEvent(this, "manage-cloud-webhook", params);
});
}
private _handleManageButton(ev: MouseEvent) {

View File

@ -1,18 +0,0 @@
import { HomeAssistant } from "../../../types";
import { SubscriptionInfo } from "./types";
export const fetchSubscriptionInfo = (hass: HomeAssistant) =>
hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" });
export const updatePref = (
hass: HomeAssistant,
prefs: {
google_enabled?: boolean;
alexa_enabled?: boolean;
google_allow_unlock?: boolean;
}
) =>
hass.callWS({
type: "cloud/update_prefs",
...prefs,
});

View File

@ -16,10 +16,10 @@ import formatDateTime from "../../../common/datetime/format_date_time";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { fireEvent } from "../../../common/dom/fire_event";
import { fetchSubscriptionInfo } from "./data";
import { fetchCloudSubscriptionInfo } from "../../../data/cloud";
import "./cloud-alexa-pref";
import "./cloud-google-pref";
import "./cloud-remote-pref";
let registeredWebhookDialog = false;
@ -66,6 +66,9 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
text-transform: capitalize;
padding: 16px;
}
a {
color: var(--primary-color);
}
</style>
<hass-subpage header="Home Assistant Cloud">
<div class="content">
@ -83,7 +86,7 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
<div class="account-row">
<paper-item-body two-line="">
[[cloudStatus.email]]
<div secondary="" class="wrap">
<div secondary class="wrap">
[[_formatSubscription(_subscription)]]
</div>
</paper-item-body>
@ -121,6 +124,11 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
</p>
</div>
<cloud-remote-pref
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
></cloud-remote-pref>
<cloud-alexa-pref
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
@ -172,8 +180,12 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
}
}
_computeRemoteConnected(connected) {
return connected ? "Connected" : "Not Connected";
}
async _fetchSubscriptionInfo() {
this._subscription = await fetchSubscriptionInfo(this.hass);
this._subscription = await fetchCloudSubscriptionInfo(this.hass);
if (
this._subscription.provider &&
this.cloudStatus &&

View File

@ -74,8 +74,10 @@ class HaConfigCloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
<span slot="header">Home Assistant Cloud</span>
<div slot="introduction">
<p>
Home Assistant Cloud connects your local instance securely to
cloud-only services Amazon Alexa and Google Assistant.
Home Assistant Cloud provides you with a secure remote
connection to your instance while away from home. It also allows
you to connect with cloud-only services: Amazon Alexa and Google
Assistant.
</p>
<p>
This service is run by our partner

View File

@ -66,8 +66,10 @@ class HaConfigCloudRegister extends EventsMixin(PolymerElement) {
The trial will give you access to all the benefits of Home Assistant Cloud, including:
</p>
<ul>
<li>Control of Home Assistant away from home</li>
<li>Integration with Google Assistant</li>
<li>Integration with Amazon Alexa</li>
<li>Easy integration with webhook-based apps like OwnTracks</li>
</ul>
<p>
This service is run by our partner <a href='https://www.nabucasa.com' target='_blank'>Nabu&nbsp;Casa,&nbsp;Inc</a>, a company founded by the founders of Home Assistant and Hass.io.

View File

@ -0,0 +1,21 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { Webhook } from "../../../data/webhook";
import { CloudWebhook } from "../../../data/cloud";
export interface WebhookDialogParams {
webhook: Webhook;
cloudhook: CloudWebhook;
disableHook: () => void;
}
export const showManageCloudhookDialog = (
element: HTMLElement,
webhookDialogParams: WebhookDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "cloud-webhook-manage-dialog",
dialogImport: () =>
import(/* webpackChunkName: "cloud-webhook-manage-dialog" */ "./cloud-webhook-manage-dialog"),
dialogParams: webhookDialogParams,
});
};

View File

@ -1,39 +0,0 @@
import { CloudWebhook } from "../../../data/cloud";
import { Webhook } from "../../../data/webhook";
export interface EntityFilter {
include_domains: string[];
include_entities: string[];
exclude_domains: string[];
exclude_entities: string[];
}
interface CloudStatusBase {
logged_in: boolean;
cloud: "disconnected" | "connecting" | "connected";
}
export type CloudStatusLoggedIn = CloudStatusBase & {
email: string;
google_entities: EntityFilter;
google_domains: string[];
alexa_entities: EntityFilter;
alexa_domains: string[];
prefs: {
google_enabled: boolean;
alexa_enabled: boolean;
google_allow_unlock: boolean;
cloudhooks: { [webhookId: string]: CloudWebhook };
};
};
export type CloudStatus = CloudStatusBase | CloudStatusLoggedIn;
export interface SubscriptionInfo {
human_description: string;
}
export interface WebhookDialogParams {
webhook: Webhook;
cloudhook: CloudWebhook;
disableHook: () => void;
}

View File

@ -139,7 +139,7 @@ class DialogEntityRegistryDetail extends LitElement {
});
this._params = undefined;
} catch (err) {
this._error = err;
this._error = err.message || "Unknown error";
} finally {
this._submitting = false;
}

View File

@ -84,7 +84,7 @@ class DialogPersonDetail extends LitElement {
<ha-entities-picker
.hass=${this.hass}
.value=${this._deviceTrackers}
domainFilter="device_tracker"
domain-filter="device_tracker"
.pickedEntityLabel=${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_picked"
)}

View File

@ -27,7 +27,7 @@ import {
showPersonDetailDialog,
loadPersonDetailDialog,
} from "./show-dialog-person-detail";
import { User, fetchUsers } from "../../../data/auth";
import { User, fetchUsers } from "../../../data/user";
class HaConfigPerson extends LitElement {
public hass?: HomeAssistant;

View File

@ -1,6 +1,6 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { Person, PersonMutableParams } from "../../../data/person";
import { User } from "../../../data/auth";
import { User } from "../../../data/user";
export interface PersonDetailDialogParams {
entry?: Person;

View File

@ -66,7 +66,7 @@ class HaUserPicker extends EventsMixin(
<paper-item-body two-line>
<div>[[_withDefault(user.name, 'Unnamed User')]]</div>
<div secondary="">
[[user.id]]
[[_computeGroup(localize, user)]]
<template is="dom-if" if="[[user.system_generated]]">
- System Generated
</template>
@ -124,6 +124,10 @@ class HaUserPicker extends EventsMixin(
return `/config/users/${user.id}`;
}
_computeGroup(localize, user) {
return localize(`groups.${user.group_ids[0]}`);
}
_computeRTL(hass) {
return computeRTL(hass);
}

View File

@ -9,7 +9,7 @@ import NavigateMixin from "../../../mixins/navigate-mixin";
import "./ha-config-user-picker";
import "./ha-user-editor";
import { fireEvent } from "../../../common/dom/fire_event";
import { fetchUsers } from "../../../data/auth";
import { fetchUsers } from "../../../data/user";
/*
* @appliesMixin NavigateMixin

View File

@ -1,113 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../layouts/hass-subpage";
import LocalizeMixin from "../../../mixins/localize-mixin";
import NavigateMixin from "../../../mixins/navigate-mixin";
import EventsMixin from "../../../mixins/events-mixin";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin
* @appliesMixin EventsMixin
*/
class HaUserEditor extends EventsMixin(
NavigateMixin(LocalizeMixin(PolymerElement))
) {
static get template() {
return html`
<style include="ha-style">
paper-card {
display: block;
max-width: 600px;
margin: 0 auto 16px;
}
paper-card:first-child {
margin-top: 16px;
}
paper-card:last-child {
margin-bottom: 16px;
}
hass-subpage paper-card:first-of-type {
direction: ltr;
}
</style>
<hass-subpage
header="[[localize('ui.panel.config.users.editor.caption')]]"
>
<paper-card heading="[[_computeName(user)]]">
<table class="card-content">
<tr>
<td>ID</td>
<td>[[user.id]]</td>
</tr>
<tr>
<td>Owner</td>
<td>[[user.is_owner]]</td>
</tr>
<tr>
<td>Active</td>
<td>[[user.is_active]]</td>
</tr>
<tr>
<td>System generated</td>
<td>[[user.system_generated]]</td>
</tr>
</table>
</paper-card>
<paper-card>
<div class="card-actions">
<mwc-button
class="warning"
on-click="_deleteUser"
disabled="[[user.system_generated]]"
>
[[localize('ui.panel.config.users.editor.delete_user')]]
</mwc-button>
<template is="dom-if" if="[[user.system_generated]]">
Unable to remove system generated users.
</template>
</div>
</paper-card>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
user: Object,
};
}
_computeName(user) {
return user && (user.name || "Unnamed user");
}
async _deleteUser(ev) {
if (
!confirm(
`Are you sure you want to delete ${this._computeName(this.user)}`
)
) {
ev.target.blur();
return;
}
try {
await this.hass.callWS({
type: "config/auth/delete",
user_id: this.user.id,
});
} catch (err) {
alert(err.code);
return;
}
this.fire("reload-users");
this.navigate("/config/users");
}
}
customElements.define("ha-user-editor", HaUserEditor);

View File

@ -0,0 +1,217 @@
import {
LitElement,
TemplateResult,
html,
customElement,
CSSResultArray,
css,
property,
} from "lit-element";
import { until } from "lit-html/directives/until";
import "@material/mwc-button";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import "../../../components/ha-card";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import {
User,
deleteUser,
updateUser,
SYSTEM_GROUP_ID_USER,
SYSTEM_GROUP_ID_ADMIN,
} from "../../../data/user";
declare global {
interface HASSDomEvents {
"reload-users": undefined;
}
}
const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN];
@customElement("ha-user-editor")
class HaUserEditor extends LitElement {
@property() public hass?: HomeAssistant;
@property() public user?: User;
protected render(): TemplateResult | void {
const hass = this.hass;
const user = this.user;
if (!hass || !user) {
return html``;
}
return html`
<hass-subpage
.header=${hass.localize("ui.panel.config.users.editor.caption")}
>
<ha-card .header=${this._name}>
<table class="card-content">
<tr>
<td>ID</td>
<td>${user.id}</td>
</tr>
<tr>
<td>Owner</td>
<td>${user.is_owner}</td>
</tr>
<tr>
<td>Group</td>
<td>
<select
@change=${this._handleGroupChange}
.value=${until(
this.updateComplete.then(() => user.group_ids[0])
)}
>
${GROUPS.map(
(groupId) => html`
<option value=${groupId}>
${hass.localize(`groups.${groupId}`)}
</option>
`
)}
</select>
</td>
</tr>
${user.group_ids[0] === SYSTEM_GROUP_ID_USER
? html`
<tr>
<td colspan="2" class="user-experiment">
The users group is a work in progress. The user will be
unable to administer the instance via the UI. We're still
auditing all management API endpoints to ensure that they
correctly limit access to administrators.
</td>
</tr>
`
: ""}
<tr>
<td>Active</td>
<td>${user.is_active}</td>
</tr>
<tr>
<td>System generated</td>
<td>${user.system_generated}</td>
</tr>
</table>
<div class="card-actions">
<mwc-button @click=${this._handleRenameUser}>
${hass.localize("ui.panel.config.users.editor.rename_user")}
</mwc-button>
<mwc-button
class="warning"
@click=${this._deleteUser}
.disabled=${user.system_generated}
>
${hass.localize("ui.panel.config.users.editor.delete_user")}
</mwc-button>
${user.system_generated
? html`
Unable to remove system generated users.
`
: ""}
</div>
</ha-card>
</hass-subpage>
`;
}
private get _name() {
return this.user && (this.user.name || "Unnamed user");
}
private async _handleRenameUser(ev): Promise<void> {
ev.currentTarget.blur();
const newName = prompt("New name?", this.user!.name);
if (newName === null || newName === this.user!.name) {
return;
}
try {
await updateUser(this.hass!, this.user!.id, {
name: newName,
});
fireEvent(this, "reload-users");
} catch (err) {
alert(`User rename failed: ${err.message}`);
}
}
private async _handleGroupChange(ev): Promise<void> {
const selectEl = ev.currentTarget as HTMLSelectElement;
const newGroup = selectEl.value;
try {
await updateUser(this.hass!, this.user!.id, {
group_ids: [newGroup],
});
fireEvent(this, "reload-users");
} catch (err) {
alert(`Group update failed: ${err.message}`);
selectEl.value = this.user!.group_ids[0];
}
}
private async _deleteUser(ev): Promise<void> {
if (!confirm(`Are you sure you want to delete ${this._name}`)) {
ev.target.blur();
return;
}
try {
await deleteUser(this.hass!, this.user!.id);
} catch (err) {
alert(err.code);
return;
}
fireEvent(this, "reload-users");
navigate(this, "/config/users");
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
ha-card {
display: block;
max-width: 600px;
margin: 0 auto 16px;
}
ha-card:first-child {
margin-top: 16px;
}
ha-card:last-child {
margin-bottom: 16px;
}
.card-content {
padding: 0 16px 16px;
}
.card-actions {
padding: 0 8px;
}
hass-subpage ha-card:first-of-type {
direction: ltr;
}
table {
width: 100%;
}
td {
vertical-align: top;
}
.user-experiment {
padding: 8px 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-user-editor": HaUserEditor;
}
}

View File

@ -3,37 +3,37 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
import {
html,
LitElement,
PropertyDeclarations,
property,
PropertyValues,
TemplateResult,
CSSResult,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { HassEntity } from "home-assistant-js-websocket";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { Cluster } from "../../../data/zha";
import { Cluster, ZHADevice, fetchBindableDevices } from "../../../data/zha";
import "../../../layouts/ha-app-layout";
import "../../../components/ha-paper-icon-button-arrow-prev";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { ZHAClusterSelectedParams, ZHANodeSelectedParams } from "./types";
import { ZHAClusterSelectedParams, ZHADeviceSelectedParams } from "./types";
import "./zha-cluster-attributes";
import "./zha-cluster-commands";
import "./zha-network";
import "./zha-node";
import "./zha-binding";
export class HaConfigZha extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _selectedNode?: HassEntity;
private _selectedCluster?: Cluster;
@property() public hass?: HomeAssistant;
@property() public isWide?: boolean;
@property() private _selectedDevice?: ZHADevice;
@property() private _selectedCluster?: Cluster;
@property() private _bindableDevices: ZHADevice[] = [];
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_selectedCluster: {},
_selectedNode: {},
};
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("_selectedDevice")) {
this._fetchBindableDevices();
}
super.update(changedProperties);
}
protected render(): TemplateResult | void {
@ -57,25 +57,35 @@ export class HaConfigZha extends LitElement {
.isWide="${this.isWide}"
.hass="${this.hass}"
@zha-cluster-selected="${this._onClusterSelected}"
@zha-node-selected="${this._onNodeSelected}"
@zha-node-selected="${this._onDeviceSelected}"
></zha-node>
${this._selectedCluster
? html`
<zha-cluster-attributes
.isWide="${this.isWide}"
.hass="${this.hass}"
.selectedNode="${this._selectedNode}"
.selectedNode="${this._selectedDevice}"
.selectedCluster="${this._selectedCluster}"
></zha-cluster-attributes>
<zha-cluster-commands
.isWide="${this.isWide}"
.hass="${this.hass}"
.selectedNode="${this._selectedNode}"
.selectedNode="${this._selectedDevice}"
.selectedCluster="${this._selectedCluster}"
></zha-cluster-commands>
`
: ""}
${this._selectedDevice && this._bindableDevices.length > 0
? html`
<zha-binding-control
.isWide="${this.isWide}"
.hass="${this.hass}"
.selectedDevice="${this._selectedDevice}"
.bindableDevices="${this._bindableDevices}"
></zha-binding-control>
`
: ""}
</ha-app-layout>
`;
}
@ -86,13 +96,24 @@ export class HaConfigZha extends LitElement {
this._selectedCluster = selectedClusterEvent.detail.cluster;
}
private _onNodeSelected(
selectedNodeEvent: HASSDomEvent<ZHANodeSelectedParams>
private _onDeviceSelected(
selectedNodeEvent: HASSDomEvent<ZHADeviceSelectedParams>
): void {
this._selectedNode = selectedNodeEvent.detail.node;
this._selectedDevice = selectedNodeEvent.detail.node;
this._selectedCluster = undefined;
}
private async _fetchBindableDevices(): Promise<void> {
if (this._selectedDevice && this.hass) {
this._bindableDevices = (await fetchBindableDevices(
this.hass,
this._selectedDevice!.ieee
)).sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
}
static get styles(): CSSResult[] {
return [haStyle];
}

View File

@ -1,4 +1,4 @@
import { ZHADeviceEntity, Cluster } from "../../../data/zha";
import { ZHADevice, Cluster } from "../../../data/zha";
export interface PickerTarget extends EventTarget {
selected: number;
@ -34,8 +34,8 @@ export interface IssueCommandServiceData {
command_type: string;
}
export interface ZHANodeSelectedParams {
node: ZHADeviceEntity;
export interface ZHADeviceSelectedParams {
node: ZHADevice;
}
export interface ZHAClusterSelectedParams {

View File

@ -0,0 +1,211 @@
import {
html,
LitElement,
property,
PropertyValues,
TemplateResult,
CSSResult,
css,
customElement,
} from "lit-element";
import "@polymer/paper-card/paper-card";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import { ZHADevice, bindDevices, unbindDevices } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { ItemSelectedEvent } from "./types";
@customElement("zha-binding-control")
export class ZHABindingControl extends LitElement {
@property() public hass?: HomeAssistant;
@property() public isWide?: boolean;
@property() public selectedDevice?: ZHADevice;
@property() private _showHelp: boolean = false;
@property() private _bindTargetIndex: number = -1;
@property() private bindableDevices: ZHADevice[] = [];
private _deviceToBind?: ZHADevice;
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedDevice")) {
this._bindTargetIndex = -1;
}
super.update(changedProperties);
}
protected render(): TemplateResult | void {
return html`
<ha-config-section .isWide="${this.isWide}">
<div class="sectionHeader" slot="header">
<span>Device Binding</span>
<paper-icon-button
class="toggle-help-icon"
@click="${this._onHelpTap}"
icon="hass:help-circle"
>
</paper-icon-button>
</div>
<span slot="introduction">Bind and unbind devices.</span>
<paper-card class="content">
<div class="command-picker">
<paper-dropdown-menu label="Bindable Devices" class="flex">
<paper-listbox
slot="dropdown-content"
.selected="${this._bindTargetIndex}"
@iron-select="${this._bindTargetIndexChanged}"
>
${this.bindableDevices.map(
(device) => html`
<paper-item>${device.name}</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div>
${this._showHelp
? html`
<div class="helpText">
Select a device to issue a bind command.
</div>
`
: ""}
<div class="card-actions">
<mwc-button @click="${this._onBindDevicesClick}">Bind</mwc-button>
${this._showHelp
? html`
<div class="helpText">
Bind devices.
</div>
`
: ""}
<mwc-button @click="${this._onUnbindDevicesClick}"
>Unbind</mwc-button
>
${this._showHelp
? html`
<div class="helpText">
Unbind devices.
</div>
`
: ""}
</div>
</paper-card>
</ha-config-section>
`;
}
private _bindTargetIndexChanged(event: ItemSelectedEvent): void {
this._bindTargetIndex = event.target!.selected;
this._deviceToBind =
this._bindTargetIndex === -1
? undefined
: this.bindableDevices[this._bindTargetIndex];
}
private _onHelpTap(): void {
this._showHelp = !this._showHelp;
}
private async _onBindDevicesClick(): Promise<void> {
if (this.hass && this._deviceToBind && this.selectedDevice) {
await bindDevices(
this.hass,
this.selectedDevice.ieee,
this._deviceToBind.ieee
);
}
}
private async _onUnbindDevicesClick(): Promise<void> {
if (this.hass && this._deviceToBind && this.selectedDevice) {
await unbindDevices(
this.hass,
this.selectedDevice.ieee,
this._deviceToBind.ieee
);
}
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.flex {
-ms-flex: 1 1 0.000000001px;
-webkit-flex: 1;
flex: 1;
-webkit-flex-basis: 0.000000001px;
flex-basis: 0.000000001px;
}
.content {
margin-top: 24px;
}
paper-card {
display: block;
margin: 0 auto;
max-width: 600px;
}
.card-actions.warning ha-call-service-button {
color: var(--google-red-500);
}
.command-picker {
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-ms-flex-direction: row;
-webkit-flex-direction: row;
flex-direction: row;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
.input-text {
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
.sectionHeader {
position: relative;
}
.helpText {
color: grey;
padding: 16px;
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
display: block;
color: grey;
}
[hidden] {
display: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-binding-control": ZHABindingControl;
}
}

View File

@ -10,6 +10,7 @@ import {
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../common/dom/fire_event";
@ -18,9 +19,13 @@ import "../../../components/ha-service-description";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { ItemSelectedEvent, NodeServiceData } from "./types";
import { ItemSelectedEvent, NodeServiceData, ChangeEvent } from "./types";
import "./zha-clusters";
import "./zha-device-card";
import {
updateDeviceRegistryEntry,
DeviceRegistryEntryMutableParams,
} from "../../../data/device_registry";
import { reconfigureNode, fetchDevices, ZHADevice } from "../../../data/zha";
declare global {
@ -40,6 +45,7 @@ export class ZHANode extends LitElement {
private _selectedNode?: ZHADevice;
private _serviceData?: {};
private _nodes: ZHADevice[];
private _userSelectedName?: string;
constructor() {
super();
@ -58,6 +64,7 @@ export class ZHANode extends LitElement {
_entities: {},
_serviceData: {},
_nodes: {},
_userSelectedName: {},
};
}
@ -109,7 +116,11 @@ export class ZHANode extends LitElement {
>
${this._nodes.map(
(entry) => html`
<paper-item>${entry.name}</paper-item>
<paper-item
>${entry.user_given_name
? entry.user_given_name
: entry.name}</paper-item
>
`
)}
</paper-listbox>
@ -132,6 +143,18 @@ export class ZHANode extends LitElement {
></zha-device-card>
`
: ""}
${this._selectedNodeIndex !== -1
? html`
<div class="input-text">
<paper-input
type="string"
.value="${this._userSelectedName}"
@value-changed="${this._onUserSelectedNameChanged}"
placeholder="User given name"
></paper-input>
</div>
`
: ""}
${this._selectedNodeIndex !== -1 ? this._renderNodeActions() : ""}
${this._selectedNode ? this._renderClusters() : ""}
</paper-card>
@ -170,6 +193,21 @@ export class ZHANode extends LitElement {
/>
`
: ""}
<mwc-button
@click="${this._onUpdateDeviceNameClick}"
.disabled="${!this._userSelectedName ||
this._userSelectedName === ""}"
>Update Name</mwc-button
>
${this._showHelp
? html`
<div class="helpText">
${this.hass!.localize(
"ui.panel.config.zha.services.updateDeviceName"
)}
</div>
`
: ""}
</div>
`;
}
@ -191,6 +229,7 @@ export class ZHANode extends LitElement {
private _selectedNodeChanged(event: ItemSelectedEvent): void {
this._selectedNodeIndex = event!.target!.selected;
this._selectedNode = this._nodes[this._selectedNodeIndex];
this._userSelectedName = "";
fireEvent(this, "zha-node-selected", { node: this._selectedNode });
this._serviceData = this._computeNodeServiceData();
}
@ -201,6 +240,27 @@ export class ZHANode extends LitElement {
}
}
private _onUserSelectedNameChanged(value: ChangeEvent): void {
this._userSelectedName = value.detail!.value;
}
private async _onUpdateDeviceNameClick(): Promise<void> {
if (this.hass) {
const values: DeviceRegistryEntryMutableParams = {
name_by_user: this._userSelectedName,
};
await updateDeviceRegistryEntry(
this.hass,
this._selectedNode!.device_reg_id,
values
);
this._selectedNode!.user_given_name = this._userSelectedName!;
this._userSelectedName = "";
}
}
private _computeNodeServiceData(): NodeServiceData {
return {
ieee_address: this._selectedNode!.ieee,
@ -277,6 +337,7 @@ export class ZHANode extends LitElement {
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
word-wrap: break-word;
}
ha-service-description {
@ -294,6 +355,12 @@ export class ZHANode extends LitElement {
right: 0;
color: var(--primary-color);
}
.input-text {
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
`,
];
}

View File

@ -6,6 +6,7 @@ import {
CSSResult,
css,
property,
customElement,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
@ -39,6 +40,7 @@ export interface Config extends LovelaceCardConfig {
states?: string[];
}
@customElement("hui-alarm-panel-card")
class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
public static async getConfigElement() {
await import(/* webpackChunkName: "hui-alarm-panel-card-editor" */ "../editor/config-elements/hui-alarm-panel-card-editor");
@ -50,7 +52,9 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
}
@property() public hass?: HomeAssistant;
@property() private _config?: Config;
@property() private _code?: string;
public getCardSize(): number {
@ -205,98 +209,109 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
this._code = "";
}
static get styles(): CSSResult[] {
return [
css`
ha-card {
padding-bottom: 16px;
position: relative;
--alarm-color-disarmed: var(--label-badge-green);
--alarm-color-pending: var(--label-badge-yellow);
--alarm-color-triggered: var(--label-badge-red);
--alarm-color-armed: var(--label-badge-red);
--alarm-color-autoarm: rgba(0, 153, 255, 0.1);
--alarm-state-color: var(--alarm-color-armed);
--base-unit: 15px;
font-size: calc(var(--base-unit));
}
ha-label-badge {
static get styles(): CSSResult {
return css`
ha-card {
padding-bottom: 16px;
position: relative;
--alarm-color-disarmed: var(--label-badge-green);
--alarm-color-pending: var(--label-badge-yellow);
--alarm-color-triggered: var(--label-badge-red);
--alarm-color-armed: var(--label-badge-red);
--alarm-color-autoarm: rgba(0, 153, 255, 0.1);
--alarm-state-color: var(--alarm-color-armed);
--base-unit: 15px;
font-size: calc(var(--base-unit));
}
ha-label-badge {
--ha-label-badge-color: var(--alarm-state-color);
--label-badge-text-color: var(--alarm-state-color);
--label-badge-background-color: var(--paper-card-background-color);
color: var(--alarm-state-color);
position: absolute;
right: 12px;
top: 12px;
}
.disarmed {
--alarm-state-color: var(--alarm-color-disarmed);
}
.triggered {
--alarm-state-color: var(--alarm-color-triggered);
animation: pulse 1s infinite;
}
.arming {
--alarm-state-color: var(--alarm-color-pending);
animation: pulse 1s infinite;
}
.pending {
--alarm-state-color: var(--alarm-color-pending);
animation: pulse 1s infinite;
}
@keyframes pulse {
0% {
--ha-label-badge-color: var(--alarm-state-color);
--label-badge-text-color: var(--alarm-state-color);
--label-badge-background-color: var(--paper-card-background-color);
color: var(--alarm-state-color);
position: absolute;
right: 12px;
top: 12px;
}
.disarmed {
--alarm-state-color: var(--alarm-color-disarmed);
100% {
--ha-label-badge-color: rgba(255, 153, 0, 0.3);
}
.triggered {
--alarm-state-color: var(--alarm-color-triggered);
animation: pulse 1s infinite;
}
.arming {
--alarm-state-color: var(--alarm-color-pending);
animation: pulse 1s infinite;
}
.pending {
--alarm-state-color: var(--alarm-color-pending);
animation: pulse 1s infinite;
}
@keyframes pulse {
0% {
--ha-label-badge-color: var(--alarm-state-color);
}
100% {
--ha-label-badge-color: rgba(255, 153, 0, 0.3);
}
}
paper-input {
margin: 0 auto 8px;
max-width: 150px;
font-size: calc(var(--base-unit));
text-align: center;
}
.state {
margin-left: 16px;
font-size: calc(var(--base-unit) * 0.9);
position: relative;
bottom: 16px;
color: var(--alarm-state-color);
animation: none;
}
#keypad {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin: auto;
width: 300px;
}
#keypad mwc-button {
margin-bottom: 5%;
width: 30%;
padding: calc(var(--base-unit));
font-size: calc(var(--base-unit) * 1.1);
box-sizing: border-box;
}
.actions {
margin: 0 8px;
padding-top: 20px;
display: flex;
flex-wrap: wrap;
justify-content: center;
font-size: calc(var(--base-unit) * 1);
}
.actions mwc-button {
min-width: calc(var(--base-unit) * 9);
margin: 0 4px;
}
mwc-button#disarm {
color: var(--google-red-500);
}
`,
];
}
paper-input {
margin: 0 auto 8px;
max-width: 150px;
font-size: calc(var(--base-unit));
text-align: center;
}
.state {
margin-left: 16px;
font-size: calc(var(--base-unit) * 0.9);
position: relative;
bottom: 16px;
color: var(--alarm-state-color);
animation: none;
}
#keypad {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin: auto;
width: 300px;
}
#keypad mwc-button {
margin-bottom: 5%;
width: 30%;
padding: calc(var(--base-unit));
font-size: calc(var(--base-unit) * 1.1);
box-sizing: border-box;
}
.actions {
margin: 0 8px;
padding-top: 20px;
display: flex;
flex-wrap: wrap;
justify-content: center;
font-size: calc(var(--base-unit) * 1);
}
.actions mwc-button {
min-width: calc(var(--base-unit) * 9);
margin: 0 4px;
}
mwc-button#disarm {
color: var(--google-red-500);
}
`;
}
}
@ -305,5 +320,3 @@ declare global {
"hui-alarm-panel-card": HuiAlarmPanelCard;
}
}
customElements.define("hui-alarm-panel-card", HuiAlarmPanelCard);

View File

@ -1,10 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
CSSResult,
css,
customElement,
property,
} from "lit-element";
import "@polymer/paper-card/paper-card";
@ -18,8 +19,9 @@ export interface Config extends LovelaceCardConfig {
title?: string;
}
@customElement("hui-empty-state-card")
export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant;
@property() public hass?: HomeAssistant;
public getCardSize(): number {
return 2;
@ -29,12 +31,6 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
// tslint:disable-next-line
}
static get properties(): PropertyDeclarations {
return {
hass: {},
};
}
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
@ -83,5 +79,3 @@ declare global {
"hui-empty-state-card": HuiEmptyStateCard;
}
}
customElements.define("hui-empty-state-card", HuiEmptyStateCard);

View File

@ -1,9 +1,12 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import "../../../components/ha-card";
@ -36,6 +39,7 @@ export interface EntitiesCardConfig extends LovelaceCardConfig {
theme?: string;
}
@customElement("hui-entities-card")
class HuiEntitiesCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-entities-card-editor" */ "../editor/config-elements/hui-entities-card-editor");
@ -46,8 +50,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
return { entities: [] };
}
@property() protected _config?: EntitiesCardConfig;
protected _hass?: HomeAssistant;
protected _config?: EntitiesCardConfig;
protected _configEntities?: EntitiesCardEntityConfig[];
set hass(hass: HomeAssistant) {
@ -65,12 +71,6 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
}
}
static get properties(): PropertyDeclarations {
return {
_config: {},
};
}
public getCardSize(): number {
if (!this._config) {
return 0;
@ -100,7 +100,6 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
const { show_header_toggle, title } = this._config;
return html`
${this.renderStyle()}
<ha-card>
${!title && !show_header_toggle
? html``
@ -128,38 +127,52 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
padding: 16px;
}
#states {
margin: -4px 0;
}
#states > * {
margin: 8px 0;
}
#states > div > * {
overflow: hidden;
}
.header {
@apply --paper-font-headline;
/* overwriting line-height +8 because entity-toggle can be 40px height,
compensating this with reduced padding */
line-height: 40px;
color: var(--primary-text-color);
padding: 4px 0 12px;
display: flex;
justify-content: space-between;
}
.header .name {
@apply --paper-font-common-nowrap;
}
.state-card-dialog {
cursor: pointer;
}
</style>
static get styles(): CSSResult {
return css`
ha-card {
padding: 16px;
}
#states {
margin: -4px 0;
}
#states > * {
margin: 8px 0;
}
#states > div > * {
overflow: hidden;
}
.header {
/* start paper-font-headline style */
font-family: "Roboto", "Noto", sans-serif;
-webkit-font-smoothing: antialiased; /* OS X subpixel AA bleed bug */
text-rendering: optimizeLegibility;
font-size: 24px;
font-weight: 400;
letter-spacing: -0.012em;
/* end paper-font-headline style */
line-height: 40px;
color: var(--primary-text-color);
padding: 4px 0 12px;
display: flex;
justify-content: space-between;
}
.header .name {
/* start paper-font-common-nowrap style */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* end paper-font-common-nowrap */
}
.state-card-dialog {
cursor: pointer;
}
`;
}
@ -192,5 +205,3 @@ declare global {
"hui-entities-card": HuiEntitiesCard;
}
}
customElements.define("hui-entities-card", HuiEntitiesCard);

View File

@ -1,11 +1,12 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
CSSResult,
css,
customElement,
property,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import { styleMap } from "lit-html/directives/style-map";
@ -34,6 +35,7 @@ export interface Config extends LovelaceCardConfig {
hold_action?: ActionConfig;
}
@customElement("hui-entity-button-card")
class HuiEntityButtonCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-entity-button-card-editor" */ "../editor/config-elements/hui-entity-button-card-editor");
@ -47,15 +49,9 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
};
}
public hass?: HomeAssistant;
private _config?: Config;
@property() public hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
};
}
@property() private _config?: Config;
public getCardSize(): number {
return 2;
@ -147,11 +143,13 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
padding: 4% 0;
font-size: 1.2rem;
}
ha-icon {
width: 40%;
height: auto;
color: var(--paper-item-icon-color, #44739e);
}
ha-icon[data-domain="light"][data-state="on"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="binary_sensor"][data-state="on"],
@ -159,6 +157,7 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
ha-icon[data-domain="sun"][data-state="above_horizon"] {
color: var(--paper-item-icon-active-color, #fdd835);
}
ha-icon[data-state="unavailable"] {
color: var(--state-icon-unavailable-color);
}
@ -198,5 +197,3 @@ declare global {
"hui-entity-button-card": HuiEntityButtonCard;
}
}
customElements.define("hui-entity-button-card", HuiEntityButtonCard);

View File

@ -1,4 +1,12 @@
import { html, LitElement, TemplateResult } from "lit-element";
import {
html,
LitElement,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
@ -21,15 +29,11 @@ export const createErrorCardConfig = (error, origConfig) => ({
origConfig,
});
@customElement("hui-error-card")
export class HuiErrorCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant;
private _config?: Config;
static get properties() {
return {
_config: {},
};
}
@property() private _config?: Config;
public getCardSize(): number {
return 4;
@ -45,22 +49,20 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
}
return html`
${this.renderStyle()} ${this._config.error}
${this._config.error}
<pre>${this._toStr(this._config.origConfig)}</pre>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
display: block;
background-color: #ef5350;
color: white;
padding: 8px;
font-weight: 500;
}
</style>
static get styles(): CSSResult {
return css`
:host {
display: block;
background-color: #ef5350;
color: white;
padding: 8px;
font-weight: 500;
}
`;
}
@ -74,5 +76,3 @@ declare global {
"hui-error-card": HuiErrorCard;
}
}
customElements.define("hui-error-card", HuiErrorCard);

View File

@ -6,6 +6,7 @@ import {
css,
CSSResult,
property,
customElement,
} from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
@ -45,6 +46,7 @@ export const severityMap = {
normal: "var(--label-badge-blue)",
};
@customElement("hui-gauge-card")
class HuiGaugeCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-gauge-card-editor" */ "../editor/config-elements/hui-gauge-card-editor");
@ -55,7 +57,9 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
}
@property() public hass?: HomeAssistant;
@property() private _config?: Config;
private _updated?: boolean;
public getCardSize(): number {
@ -306,5 +310,3 @@ declare global {
"hui-gauge-card": HuiGaugeCard;
}
}
customElements.define("hui-gauge-card", HuiGaugeCard);

View File

@ -2,8 +2,11 @@ import {
html,
LitElement,
PropertyValues,
PropertyDeclarations,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
@ -29,34 +32,32 @@ export interface ConfigEntity extends EntityConfig {
hold_action?: ActionConfig;
}
export interface Config extends LovelaceCardConfig {
export interface GlanceCardConfig extends LovelaceCardConfig {
show_name?: boolean;
show_state?: boolean;
show_icon?: boolean;
title?: string;
theme?: string;
entities: ConfigEntity[];
columns?: number;
}
@customElement("hui-glance-card")
export class HuiGlanceCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-glance-card-editor" */ "../editor/config-elements/hui-glance-card-editor");
return document.createElement("hui-glance-card-editor");
}
public static getStubConfig(): object {
return { entities: [] };
}
public hass?: HomeAssistant;
private _config?: Config;
private _configEntities?: ConfigEntity[];
@property() public hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
};
}
@property() private _config?: GlanceCardConfig;
private _configEntities?: ConfigEntity[];
public getCardSize(): number {
return (
@ -65,7 +66,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
);
}
public setConfig(config: Config): void {
public setConfig(config: GlanceCardConfig): void {
this._config = { theme: "default", ...config };
const entities = processConfigEntities<ConfigEntity>(config.entities);
@ -120,7 +121,6 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
const { title } = this._config;
return html`
${this.renderStyle()}
<ha-card .header="${title}">
<div class="entities ${classMap({ "no-header": !title })}">
${this._configEntities!.map((entityConf) =>
@ -143,41 +143,39 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
}
}
private renderStyle(): TemplateResult {
return html`
<style>
.entities {
display: flex;
padding: 0 16px 4px;
flex-wrap: wrap;
}
.entities.no-header {
padding-top: 16px;
}
.entity {
box-sizing: border-box;
padding: 0 4px;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
margin-bottom: 12px;
width: var(--glance-column-width, 20%);
}
.entity div {
width: 100%;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.name {
min-height: var(--paper-font-body1_-_line-height, 20px);
}
state-badge {
margin: 8px 0;
}
</style>
static get styles(): CSSResult {
return css`
.entities {
display: flex;
padding: 0 16px 4px;
flex-wrap: wrap;
}
.entities.no-header {
padding-top: 16px;
}
.entity {
box-sizing: border-box;
padding: 0 4px;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
margin-bottom: 12px;
width: var(--glance-column-width, 20%);
}
.entity div {
width: 100%;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.name {
min-height: var(--paper-font-body1_-_line-height, 20px);
}
state-badge {
margin: 8px 0;
}
`;
}
@ -213,10 +211,14 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
</div>
`
: ""}
<state-badge
.stateObj="${stateObj}"
.overrideIcon="${entityConf.icon}"
></state-badge>
${this._config!.show_icon !== false
? html`
<state-badge
.stateObj="${stateObj}"
.overrideIcon="${entityConf.icon}"
></state-badge>
`
: ""}
${this._config!.show_state !== false
? html`
<div>
@ -232,12 +234,12 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
`;
}
private _handleTap(ev: MouseEvent) {
private _handleTap(ev: MouseEvent): void {
const config = (ev.currentTarget as any).entityConf as ConfigEntity;
handleClick(this, this.hass!, config, false);
}
private _handleHold(ev: MouseEvent) {
private _handleHold(ev: MouseEvent): void {
const config = (ev.currentTarget as any).entityConf as ConfigEntity;
handleClick(this, this.hass!, config, true);
}
@ -248,5 +250,3 @@ declare global {
"hui-glance-card": HuiGlanceCard;
}
}
customElements.define("hui-glance-card", HuiGlanceCard);

View File

@ -1,8 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import "../../../components/ha-card";
@ -17,6 +20,7 @@ export interface Config extends LovelaceCardConfig {
url: string;
}
@customElement("hui-iframe-card")
export class HuiIframeCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-iframe-card-editor" */ "../editor/config-elements/hui-iframe-card-editor");
@ -26,13 +30,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
return { url: "https://www.home-assistant.io", aspect_ratio: "50%" };
}
protected _config?: Config;
static get properties(): PropertyDeclarations {
return {
_config: {},
};
}
@property() protected _config?: Config;
public getCardSize(): number {
if (!this._config) {
@ -60,7 +58,6 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
const aspectRatio = this._config.aspect_ratio || "50%";
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<div
id="root"
@ -74,25 +71,25 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
overflow: hidden;
}
#root {
width: 100%;
position: relative;
}
iframe {
position: absolute;
border: none;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
</style>
static get styles(): CSSResult {
return css`
ha-card {
overflow: hidden;
}
#root {
width: 100%;
position: relative;
}
iframe {
position: absolute;
border: none;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
`;
}
}
@ -102,5 +99,3 @@ declare global {
"hui-iframe-card": HuiIframeCard;
}
}
customElements.define("hui-iframe-card", HuiIframeCard);

View File

@ -4,6 +4,7 @@ import {
PropertyValues,
TemplateResult,
property,
customElement,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
@ -45,6 +46,7 @@ export interface Config extends LovelaceCardConfig {
theme?: string;
}
@customElement("hui-light-card")
export class HuiLightCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-light-card-editor" */ "../editor/config-elements/hui-light-card-editor");
@ -55,9 +57,13 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
}
@property() public hass?: HomeAssistant;
@property() private _config?: Config;
@property() private _roundSliderStyle?: TemplateResult;
@property() private _jQuery?: any;
private _brightnessTimout?: number;
public getCardSize(): number {
@ -183,6 +189,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
:host {
display: block;
}
ha-card {
position: relative;
overflow: hidden;
@ -193,6 +200,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
--brightness-font-size: 1.2rem;
--rail-border-color: transparent;
}
#tooltip {
position: absolute;
top: 0;
@ -202,6 +210,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
text-align: center;
z-index: 15;
}
.icon-state {
display: block;
margin: auto;
@ -209,40 +218,50 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
height: 100%;
transform: translate(0, 25%);
}
#light {
margin: 0 auto;
padding-top: 16px;
padding-bottom: 16px;
}
#light .rs-bar.rs-transition.rs-first,
.rs-bar.rs-transition.rs-second {
z-index: 20 !important;
}
#light .rs-range-color {
background-color: var(--primary-color);
}
#light .rs-path-color {
background-color: var(--disabled-text-color);
}
#light .rs-handle {
background-color: var(--paper-card-background-color, white);
padding: 7px;
border: 2px solid var(--disabled-text-color);
}
#light .rs-handle.rs-focus {
border-color: var(--primary-color);
}
#light .rs-handle:after {
border-color: var(--primary-color);
background-color: var(--primary-color);
}
#light .rs-border {
border-color: var(--rail-border-color);
}
#light .rs-inner.rs-bg-color.rs-border,
#light .rs-overlay.rs-transition.rs-bg-color {
background-color: var(--paper-card-background-color, white);
}
.light-icon {
margin: auto;
width: 76px;
@ -250,16 +269,20 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
color: var(--paper-item-icon-color, #44739e);
cursor: pointer;
}
.light-icon[data-state="on"] {
color: var(--paper-item-icon-active-color, #fdd835);
}
.light-icon[data-state="unavailable"] {
color: var(--state-icon-unavailable-color);
}
.name {
padding-top: 40px;
font-size: var(--name-font-size);
}
.brightness {
font-size: var(--brightness-font-size);
position: absolute;
@ -276,9 +299,11 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
text-shadow: var(--brightness-font-text-shadow);
pointer-events: none;
}
.show_brightness {
opacity: 1;
}
.more-info {
position: absolute;
cursor: pointer;
@ -352,5 +377,3 @@ declare global {
"hui-light-card": HuiLightCard;
}
}
customElements.define("hui-light-card", HuiLightCard);

View File

@ -1,8 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
@ -17,22 +20,18 @@ export interface Config extends LovelaceCardConfig {
title?: string;
}
@customElement("hui-markdown-card")
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-markdown-card-editor" */ "../editor/config-elements/hui-markdown-card-editor");
return document.createElement("hui-markdown-card-editor");
}
public static getStubConfig(): object {
return { content: " " };
}
private _config?: Config;
static get properties(): PropertyDeclarations {
return {
_config: {},
};
}
@property() private _config?: Config;
public getCardSize(): number {
return this._config!.content.split("\n").length;
@ -52,7 +51,6 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
}
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<ha-markdown
class="markdown ${classMap({
@ -64,35 +62,40 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
@apply --paper-font-body1;
}
ha-markdown {
display: block;
padding: 0 16px 16px;
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.markdown.no-header {
padding-top: 16px;
}
ha-markdown > *:first-child {
margin-top: 0;
}
ha-markdown > *:last-child {
margin-bottom: 0;
}
ha-markdown a {
color: var(--primary-color);
}
ha-markdown img {
max-width: 100%;
}
</style>
static get styles(): CSSResult {
return css`
:host {
/* start paper-font-headline style */
font-family: "Roboto", "Noto", sans-serif;
-webkit-font-smoothing: antialiased; /* OS X subpixel AA bleed bug */
text-rendering: optimizeLegibility;
font-size: 24px;
font-weight: 400;
letter-spacing: -0.012em;
/* end paper-font-headline style */
}
ha-markdown {
display: block;
padding: 0 16px 16px;
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.markdown.no-header {
padding-top: 16px;
}
ha-markdown > *:first-child {
margin-top: 0;
}
ha-markdown > *:last-child {
margin-bottom: 0;
}
ha-markdown a {
color: var(--primary-color);
}
ha-markdown img {
max-width: 100%;
}
`;
}
}
@ -102,5 +105,3 @@ declare global {
"hui-markdown-card": HuiMarkdownCard;
}
}
customElements.define("hui-markdown-card", HuiMarkdownCard);

View File

@ -1,8 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import "../../../components/ha-card";
@ -20,6 +23,7 @@ export interface Config extends LovelaceCardConfig {
hold_action?: ActionConfig;
}
@customElement("hui-picture-card")
export class HuiPictureCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-picture-card-editor" */ "../editor/config-elements/hui-picture-card-editor");
@ -35,11 +39,8 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
}
public hass?: HomeAssistant;
protected _config?: Config;
static get properties(): PropertyDeclarations {
return { _config: {} };
}
@property() protected _config?: Config;
public getCardSize(): number {
return 3;
@ -59,7 +60,6 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
}
return html`
${this.renderStyle()}
<ha-card
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
@ -75,20 +75,20 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
overflow: hidden;
}
ha-card.clickable {
cursor: pointer;
}
img {
display: block;
width: 100%;
}
</style>
static get styles(): CSSResult {
return css`
ha-card {
overflow: hidden;
}
ha-card.clickable {
cursor: pointer;
}
img {
display: block;
width: 100%;
}
`;
}
@ -106,5 +106,3 @@ declare global {
"hui-picture-card": HuiPictureCard;
}
}
customElements.define("hui-picture-card", HuiPictureCard);

View File

@ -1,4 +1,12 @@
import { html, LitElement, TemplateResult } from "lit-element";
import {
html,
LitElement,
TemplateResult,
property,
customElement,
css,
CSSResult,
} from "lit-element";
import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
@ -17,15 +25,11 @@ interface Config extends LovelaceCardConfig {
elements: LovelaceElementConfig[];
}
@customElement("hui-picture-elements-card")
class HuiPictureElementsCard extends LitElement implements LovelaceCard {
private _config?: Config;
private _hass?: HomeAssistant;
@property() private _config?: Config;
static get properties() {
return {
_config: {},
};
}
private _hass?: HomeAssistant;
set hass(hass: HomeAssistant) {
this._hass = hass;
@ -60,7 +64,6 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
}
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<div id="root">
<hui-image
@ -84,20 +87,20 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
#root {
position: relative;
}
.element {
position: absolute;
transform: translate(-50%, -50%);
}
ha-card {
overflow: hidden;
}
</style>
static get styles(): CSSResult {
return css`
#root {
position: relative;
}
.element {
position: absolute;
transform: translate(-50%, -50%);
}
ha-card {
overflow: hidden;
}
`;
}
}
@ -107,5 +110,3 @@ declare global {
"hui-picture-elements-card": HuiPictureElementsCard;
}
}
customElements.define("hui-picture-elements-card", HuiPictureElementsCard);

View File

@ -1,8 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
@ -34,16 +37,11 @@ interface Config extends LovelaceCardConfig {
show_state?: boolean;
}
@customElement("hui-picture-entity-card")
class HuiPictureEntityCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant;
private _config?: Config;
@property() public hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
};
}
@property() private _config?: Config;
public getCardSize(): number {
return 3;
@ -109,7 +107,6 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
}
return html`
${this.renderStyle()}
<ha-card>
<hui-image
.hass="${this.hass}"
@ -132,37 +129,44 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
min-height: 75px;
overflow: hidden;
position: relative;
}
hui-image.clickable {
cursor: pointer;
}
.footer {
@apply --paper-font-common-nowrap;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
padding: 16px;
font-size: 16px;
line-height: 16px;
color: white;
}
.both {
display: flex;
justify-content: space-between;
}
.state {
text-align: right;
}
</style>
static get styles(): CSSResult {
return css`
ha-card {
min-height: 75px;
overflow: hidden;
position: relative;
}
hui-image.clickable {
cursor: pointer;
}
.footer {
/* start paper-font-common-nowrap style */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* end paper-font-common-nowrap style */
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
padding: 16px;
font-size: 16px;
line-height: 16px;
color: white;
}
.both {
display: flex;
justify-content: space-between;
}
.state {
text-align: right;
}
`;
}
@ -180,5 +184,3 @@ declare global {
"hui-picture-entity-card": HuiPictureEntityCard;
}
}
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);

View File

@ -1,8 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
@ -39,18 +42,15 @@ interface Config extends LovelaceCardConfig {
hold_action?: ActionConfig;
}
@customElement("hui-picture-glance-card")
class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant;
private _config?: Config;
private _entitiesDialog?: EntityConfig[];
private _entitiesToggle?: EntityConfig[];
@property() public hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
};
}
@property() private _config?: Config;
private _entitiesDialog?: EntityConfig[];
private _entitiesToggle?: EntityConfig[];
public getCardSize(): number {
return 3;
@ -91,7 +91,6 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
}
return html`
${this.renderStyle()}
<ha-card>
<hui-image
class="${classMap({
@ -177,44 +176,52 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
toggleEntity(this.hass!, (ev.target as any).entity);
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
position: relative;
min-height: 48px;
overflow: hidden;
}
hui-image.clickable {
cursor: pointer;
}
.box {
@apply --paper-font-common-nowrap;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
padding: 4px 8px;
font-size: 16px;
line-height: 40px;
color: white;
display: flex;
justify-content: space-between;
}
.box .title {
font-weight: 500;
margin-left: 8px;
}
ha-icon {
cursor: pointer;
padding: 8px;
color: #a9a9a9;
}
ha-icon.state-on {
color: white;
}
</style>
static get styles(): CSSResult {
return css`
ha-card {
position: relative;
min-height: 48px;
overflow: hidden;
}
hui-image.clickable {
cursor: pointer;
}
.box {
/* start paper-font-common-nowrap style */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* end paper-font-common-nowrap style */
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
padding: 4px 8px;
font-size: 16px;
line-height: 40px;
color: white;
display: flex;
justify-content: space-between;
}
.box .title {
font-weight: 500;
margin-left: 8px;
}
ha-icon {
cursor: pointer;
padding: 8px;
color: #a9a9a9;
}
ha-icon.state-on {
color: white;
}
`;
}
}
@ -224,5 +231,3 @@ declare global {
"hui-picture-glance-card": HuiPictureGlanceCard;
}
}
customElements.define("hui-picture-glance-card", HuiPictureGlanceCard);

View File

@ -1,26 +0,0 @@
import "../../../cards/ha-plant-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
// should be interface when converted to TS
export const Config = {
name: "",
entity: "",
};
class HuiPlantStatusCard extends LegacyWrapperCard {
static async getConfigElement() {
await import(/* webpackChunkName: "hui-plant-status-card-editor" */ "../editor/config-elements/hui-plant-status-card-editor");
return document.createElement("hui-plant-status-card-editor");
}
static getStubConfig() {
return {};
}
constructor() {
super("ha-plant-card", "plant");
}
}
customElements.define("hui-plant-status-card", HuiPlantStatusCard);

View File

@ -0,0 +1,237 @@
import {
html,
LitElement,
TemplateResult,
css,
CSSResult,
property,
customElement,
} from "lit-element";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { HassEntity } from "home-assistant-js-websocket";
import computeStateName from "../../../common/entity/compute_state_name";
import { LovelaceCardEditor, LovelaceCard } from "../types";
import { HomeAssistant } from "../../../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
const SENSORS = {
moisture: "hass:water",
temperature: "hass:thermometer",
brightness: "hass:white-balance-sunny",
conductivity: "hass:emoticon-poop",
battery: "hass:battery",
};
export interface PlantAttributeTarget extends EventTarget {
value?: string;
}
export interface PlantStatusConfig extends LovelaceCardConfig {
name?: string;
entity: string;
}
@customElement("hui-plant-status-card")
class HuiPlantStatusCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-plant-status-card-editor" */ "../editor/config-elements/hui-plant-status-card-editor");
return document.createElement("hui-plant-status-card-editor");
}
public static getStubConfig(): object {
return {};
}
@property() public hass?: HomeAssistant;
@property() private _config?: PlantStatusConfig;
public getCardSize(): number {
return 3;
}
public setConfig(config: PlantStatusConfig): void {
if (!config.entity || config.entity.split(".")[0] !== "plant") {
throw new Error("Specify an entity from within the plant domain.");
}
this._config = config;
}
protected render(): TemplateResult | void {
if (!this.hass || !this._config) {
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
>
`;
}
return html`
<ha-card
class="${stateObj.attributes.entity_picture ? "has-plant-image" : ""}"
>
<div
class="banner"
style="background-image:url(${stateObj.attributes.entity_picture})"
>
<div class="header">
${this._config.title || computeStateName(stateObj)}
</div>
</div>
<div class="content">
${this.computeAttributes(stateObj).map(
(item) => html`
<div
class="attributes"
@click="${this._handleMoreInfo}"
.value="${item}"
>
<div>
<ha-icon
icon="${this.computeIcon(
item,
stateObj.attributes.battery
)}"
></ha-icon>
</div>
<div
class="${stateObj.attributes.problem.indexOf(item) === -1
? ""
: "problem"}"
>
${stateObj.attributes[item]}
</div>
<div class="uom">
${stateObj.attributes.unit_of_measurement_dict[item] || ""}
</div>
</div>
`
)}
</div>
</ha-card>
`;
}
static get styles(): CSSResult {
return css`
.banner {
display: flex;
align-items: flex-end;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
padding-top: 12px;
}
.has-plant-image .banner {
padding-top: 30%;
}
.header {
/* start paper-font-headline style */
font-family: "Roboto", "Noto", sans-serif;
-webkit-font-smoothing: antialiased; /* OS X subpixel AA bleed bug */
text-rendering: optimizeLegibility;
font-size: 24px;
font-weight: 400;
letter-spacing: -0.012em;
/* end paper-font-headline style */
line-height: 40px;
padding: 8px 16px;
}
.has-plant-image .header {
font-size: 16px;
font-weight: 500;
line-height: 16px;
padding: 16px;
color: white;
width: 100%;
background: rgba(0, 0, 0, var(--dark-secondary-opacity));
}
.content {
display: flex;
justify-content: space-between;
padding: 16px 32px 24px 32px;
}
.has-plant-image .content {
padding-bottom: 16px;
}
ha-icon {
color: var(--paper-item-icon-color);
margin-bottom: 8px;
}
.attributes {
cursor: pointer;
}
.attributes div {
text-align: center;
}
.problem {
color: var(--google-red-500);
font-weight: bold;
}
.uom {
color: var(--secondary-text-color);
}
`;
}
private computeAttributes(stateObj: HassEntity): string[] {
return Object.keys(SENSORS).filter((key) => key in stateObj.attributes);
}
private computeIcon(attr: string, batLvl: number): string {
const icon = SENSORS[attr];
if (attr === "battery") {
if (batLvl <= 5) {
return `${icon}-alert`;
}
if (batLvl < 95) {
return `${icon}-${Math.round(batLvl / 10 - 0.01) * 10}`;
}
}
return icon;
}
private _handleMoreInfo(ev: Event): void {
const target = ev.currentTarget! as PlantAttributeTarget;
const stateObj = this.hass!.states[this._config!.entity];
if (target.value) {
fireEvent(this, "hass-more-info", {
entityId: stateObj.attributes.sensors[target.value],
});
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-plant-status-card": HuiPlantStatusCard;
}
}

View File

@ -2,9 +2,12 @@ import {
html,
svg,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import "@polymer/paper-spinner/paper-spinner";
@ -145,6 +148,7 @@ export interface Config extends LovelaceCardConfig {
hours_to_show?: number;
}
@customElement("hui-sensor-card")
class HuiSensorCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-sensor-card-editor" */ "../editor/config-elements/hui-sensor-card-editor");
@ -155,18 +159,13 @@ class HuiSensorCard extends LitElement implements LovelaceCard {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
private _history?: any;
private _date?: Date;
@property() public hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
_history: {},
};
}
@property() private _config?: Config;
@property() private _history?: any;
private _date?: Date;
public setConfig(config: Config): void {
if (!config.entity || config.entity.split(".")[0] !== "sensor") {
@ -244,7 +243,6 @@ class HuiSensorCard extends LitElement implements LovelaceCard {
graph = "";
}
return html`
${this.renderStyle()}
<ha-card @click="${this._handleClick}">
<div class="flex">
<div class="icon">
@ -324,87 +322,95 @@ class HuiSensorCard extends LitElement implements LovelaceCard {
this._date = new Date();
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
display: flex;
flex-direction: column;
}
ha-card {
display: flex;
flex-direction: column;
flex: 1;
padding: 16px;
position: relative;
cursor: pointer;
}
.flex {
display: flex;
}
.header {
align-items: center;
display: flex;
min-width: 0;
opacity: 0.8;
position: relative;
}
.name {
display: block;
display: -webkit-box;
font-size: 1.2rem;
font-weight: 500;
max-height: 1.4rem;
margin-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);
display: inline-block;
flex: 0 0 40px;
line-height: 40px;
position: relative;
text-align: center;
width: 40px;
}
.info {
flex-wrap: wrap;
margin: 16px 0 16px 8px;
}
#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%;
}
.graph > div {
align-self: flex-end;
margin: auto 8px;
}
</style>
static get styles(): CSSResult {
return css`
:host {
display: flex;
flex-direction: column;
}
ha-card {
display: flex;
flex-direction: column;
flex: 1;
padding: 16px;
position: relative;
cursor: pointer;
}
.flex {
display: flex;
}
.header {
align-items: center;
display: flex;
min-width: 0;
opacity: 0.8;
position: relative;
}
.name {
display: block;
display: -webkit-box;
font-size: 1.2rem;
font-weight: 500;
max-height: 1.4rem;
margin-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);
display: inline-block;
flex: 0 0 40px;
line-height: 40px;
position: relative;
text-align: center;
width: 40px;
}
.info {
flex-wrap: wrap;
margin: 16px 0 16px 8px;
}
#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%;
}
.graph > div {
align-self: flex-end;
margin: auto 8px;
}
`;
}
}
@ -414,5 +420,3 @@ declare global {
"hui-sensor-card": HuiSensorCard;
}
}
customElements.define("hui-sensor-card", HuiSensorCard);

View File

@ -5,6 +5,7 @@ import {
css,
CSSResult,
property,
customElement,
} from "lit-element";
import { repeat } from "lit-html/directives/repeat";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
@ -28,6 +29,7 @@ export interface Config extends LovelaceCardConfig {
title?: string;
}
@customElement("hui-shopping-list-card")
class HuiShoppingListCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-shopping-list-editor" */ "../editor/config-elements/hui-shopping-list-editor");
@ -39,9 +41,13 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
}
@property() public hass?: HomeAssistant;
@property() private _config?: Config;
@property() private _uncheckedItems?: ShoppingListItem[];
@property() private _checkedItems?: ShoppingListItem[];
private _unsubEvents?: Promise<() => Promise<void>>;
public getCardSize(): number {
@ -178,61 +184,68 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
`;
}
static get styles(): CSSResult[] {
return [
css`
.editRow,
.addRow {
display: flex;
flex-direction: row;
static get styles(): CSSResult {
return css`
.editRow,
.addRow {
display: flex;
flex-direction: row;
}
.addButton {
padding: 9px 15px 11px 15px;
cursor: pointer;
}
paper-item-body {
width: 75%;
}
paper-checkbox {
padding: 11px 11px 11px 18px;
}
paper-input {
--paper-input-container-underline: {
display: none;
}
.addButton {
padding: 9px 15px 11px 15px;
cursor: pointer;
--paper-input-container-underline-focus: {
display: none;
}
paper-item-body {
width: 75%;
--paper-input-container-underline-disabled: {
display: none;
}
paper-checkbox {
padding: 11px 11px 11px 18px;
}
paper-input {
--paper-input-container-underline: {
display: none;
}
--paper-input-container-underline-focus: {
display: none;
}
--paper-input-container-underline-disabled: {
display: none;
}
position: relative;
top: 1px;
}
.checked {
margin-left: 17px;
margin-bottom: 11px;
margin-top: 11px;
}
.label {
color: var(--primary-color);
}
.divider {
height: 1px;
background-color: var(--divider-color);
margin: 10px;
}
.clearall {
cursor: pointer;
margin-bottom: 3px;
float: right;
padding-right: 10px;
}
.addRow > ha-icon {
color: var(--secondary-text-color);
}
`,
];
position: relative;
top: 1px;
}
.checked {
margin-left: 17px;
margin-bottom: 11px;
margin-top: 11px;
}
.label {
color: var(--primary-color);
}
.divider {
height: 1px;
background-color: var(--divider-color);
margin: 10px;
}
.clearall {
cursor: pointer;
margin-bottom: 3px;
float: right;
padding-right: 10px;
}
.addRow > ha-icon {
color: var(--secondary-text-color);
}
`;
}
private async _fetchData(): Promise<void> {
@ -301,5 +314,3 @@ declare global {
"hui-shopping-list-card": HuiShoppingListCard;
}
}
customElements.define("hui-shopping-list-card", HuiShoppingListCard);

View File

@ -1,9 +1,10 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
customElement,
property,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "@polymer/paper-icon-button/paper-icon-button";
@ -52,6 +53,7 @@ export interface Config extends LovelaceCardConfig {
name?: string;
}
@customElement("hui-thermostat-card")
export class HuiThermostatCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-thermostat-card-editor" */ "../editor/config-elements/hui-thermostat-card-editor");
@ -62,22 +64,19 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
return { entity: "" };
}
public hass?: HomeAssistant;
private _config?: Config;
private _roundSliderStyle?: TemplateResult;
private _jQuery?: any;
private _broadCard?: boolean;
private _loaded?: boolean;
private _updated?: boolean;
@property() public hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
roundSliderStyle: {},
_jQuery: {},
};
}
@property() private _config?: Config;
@property() private _roundSliderStyle?: TemplateResult;
@property() private _jQuery?: any;
private _broadCard?: boolean;
private _loaded?: boolean;
private _updated?: boolean;
public getCardSize(): number {
return 4;
@ -574,5 +573,3 @@ declare global {
"hui-thermostat-card": HuiThermostatCard;
}
}
customElements.define("hui-thermostat-card", HuiThermostatCard);

View File

@ -1,8 +1,9 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
} from "lit-element";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
@ -31,15 +32,15 @@ declare global {
}
}
@customElement("hui-action-editor")
export class HuiActionEditor extends LitElement {
public config?: ActionConfig;
public label?: string;
public actions?: string[];
protected hass?: HomeAssistant;
@property() public config?: ActionConfig;
static get properties(): PropertyDeclarations {
return { hass: {}, config: {}, label: {}, actions: {} };
}
@property() public label?: string;
@property() public actions?: string[];
@property() protected hass?: HomeAssistant;
get _action(): string {
return this.config!.action || "";
@ -126,5 +127,3 @@ declare global {
"hui-action-editor": HuiActionEditor;
}
}
customElements.define("hui-action-editor", HuiActionEditor);

View File

@ -1,4 +1,12 @@
import { html, LitElement, PropertyDeclarations } from "lit-element";
import {
html,
LitElement,
customElement,
property,
css,
CSSResult,
TemplateResult,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-menu-button/paper-menu-button";
import "@polymer/paper-icon-button/paper-icon-button";
@ -12,63 +20,18 @@ import { Lovelace } from "../types";
import { swapCard } from "../editor/config-util";
import { showMoveCardViewDialog } from "../editor/card-editor/show-move-card-view-dialog";
@customElement("hui-card-options")
export class HuiCardOptions extends LitElement {
public cardConfig?: LovelaceCardConfig;
public hass?: HomeAssistant;
public lovelace?: Lovelace;
public path?: [number, number];
static get properties(): PropertyDeclarations {
return { hass: {}, lovelace: {}, path: {} };
}
@property() public hass?: HomeAssistant;
protected render() {
@property() public lovelace?: Lovelace;
@property() public path?: [number, number];
protected render(): TemplateResult | void {
return html`
<style>
div.options {
border-top: 1px solid #e8e8e8;
padding: 5px 8px;
background: var(--paper-card-background-color, white);
box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px,
rgba(0, 0, 0, 0.12) 0px 1px 5px -4px,
rgba(0, 0, 0, 0.2) 0px 3px 1px -2px;
display: flex;
}
div.options .primary-actions {
flex: 1;
margin: auto;
}
div.options .secondary-actions {
flex: 4;
text-align: right;
}
paper-icon-button {
color: var(--primary-text-color);
}
paper-icon-button.move-arrow[disabled] {
color: var(--disabled-text-color);
}
paper-menu-button {
color: var(--secondary-text-color);
padding: 0;
}
paper-item.header {
color: var(--primary-text-color);
text-transform: uppercase;
font-weight: 500;
font-size: 14px;
}
paper-item {
cursor: pointer;
}
</style>
<slot></slot>
<div class="options">
<div class="primary-actions">
@ -122,6 +85,54 @@ export class HuiCardOptions extends LitElement {
`;
}
static get styles(): CSSResult {
return css`
div.options {
border-top: 1px solid #e8e8e8;
padding: 5px 8px;
background: var(--paper-card-background-color, white);
box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px,
rgba(0, 0, 0, 0.12) 0px 1px 5px -4px,
rgba(0, 0, 0, 0.2) 0px 3px 1px -2px;
display: flex;
}
div.options .primary-actions {
flex: 1;
margin: auto;
}
div.options .secondary-actions {
flex: 4;
text-align: right;
}
paper-icon-button {
color: var(--primary-text-color);
}
paper-icon-button.move-arrow[disabled] {
color: var(--disabled-text-color);
}
paper-menu-button {
color: var(--secondary-text-color);
padding: 0;
}
paper-item.header {
color: var(--primary-text-color);
text-transform: uppercase;
font-weight: 500;
font-size: 14px;
}
paper-item {
cursor: pointer;
}
`;
}
private _editCard(): void {
showEditCardDialog(this, {
lovelace: this.lovelace!,
@ -162,5 +173,3 @@ declare global {
"hui-card-options": HuiCardOptions;
}
}
customElements.define("hui-card-options", HuiCardOptions);

View File

@ -1,9 +1,12 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import { PaperToggleButtonElement } from "@polymer/paper-toggle-button/paper-toggle-button";
@ -11,20 +14,15 @@ import { DOMAINS_TOGGLE } from "../../../common/const";
import { turnOnOffEntities } from "../common/entity/turn-on-off-entities";
import { HomeAssistant } from "../../../types";
@customElement("hui-entities-toggle")
class HuiEntitiesToggle extends LitElement {
public entities?: string[];
protected hass?: HomeAssistant;
private _toggleEntities?: string[];
@property() public entities?: string[];
static get properties(): PropertyDeclarations {
return {
hass: {},
entities: {},
_toggleEntities: {},
};
}
@property() protected hass?: HomeAssistant;
public updated(changedProperties: PropertyValues) {
@property() private _toggleEntities?: string[];
public updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("entities")) {
this._toggleEntities = this.entities!.filter(
@ -41,7 +39,6 @@ class HuiEntitiesToggle extends LitElement {
}
return html`
${this.renderStyle()}
<paper-toggle-button
?checked="${this._toggleEntities!.some((entityId) => {
const stateObj = this.hass!.states[entityId];
@ -52,20 +49,18 @@ class HuiEntitiesToggle extends LitElement {
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
width: 38px;
display: block;
}
paper-toggle-button {
cursor: pointer;
--paper-toggle-button-label-spacing: 0;
padding: 13px 5px;
margin: -4px -5px;
}
</style>
static get styles(): CSSResult {
return css`
:host {
width: 38px;
display: block;
}
paper-toggle-button {
cursor: pointer;
--paper-toggle-button-label-spacing: 0;
padding: 13px 5px;
margin: -4px -5px;
}
`;
}
@ -80,5 +75,3 @@ declare global {
"hui-entities-toggle": HuiEntitiesToggle;
}
}
customElements.define("hui-entities-toggle", HuiEntitiesToggle);

View File

@ -1,8 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import { HomeAssistant } from "../../../types";
@ -12,16 +15,11 @@ import { EntityConfig } from "../entity-rows/types";
import "../../../components/entity/ha-entity-picker";
import { EditorTarget } from "../editor/types";
@customElement("hui-entity-editor")
export class HuiEntityEditor extends LitElement {
protected hass?: HomeAssistant;
protected entities?: EntityConfig[];
@property() protected hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
entities: {},
};
}
@property() protected entities?: EntityConfig[];
protected render(): TemplateResult | void {
if (!this.entities) {
@ -29,7 +27,6 @@ export class HuiEntityEditor extends LitElement {
}
return html`
${this.renderStyle()}
<h3>Entities</h3>
<div class="entities">
${this.entities.map((entityConf, index) => {
@ -79,13 +76,11 @@ export class HuiEntityEditor extends LitElement {
fireEvent(this, "entities-changed", { entities: newConfigEntities });
}
private renderStyle(): TemplateResult {
return html`
<style>
.entities {
padding-left: 20px;
}
</style>
static get styles(): CSSResult {
return css`
.entities {
padding-left: 20px;
}
`;
}
}
@ -95,5 +90,3 @@ declare global {
"hui-entity-editor": HuiEntityEditor;
}
}
customElements.define("hui-entity-editor", HuiEntityEditor);

View File

@ -6,6 +6,7 @@ import {
CSSResult,
PropertyValues,
property,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../types";
@ -19,10 +20,12 @@ import "../components/hui-warning";
class HuiGenericEntityRow extends LitElement {
@property() public hass?: HomeAssistant;
@property() public config?: EntitiesCardEntityConfig;
@property() public showSecondary: boolean = true;
protected render() {
protected render(): TemplateResult | void {
if (!this.hass || !this.config) {
return html``;
}
@ -73,7 +76,7 @@ class HuiGenericEntityRow extends LitElement {
`;
}
protected updated(changedProps: PropertyValues) {
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("hass")) {
this.toggleAttribute("rtl", computeRTL(this.hass!));

View File

@ -12,6 +12,7 @@ import {
css,
PropertyValues,
query,
customElement,
} from "lit-element";
import { HomeAssistant } from "../../../types";
import { styleMap } from "lit-html/directives/style-map";
@ -26,33 +27,43 @@ export interface StateSpecificConfig {
[state: string]: string;
}
/*
* @appliesMixin LocalizeMixin
*/
@customElement("hui-image")
class HuiImage extends LitElement {
@property() public hass?: HomeAssistant;
@property() public entity?: string;
@property() public image?: string;
@property() public stateImage?: StateSpecificConfig;
@property() public cameraImage?: string;
@property() public aspectRatio?: string;
@property() public filter?: string;
@property() public stateFilter?: StateSpecificConfig;
@property() private _loadError?: boolean;
@property() private _cameraImageSrc?: string;
@query("img") private _image!: HTMLImageElement;
private _lastImageHeight?: number;
private _cameraUpdater?: number;
private _attached?: boolean;
public connectedCallback() {
public connectedCallback(): void {
super.connectedCallback();
this._attached = true;
this._startUpdateCameraInterval();
}
public disconnectedCallback() {
public disconnectedCallback(): void {
super.disconnectedCallback();
this._attached = false;
this._stopUpdateCameraInterval();
@ -137,7 +148,7 @@ class HuiImage extends LitElement {
}
}
private _startUpdateCameraInterval() {
private _startUpdateCameraInterval(): void {
this._stopUpdateCameraInterval();
if (this.cameraImage && this._attached) {
this._cameraUpdater = window.setInterval(
@ -147,23 +158,23 @@ class HuiImage extends LitElement {
}
}
private _stopUpdateCameraInterval() {
private _stopUpdateCameraInterval(): void {
if (this._cameraUpdater) {
clearInterval(this._cameraUpdater);
}
}
private _onImageError() {
private _onImageError(): void {
this._loadError = true;
}
private async _onImageLoad() {
private async _onImageLoad(): Promise<void> {
this._loadError = false;
await this.updateComplete;
this._lastImageHeight = this._image.offsetHeight;
}
private async _updateCameraImageSrc() {
private async _updateCameraImageSrc(): Promise<void> {
if (!this.hass || !this.cameraImage) {
return;
}
@ -221,5 +232,3 @@ declare global {
"hui-image": HuiImage;
}
}
customElements.define("hui-image", HuiImage);

View File

@ -0,0 +1,115 @@
import {
html,
css,
LitElement,
property,
TemplateResult,
CSSResult,
customElement,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { EditorTarget } from "../editor/types";
@customElement("hui-input-list-editor")
export class HuiInputListEditor extends LitElement {
@property() protected value?: string[];
@property() protected hass?: HomeAssistant;
@property() protected inputLabel?: string;
protected render(): TemplateResult | void {
if (!this.value) {
return html``;
}
return html`
${this.value.map((listEntry, index) => {
return html`
<paper-input
label="${this.inputLabel}"
.value="${listEntry}"
.configValue="${"entry"}"
.index="${index}"
@value-changed="${this._valueChanged}"
@blur="${this._consolidateEntries}"
><paper-icon-button
slot="suffix"
class="clear-button"
icon="hass:close"
no-ripple
@click="${this._removeEntry}"
>Clear</paper-icon-button
></paper-input
>
`;
})}
<paper-input
label="${this.inputLabel}"
@change="${this._addEntry}"
></paper-input>
`;
}
private _addEntry(ev: Event): void {
const target = ev.target! as EditorTarget;
if (target.value === "") {
return;
}
const newEntries = this.value!.concat(target.value as string);
target.value = "";
fireEvent(this, "value-changed", {
value: newEntries,
});
(ev.target! as LitElement).blur();
}
private _valueChanged(ev: Event): void {
ev.stopPropagation();
const target = ev.target! as EditorTarget;
const newEntries = this.value!.concat();
newEntries[target.index!] = target.value!;
fireEvent(this, "value-changed", {
value: newEntries,
});
}
private _consolidateEntries(ev: Event): void {
const target = ev.target! as EditorTarget;
if (target.value === "") {
const newEntries = this.value!.concat();
newEntries.splice(target.index!, 1);
fireEvent(this, "value-changed", {
value: newEntries,
});
}
}
private _removeEntry(ev: Event): void {
const parent = (ev.currentTarget as any).parentElement;
const newEntries = this.value!.concat();
newEntries.splice(parent.index!, 1);
fireEvent(this, "value-changed", {
value: newEntries,
});
}
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-input-list-editor": HuiInputListEditor;
}
}

View File

@ -1,8 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import "@material/mwc-button";
@ -20,16 +23,11 @@ declare global {
}
}
export class HuiThemeSelectionEditor extends LitElement {
public value?: string;
public hass?: HomeAssistant;
@customElement("hui-theme-select-editor")
export class HuiThemeSelectEditor extends LitElement {
@property() public value?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
value: {},
};
}
@property() public hass?: HomeAssistant;
protected render(): TemplateResult | void {
const themes = ["Backend-selected", "default"].concat(
@ -37,7 +35,6 @@ export class HuiThemeSelectionEditor extends LitElement {
);
return html`
${this.renderStyle()}
<paper-dropdown-menu
label="Theme"
dynamic-align
@ -58,13 +55,11 @@ export class HuiThemeSelectionEditor extends LitElement {
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
paper-dropdown-menu {
width: 100%;
}
</style>
static get styles(): CSSResult {
return css`
paper-dropdown-menu {
width: 100%;
}
`;
}
@ -79,8 +74,6 @@ export class HuiThemeSelectionEditor extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hui-theme-select-editor": HuiThemeSelectionEditor;
"hui-theme-select-editor": HuiThemeSelectEditor;
}
}
customElements.define("hui-theme-select-editor", HuiThemeSelectionEditor);

View File

@ -1,9 +1,10 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
customElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../../types";
@ -19,30 +20,32 @@ const FORMATS: { [key: string]: (ts: Date, lang: string) => string } = {
};
const INTERVAL_FORMAT = ["relative", "total"];
@customElement("hui-timestamp-display")
class HuiTimestampDisplay extends LitElement {
public hass?: HomeAssistant;
public ts?: Date;
public format?: "relative" | "total" | "date" | "datetime" | "time";
private _relative?: string;
@property() public hass?: HomeAssistant;
@property() public ts?: Date;
@property() public format?:
| "relative"
| "total"
| "date"
| "datetime"
| "time";
@property() private _relative?: string;
private _connected?: boolean;
private _interval?: number;
static get properties(): PropertyDeclarations {
return {
ts: {},
hass: {},
format: {},
_relative: {},
};
}
public connectedCallback() {
public connectedCallback(): void {
super.connectedCallback();
this._connected = true;
this._startInterval();
}
public disconnectedCallback() {
public disconnectedCallback(): void {
super.disconnectedCallback();
this._connected = false;
this._clearInterval();
@ -65,18 +68,18 @@ class HuiTimestampDisplay extends LitElement {
return html`
${this._relative}
`;
} else if (format in FORMATS) {
}
if (format in FORMATS) {
return html`
${FORMATS[format](this.ts, this.hass.language)}
`;
} else {
return html`
Invalid format
`;
}
return html`
Invalid format
`;
}
protected updated(changedProperties: PropertyValues) {
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (!changedProperties.has("format") || !this._connected) {
return;
@ -89,11 +92,11 @@ class HuiTimestampDisplay extends LitElement {
}
}
private get _format() {
private get _format(): string {
return this.format || "relative";
}
private _startInterval() {
private _startInterval(): void {
this._clearInterval();
if (this._connected && INTERVAL_FORMAT.includes(this._format)) {
this._updateRelative();
@ -101,14 +104,14 @@ class HuiTimestampDisplay extends LitElement {
}
}
private _clearInterval() {
private _clearInterval(): void {
if (this._interval) {
clearInterval(this._interval);
this._interval = undefined;
}
}
private _updateRelative() {
private _updateRelative(): void {
if (this.ts && this.hass!.localize) {
this._relative =
this._format === "relative"
@ -126,5 +129,3 @@ declare global {
"hui-timestamp-display": HuiTimestampDisplay;
}
}
customElements.define("hui-timestamp-display", HuiTimestampDisplay);

View File

@ -6,6 +6,7 @@ import codeMirrorCSS from "codemirror/lib/codemirror.css";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeRTL } from "../../../common/util/compute_rtl";
import { customElement } from "lit-element";
declare global {
interface HASSDomEvents {
@ -16,9 +17,12 @@ declare global {
}
}
@customElement("hui-yaml-editor")
export class HuiYamlEditor extends HTMLElement {
public _hass?: HomeAssistant;
public codemirror: CodeMirror;
private _value: string;
public constructor() {
@ -108,7 +112,7 @@ export class HuiYamlEditor extends HTMLElement {
fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() });
}
private setScrollBarDirection() {
private setScrollBarDirection(): void {
if (!this.codemirror) {
return;
}
@ -124,5 +128,3 @@ declare global {
"hui-yaml-editor": HuiYamlEditor;
}
}
window.customElements.define("hui-yaml-editor", HuiYamlEditor);

View File

@ -1,4 +1,11 @@
import { html, css, LitElement, TemplateResult, CSSResult } from "lit-element";
import {
html,
css,
LitElement,
TemplateResult,
CSSResult,
customElement,
} from "lit-element";
import "@material/mwc-button";
import { HomeAssistant } from "../../../../types";
@ -33,8 +40,10 @@ const cards = [
{ name: "Weather Forecast", type: "weather-forecast" },
];
@customElement("hui-card-picker")
export class HuiCardPicker extends LitElement {
public hass?: HomeAssistant;
public cardPicked?: (cardConf: LovelaceCardConfig) => void;
protected render(): TemplateResult | void {
@ -97,5 +106,3 @@ declare global {
"hui-card-picker": HuiCardPicker;
}
}
customElements.define("hui-card-picker", HuiCardPicker);

View File

@ -1,8 +1,9 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../../../types";
@ -23,18 +24,13 @@ declare global {
}
}
@customElement("hui-dialog-edit-card")
export class HuiDialogEditCard extends LitElement {
protected hass?: HomeAssistant;
private _params?: EditCardDialogParams;
private _cardConfig?: LovelaceCardConfig;
@property() protected hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
_params: {},
_cardConfig: {},
};
}
@property() private _params?: EditCardDialogParams;
@property() private _cardConfig?: LovelaceCardConfig;
constructor() {
super();
@ -78,11 +74,11 @@ export class HuiDialogEditCard extends LitElement {
`;
}
private _cardPicked(cardConf: LovelaceCardConfig) {
private _cardPicked(cardConf: LovelaceCardConfig): void {
this._cardConfig = cardConf;
}
private _cancel() {
private _cancel(): void {
this._params = undefined;
this._cardConfig = undefined;
}
@ -93,5 +89,3 @@ declare global {
"hui-dialog-edit-card": HuiDialogEditCard;
}
}
customElements.define("hui-dialog-edit-card", HuiDialogEditCard);

View File

@ -1,8 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-item/paper-item";
@ -13,14 +16,9 @@ import { moveCard } from "../config-util";
import { MoveCardViewDialogParams } from "./show-move-card-view-dialog";
import { PolymerChangedEvent } from "../../../../polymer-types";
@customElement("hui-dialog-move-card-view")
export class HuiDialogMoveCardView extends LitElement {
private _params?: MoveCardViewDialogParams;
static get properties(): PropertyDeclarations {
return {
_params: {},
};
}
@property() private _params?: MoveCardViewDialogParams;
public async showDialog(params: MoveCardViewDialogParams): Promise<void> {
this._params = params;
@ -32,29 +30,6 @@ export class HuiDialogMoveCardView extends LitElement {
return html``;
}
return html`
<style>
paper-item {
margin: 8px;
cursor: pointer;
}
paper-item[active] {
color: var(--primary-color);
}
paper-item[active]:before {
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: "";
background-color: var(--primary-color);
opacity: 0.12;
transition: opacity 15ms linear;
will-change: opacity;
}
</style>
<paper-dialog
with-backdrop
opened
@ -75,6 +50,32 @@ export class HuiDialogMoveCardView extends LitElement {
`;
}
static get styles(): CSSResult {
return css`
paper-item {
margin: 8px;
cursor: pointer;
}
paper-item[active] {
color: var(--primary-color);
}
paper-item[active]:before {
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: "";
background-color: var(--primary-color);
opacity: 0.12;
transition: opacity 15ms linear;
will-change: opacity;
}
`;
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
}
@ -104,5 +105,3 @@ declare global {
"hui-dialog-move-card-view": HuiDialogMoveCardView;
}
}
customElements.define("hui-dialog-move-card-view", HuiDialogMoveCardView);

View File

@ -2,9 +2,9 @@ import {
html,
css,
LitElement,
PropertyDeclarations,
TemplateResult,
CSSResult,
customElement,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
@ -15,15 +15,12 @@ import "./hui-card-picker";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardConfig } from "../../../../data/lovelace";
@customElement("hui-dialog-pick-card")
export class HuiDialogPickCard extends LitElement {
public hass?: HomeAssistant;
public cardPicked?: (cardConf: LovelaceCardConfig) => void;
public closeDialog?: () => void;
static get properties(): PropertyDeclarations {
return {};
}
protected render(): TemplateResult | void {
return html`
<paper-dialog
@ -47,7 +44,7 @@ export class HuiDialogPickCard extends LitElement {
`;
}
private _openedChanged(ev) {
private _openedChanged(ev): void {
if (!ev.detail.value) {
this.closeDialog!();
}
@ -88,5 +85,3 @@ declare global {
"hui-dialog-pick-card": HuiDialogPickCard;
}
}
customElements.define("hui-dialog-pick-card", HuiDialogPickCard);

View File

@ -2,10 +2,11 @@ import {
html,
css,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
CSSResult,
customElement,
property,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import yaml from "js-yaml";
@ -49,36 +50,33 @@ declare global {
}
}
@customElement("hui-edit-card")
export class HuiEditCard extends LitElement {
public hass?: HomeAssistant;
public lovelace?: Lovelace;
public path?: [number] | [number, number];
public cardConfig?: LovelaceCardConfig;
public closeDialog?: () => void;
private _configElement?: LovelaceCardEditor | null;
private _uiEditor?: boolean;
private _configValue?: ConfigValue;
private _configState?: string;
private _loading?: boolean;
private _saving: boolean;
private _errorMsg?: TemplateResult;
private _cardType?: string;
@property() public hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {
hass: {},
cardConfig: {},
viewIndex: {},
_cardIndex: {},
_configElement: {},
_configValue: {},
_configState: {},
_errorMsg: {},
_uiEditor: {},
_saving: {},
_loading: {},
};
}
@property() public cardConfig?: LovelaceCardConfig;
public lovelace?: Lovelace;
public path?: [number] | [number, number];
public closeDialog?: () => void;
@property() private _configElement?: LovelaceCardEditor | null;
@property() private _uiEditor?: boolean;
@property() private _configValue?: ConfigValue;
@property() private _configState?: string;
@property() private _loading?: boolean;
@property() private _saving: boolean;
@property() private _errorMsg?: TemplateResult;
private _cardType?: string;
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
@ -88,7 +86,7 @@ export class HuiEditCard extends LitElement {
return this.shadowRoot!.querySelector("hui-card-preview")!;
}
protected constructor() {
public constructor() {
super();
this._saving = false;
}
@ -270,7 +268,7 @@ export class HuiEditCard extends LitElement {
this._updatePreview(value);
}
private async _updatePreview(config: LovelaceCardConfig) {
private async _updatePreview(config: LovelaceCardConfig): Promise<void> {
await this.updateComplete;
if (!this._previewEl) {
@ -286,7 +284,7 @@ export class HuiEditCard extends LitElement {
}
}
private _setPreviewError(error: ConfigError) {
private _setPreviewError(error: ConfigError): void {
if (!this._previewEl) {
return;
}
@ -323,7 +321,7 @@ export class HuiEditCard extends LitElement {
this._resizeDialog();
}
private _isConfigValid() {
private _isConfigValid(): boolean {
if (!this._configValue || !this._configValue.value) {
return false;
}
@ -401,7 +399,7 @@ export class HuiEditCard extends LitElement {
return this.path!.length === 1;
}
private _openedChanged(ev) {
private _openedChanged(ev): void {
if (!ev.detail.value) {
this.closeDialog!();
}
@ -518,5 +516,3 @@ declare global {
"hui-edit-card": HuiEditCard;
}
}
customElements.define("hui-edit-card", HuiEditCard);

View File

@ -17,7 +17,7 @@ export interface EditCardDialogParams {
path: [number] | [number, number];
}
const registerEditCardDialog = (element: HTMLElement) =>
const registerEditCardDialog = (element: HTMLElement): Event =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
@ -28,7 +28,7 @@ const registerEditCardDialog = (element: HTMLElement) =>
export const showEditCardDialog = (
element: HTMLElement,
editCardDialogParams: EditCardDialogParams
) => {
): void => {
if (!registeredDialog) {
registeredDialog = true;
registerEditCardDialog(element);

View File

@ -15,7 +15,7 @@ export interface MoveCardViewDialogParams {
lovelace: Lovelace;
}
const registerEditCardDialog = (element: HTMLElement) =>
const registerEditCardDialog = (element: HTMLElement): Event =>
fireEvent(element, "register-dialog", {
dialogShowEvent: "show-move-card-view",
dialogTag: "hui-dialog-move-card-view",
@ -26,7 +26,7 @@ const registerEditCardDialog = (element: HTMLElement) =>
export const showMoveCardViewDialog = (
element: HTMLElement,
moveCardViewDialogParams: MoveCardViewDialogParams
) => {
): void => {
if (!registeredDialog) {
registeredDialog = true;
registerEditCardDialog(element);

View File

@ -1,8 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
CSSResult,
css,
} from "lit-element";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
@ -26,20 +29,18 @@ const cardConfigStruct = struct({
states: "array?",
});
@customElement("hui-alarm-panel-card-editor")
export class HuiAlarmPanelCardEditor extends LitElement
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
@property() public hass?: HomeAssistant;
@property() private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
@ -60,7 +61,7 @@ export class HuiAlarmPanelCardEditor extends LitElement
const states = ["arm_home", "arm_away", "arm_night", "arm_custom_bypass"];
return html`
${configElementStyle} ${this.renderStyle()}
${configElementStyle}
<div class="card-config">
<div class="side-by-side">
<paper-input
@ -107,23 +108,21 @@ export class HuiAlarmPanelCardEditor extends LitElement
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
.states {
display: flex;
flex-direction: row;
}
.deleteState {
visibility: hidden;
}
.states:hover > .deleteState {
visibility: visible;
}
ha-icon {
padding-top: 12px;
}
</style>
static get styles(): CSSResult {
return css`
.states {
display: flex;
flex-direction: row;
}
.deleteState {
visibility: hidden;
}
.states:hover > .deleteState {
visibility: visible;
}
ha-icon {
padding-top: 12px;
}
`;
}
@ -190,5 +189,3 @@ declare global {
"hui-alarm-panel-card-editor": HuiAlarmPanelCardEditor;
}
}
customElements.define("hui-alarm-panel-card-editor", HuiAlarmPanelCardEditor);

View File

@ -1,8 +1,9 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
} from "lit-element";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
@ -44,10 +45,19 @@ const cardConfigStruct = struct({
entities: [entitiesConfigStruct],
});
@customElement("hui-entities-card-editor")
export class HuiEntitiesCardEditor extends LitElement
implements LovelaceCardEditor {
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {}, _configEntities: {} };
@property() public hass?: HomeAssistant;
@property() private _config?: EntitiesCardConfig;
@property() private _configEntities?: EntitiesCardEntityConfig[];
public setConfig(config: EntitiesCardConfig): void {
config = cardConfigStruct(config);
this._config = config;
this._configEntities = processEditorEntities(config.entities);
}
get _title(): string {
@ -58,16 +68,6 @@ export class HuiEntitiesCardEditor extends LitElement
return this._config!.theme || "Backend-selected";
}
public hass?: HomeAssistant;
private _config?: EntitiesCardConfig;
private _configEntities?: EntitiesCardEntityConfig[];
public setConfig(config: EntitiesCardConfig): void {
config = cardConfigStruct(config);
this._config = config;
this._configEntities = processEditorEntities(config.entities);
}
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
@ -141,5 +141,3 @@ declare global {
"hui-entities-card-editor": HuiEntitiesCardEditor;
}
}
customElements.define("hui-entities-card-editor", HuiEntitiesCardEditor);

View File

@ -1,8 +1,9 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
property,
} from "lit-element";
import "@polymer/paper-input/paper-input";
@ -33,20 +34,18 @@ const cardConfigStruct = struct({
theme: "string?",
});
@customElement("hui-entity-button-card-editor")
export class HuiEntityButtonCardEditor extends LitElement
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
@property() public hass?: HomeAssistant;
@property() private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
@ -161,8 +160,3 @@ declare global {
"hui-entity-button-card-editor": HuiEntityButtonCardEditor;
}
}
customElements.define(
"hui-entity-button-card-editor",
HuiEntityButtonCardEditor
);

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