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):** **Last working Home Assistant release (if known):**
**UI (States or Lovelace UI?):**
<!--
- Frontend -> Developer tools -> Info
-->
**Browser and Operating System:** **Browser and Operating System:**
<!-- <!--

View File

@ -1,6 +1,6 @@
const webpack = require("webpack"); const webpack = require("webpack");
const path = require("path"); const path = require("path");
const BabelMinifyPlugin = require("babel-minify-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
module.exports.resolve = { module.exports.resolve = {
extensions: [".ts", ".js", ".json", ".tsx"], extensions: [".ts", ".js", ".json", ".tsx"],
@ -29,32 +29,15 @@ module.exports.plugins = [
), ),
]; ];
module.exports.optimization = { module.exports.optimization = (latestBuild) => ({
minimizer: [ minimizer: [
// Took options from Polymer build tool new TerserPlugin({
// https://github.com/Polymer/tools/blob/master/packages/build/src/js-transform.ts cache: true,
new BabelMinifyPlugin( parallel: true,
{ extractComments: true,
// Disable the minify-constant-folding plugin because it has a bug relating terserOptions: {
// to invalid substitution of constant values into export specifiers: ecma: latestBuild ? undefined : 5,
// 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,
}, },
{} }),
),
], ],
}; });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ import {
} from "lit-element"; } from "lit-element";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event"; 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"; import compare from "../../common/string/compare";
class HaEntityPicker extends LitElement { class HaEntityPicker extends LitElement {

View File

@ -1,26 +1,9 @@
import { HomeAssistant } from "../types";
export interface AuthProvider { export interface AuthProvider {
name: string; name: string;
id: string; id: string;
type: string; type: string;
} }
interface Credential { export interface Credential {
type: string; 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 { export interface CameraThumbnail {
content_type: string; content_type: string;
content: 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) => export const fetchThumbnail = (hass: HomeAssistant, entityId: string) =>
hass.callWS<CameraThumbnail>({ hass.callWS<CameraThumbnail>({
type: "camera_thumbnail", type: "camera_thumbnail",
entity_id: entityId, 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"; 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 { export interface CloudWebhook {
webhook_id: string; webhook_id: string;
cloudhook_id: string; cloudhook_id: string;
@ -18,3 +58,29 @@ export const deleteCloudhook = (hass: HomeAssistant, webhookId: string) =>
type: "cloud/cloudhook/delete", type: "cloud/cloudhook/delete",
webhook_id: webhookId, 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; sw_version?: string;
hub_device_id?: string; hub_device_id?: string;
area_id?: string; area_id?: string;
name_by_user?: string;
} }
export interface DeviceRegistryEntryMutableParams { export interface DeviceRegistryEntryMutableParams {
area_id: string; area_id?: string;
name_by_user?: string;
} }
export const fetchDeviceRegistry = (hass: HomeAssistant) => 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 { HassEntity } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
export interface ZHADeviceEntity extends HassEntity {
device_info?: {
identifiers: any[];
};
}
export interface ZHAEntityReference extends HassEntity { export interface ZHAEntityReference extends HassEntity {
name: string; name: string;
} }
@ -20,6 +14,8 @@ export interface ZHADevice {
quirk_class: string; quirk_class: string;
entities: ZHAEntityReference[]; entities: ZHAEntityReference[];
manufacturer_code: number; manufacturer_code: number;
device_reg_id: string;
user_given_name: string;
} }
export interface Attribute { export interface Attribute {
@ -78,6 +74,37 @@ export const fetchDevices = (hass: HomeAssistant): Promise<ZHADevice[]> =>
type: "zha/devices", 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 = ( export const readAttributeValue = (
hass: HomeAssistant, hass: HomeAssistant,
data: ReadAttributeServiceData 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 { getEntity, Entity } from "./entity";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { HassEntities } from "home-assistant-js-websocket"; 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"; import { translationMetadata } from "../resources/translations-metadata";
const ensureArray = <T>(val: T | T[]): T[] => const ensureArray = <T>(val: T | T[]): T[] =>
@ -87,6 +87,8 @@ export const provideHass = (
); );
}); });
const localLanguage = getLocalLanguage();
const hassObj: MockHomeAssistant = { const hassObj: MockHomeAssistant = {
// Home Assistant properties // Home Assistant properties
auth: {} as any, auth: {} as any,
@ -128,13 +130,15 @@ export const provideHass = (
user: { user: {
credentials: [], credentials: [],
id: "abcd", id: "abcd",
is_admin: true,
is_owner: true, is_owner: true,
mfa_modules: [], mfa_modules: [],
name: "Demo User", name: "Demo User",
}, },
panelUrl: "lovelace", panelUrl: "lovelace",
language: getActiveTranslation(), language: localLanguage,
selectedLanguage: localLanguage,
resources: null as any, resources: null as any,
localize: () => "", localize: () => "",

View File

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

View File

@ -24,7 +24,7 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.addEventListener("hass-dock-sidebar", (ev) => { this.addEventListener("hass-dock-sidebar", (ev) => {
this._updateHass({ dockedSidebar: ev.detail.dock }); 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 applyThemesOnElement from "../../common/dom/apply_themes_on_element";
import { storeState } from "../../util/ha-pref-storage"; import { storeState } from "../../util/ha-pref-storage";
import { subscribeThemes } from "../../data/ws-themes"; 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 { class extends superClass {
firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.addEventListener("settheme", (ev) => { this.addEventListener("settheme", (ev) => {
this._updateHass({ selectedTheme: ev.detail }); this._updateHass({ selectedTheme: ev.detail });
this._applyTheme(); this._applyTheme();
storeState(this.hass); storeState(this.hass!);
}); });
} }
hassConnected() { protected hassConnected() {
super.hassConnected(); super.hassConnected();
subscribeThemes(this.hass.connection, (themes) => { subscribeThemes(this.hass!.connection, (themes) => {
this._updateHass({ themes }); this._updateHass({ themes });
this._applyTheme(); this._applyTheme();
}); });
} }
_applyTheme() { private _applyTheme() {
applyThemesOnElement( applyThemesOnElement(
document.documentElement, document.documentElement,
this.hass.themes, this.hass!.themes,
this.hass.selectedTheme, this.hass!.selectedTheme,
true true
); );
} }

View File

@ -1,11 +1,17 @@
import { translationMetadata } from "../../resources/translations-metadata"; import { translationMetadata } from "../../resources/translations-metadata";
import { getTranslation } from "../../util/hass-translation"; import {
import { storeState } from "../../util/ha-pref-storage"; getTranslation,
getLocalLanguage,
getUserLanguage,
} from "../../util/hass-translation";
import { Constructor, LitElement } from "lit-element"; import { Constructor, LitElement } from "lit-element";
import { HassBaseEl } from "./hass-base-mixin"; import { HassBaseEl } from "./hass-base-mixin";
import { computeLocalize } from "../../common/translations/localize"; import { computeLocalize } from "../../common/translations/localize";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import { HomeAssistant } from "../../types"; 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`. * superClass needs to contain `this.hass` and `this._updateHass`.
@ -16,60 +22,86 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.addEventListener("hass-language-select", (e) => this.addEventListener("hass-language-select", (e) =>
this._selectLanguage(e) this._selectLanguage((e as CustomEvent).detail.language, true)
); );
this._loadResources(); this._loadCoreTranslations(getLocalLanguage());
} }
protected hassConnected() { protected hassConnected() {
super.hassConnected(); super.hassConnected();
this._loadBackendTranslations(); getUserLanguage(this.hass!).then((language) => {
this.style.direction = computeRTL(this.hass!) ? "rtl" : "ltr"; 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() { protected hassReconnected() {
super.hassReconnected(); super.hassReconnected();
this._loadBackendTranslations(); this._applyTranslations(this.hass!);
} }
protected panelUrlChanged(newPanelUrl) { protected panelUrlChanged(newPanelUrl) {
super.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() { private _selectLanguage(language: string, saveToBackend: boolean) {
const hass = this.hass; if (!this.hass) {
if (!hass || !hass.language) { // should not happen, do it to avoid use this.hass!
return; 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({ this._applyTranslations(this.hass);
type: "frontend/get_translations", }
language,
});
// If we've switched selected languages just ignore this response private _applyTranslations(hass: HomeAssistant) {
if ((hass.selectedLanguage || hass.language) !== language) { 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; return;
} }
this._updateResources(language, resources); this._updateResources(language, resources);
} }
private _loadTranslationFragment(panelUrl) { private async _loadFragmentTranslations(
language: string,
panelUrl: string
) {
if (translationMetadata.fragments.includes(panelUrl)) { if (translationMetadata.fragments.includes(panelUrl)) {
this._loadResources(panelUrl); const result = await getTranslation(panelUrl, language);
this._updateResources(result.language, result.data);
} }
} }
private async _loadResources(fragment?) { private async _loadCoreTranslations(language: string) {
const result = await getTranslation(fragment); const result = await getTranslation(null, language);
this._updateResources(result.language, result.data); 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 // 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 // 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 // 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); 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, PropertyDeclarations,
PropertyValues, PropertyValues,
} from "lit-element"; } from "lit-element";
import { getActiveTranslation } from "../util/hass-translation"; import { getLocalLanguage } from "../util/hass-translation";
import { localizeLiteBaseMixin } from "./localize-lite-base-mixin"; import { localizeLiteBaseMixin } from "./localize-lite-base-mixin";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize"; import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
@ -35,7 +35,8 @@ export const litLocalizeLiteMixin = <T extends LitElement>(
super(); super();
// This will prevent undefined errors if called before connected to DOM. // This will prevent undefined errors if called before connected to DOM.
this.localize = empty; this.localize = empty;
this.language = getActiveTranslation(); // Use browser language setup before login.
this.language = getLocalLanguage();
} }
public connectedCallback(): void { public connectedCallback(): void {

View File

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

View File

@ -2,7 +2,7 @@
* Lite mixin to add localization without depending on the Hass object. * Lite mixin to add localization without depending on the Hass object.
*/ */
import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin"; 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 { localizeLiteBaseMixin } from "./localize-lite-base-mixin";
import { computeLocalize } from "../common/translations/localize"; import { computeLocalize } from "../common/translations/localize";
@ -16,7 +16,8 @@ export const localizeLiteMixin = dedupingMixin(
return { return {
language: { language: {
type: String, type: String,
value: getActiveTranslation(), // Use browser language setup before login.
value: getLocalLanguage(),
}, },
resources: Object, resources: Object,
// The fragment to load. // 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; this._params = undefined;
} catch (err) { } catch (err) {
this._error = err; this._error = err.message || "Unknown error";
} finally { } finally {
this._submitting = false; this._submitting = false;
} }

View File

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

View File

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

View File

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

View File

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

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

View File

@ -10,23 +10,15 @@ import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import "../../../components/ha-card"; import "../../../components/ha-card";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant, WebhookError } from "../../../types"; import { HomeAssistant, WebhookError } from "../../../types";
import { WebhookDialogParams, CloudStatusLoggedIn } from "./types";
import { Webhook, fetchWebhooks } from "../../../data/webhook"; import { Webhook, fetchWebhooks } from "../../../data/webhook";
import { import {
createCloudhook, createCloudhook,
deleteCloudhook, deleteCloudhook,
CloudWebhook, CloudWebhook,
CloudStatusLoggedIn,
} from "../../../data/cloud"; } from "../../../data/cloud";
import { showManageCloudhookDialog } from "./show-cloud-webhook-manage-dialog";
declare global {
// for fire event
interface HASSDomEvents {
"manage-cloud-webhook": WebhookDialogParams;
}
}
export class CloudWebhooks extends LitElement { export class CloudWebhooks extends LitElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
@ -138,14 +130,13 @@ export class CloudWebhooks extends LitElement {
private _showDialog(webhookId: string) { private _showDialog(webhookId: string) {
const webhook = this._localHooks!.find( const webhook = this._localHooks!.find(
(ent) => ent.webhook_id === webhookId (ent) => ent.webhook_id === webhookId
); )!;
const cloudhook = this._cloudHooks![webhookId]; const cloudhook = this._cloudHooks![webhookId];
const params: WebhookDialogParams = { showManageCloudhookDialog(this, {
webhook: webhook!, webhook,
cloudhook, cloudhook,
disableHook: () => this._disableWebhook(webhookId), disableHook: () => this._disableWebhook(webhookId),
}; });
fireEvent(this, "manage-cloud-webhook", params);
} }
private _handleManageButton(ev: MouseEvent) { 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 EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { fetchCloudSubscriptionInfo } from "../../../data/cloud";
import { fetchSubscriptionInfo } from "./data";
import "./cloud-alexa-pref"; import "./cloud-alexa-pref";
import "./cloud-google-pref"; import "./cloud-google-pref";
import "./cloud-remote-pref";
let registeredWebhookDialog = false; let registeredWebhookDialog = false;
@ -66,6 +66,9 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
text-transform: capitalize; text-transform: capitalize;
padding: 16px; padding: 16px;
} }
a {
color: var(--primary-color);
}
</style> </style>
<hass-subpage header="Home Assistant Cloud"> <hass-subpage header="Home Assistant Cloud">
<div class="content"> <div class="content">
@ -83,7 +86,7 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
<div class="account-row"> <div class="account-row">
<paper-item-body two-line=""> <paper-item-body two-line="">
[[cloudStatus.email]] [[cloudStatus.email]]
<div secondary="" class="wrap"> <div secondary class="wrap">
[[_formatSubscription(_subscription)]] [[_formatSubscription(_subscription)]]
</div> </div>
</paper-item-body> </paper-item-body>
@ -121,6 +124,11 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
</p> </p>
</div> </div>
<cloud-remote-pref
hass="[[hass]]"
cloud-status="[[cloudStatus]]"
></cloud-remote-pref>
<cloud-alexa-pref <cloud-alexa-pref
hass="[[hass]]" hass="[[hass]]"
cloud-status="[[cloudStatus]]" cloud-status="[[cloudStatus]]"
@ -172,8 +180,12 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
} }
} }
_computeRemoteConnected(connected) {
return connected ? "Connected" : "Not Connected";
}
async _fetchSubscriptionInfo() { async _fetchSubscriptionInfo() {
this._subscription = await fetchSubscriptionInfo(this.hass); this._subscription = await fetchCloudSubscriptionInfo(this.hass);
if ( if (
this._subscription.provider && this._subscription.provider &&
this.cloudStatus && this.cloudStatus &&

View File

@ -74,8 +74,10 @@ class HaConfigCloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
<span slot="header">Home Assistant Cloud</span> <span slot="header">Home Assistant Cloud</span>
<div slot="introduction"> <div slot="introduction">
<p> <p>
Home Assistant Cloud connects your local instance securely to Home Assistant Cloud provides you with a secure remote
cloud-only services Amazon Alexa and Google Assistant. 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>
<p> <p>
This service is run by our partner 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: The trial will give you access to all the benefits of Home Assistant Cloud, including:
</p> </p>
<ul> <ul>
<li>Control of Home Assistant away from home</li>
<li>Integration with Google Assistant</li> <li>Integration with Google Assistant</li>
<li>Integration with Amazon Alexa</li> <li>Integration with Amazon Alexa</li>
<li>Easy integration with webhook-based apps like OwnTracks</li>
</ul> </ul>
<p> <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. 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; this._params = undefined;
} catch (err) { } catch (err) {
this._error = err; this._error = err.message || "Unknown error";
} finally { } finally {
this._submitting = false; this._submitting = false;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import NavigateMixin from "../../../mixins/navigate-mixin";
import "./ha-config-user-picker"; import "./ha-config-user-picker";
import "./ha-user-editor"; import "./ha-user-editor";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { fetchUsers } from "../../../data/auth"; import { fetchUsers } from "../../../data/user";
/* /*
* @appliesMixin NavigateMixin * @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 { import {
html, html,
LitElement, LitElement,
PropertyDeclarations, property,
PropertyValues,
TemplateResult, TemplateResult,
CSSResult, CSSResult,
} from "lit-element"; } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { HassEntity } from "home-assistant-js-websocket";
import { HASSDomEvent } from "../../../common/dom/fire_event"; 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 "../../../layouts/ha-app-layout";
import "../../../components/ha-paper-icon-button-arrow-prev"; import "../../../components/ha-paper-icon-button-arrow-prev";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { ZHAClusterSelectedParams, ZHANodeSelectedParams } from "./types"; import { ZHAClusterSelectedParams, ZHADeviceSelectedParams } from "./types";
import "./zha-cluster-attributes"; import "./zha-cluster-attributes";
import "./zha-cluster-commands"; import "./zha-cluster-commands";
import "./zha-network"; import "./zha-network";
import "./zha-node"; import "./zha-node";
import "./zha-binding";
export class HaConfigZha extends LitElement { export class HaConfigZha extends LitElement {
public hass?: HomeAssistant; @property() public hass?: HomeAssistant;
public isWide?: boolean; @property() public isWide?: boolean;
private _selectedNode?: HassEntity; @property() private _selectedDevice?: ZHADevice;
private _selectedCluster?: Cluster; @property() private _selectedCluster?: Cluster;
@property() private _bindableDevices: ZHADevice[] = [];
static get properties(): PropertyDeclarations { protected updated(changedProperties: PropertyValues): void {
return { if (changedProperties.has("_selectedDevice")) {
hass: {}, this._fetchBindableDevices();
isWide: {}, }
_selectedCluster: {}, super.update(changedProperties);
_selectedNode: {},
};
} }
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
@ -57,25 +57,35 @@ export class HaConfigZha extends LitElement {
.isWide="${this.isWide}" .isWide="${this.isWide}"
.hass="${this.hass}" .hass="${this.hass}"
@zha-cluster-selected="${this._onClusterSelected}" @zha-cluster-selected="${this._onClusterSelected}"
@zha-node-selected="${this._onNodeSelected}" @zha-node-selected="${this._onDeviceSelected}"
></zha-node> ></zha-node>
${this._selectedCluster ${this._selectedCluster
? html` ? html`
<zha-cluster-attributes <zha-cluster-attributes
.isWide="${this.isWide}" .isWide="${this.isWide}"
.hass="${this.hass}" .hass="${this.hass}"
.selectedNode="${this._selectedNode}" .selectedNode="${this._selectedDevice}"
.selectedCluster="${this._selectedCluster}" .selectedCluster="${this._selectedCluster}"
></zha-cluster-attributes> ></zha-cluster-attributes>
<zha-cluster-commands <zha-cluster-commands
.isWide="${this.isWide}" .isWide="${this.isWide}"
.hass="${this.hass}" .hass="${this.hass}"
.selectedNode="${this._selectedNode}" .selectedNode="${this._selectedDevice}"
.selectedCluster="${this._selectedCluster}" .selectedCluster="${this._selectedCluster}"
></zha-cluster-commands> ></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> </ha-app-layout>
`; `;
} }
@ -86,13 +96,24 @@ export class HaConfigZha extends LitElement {
this._selectedCluster = selectedClusterEvent.detail.cluster; this._selectedCluster = selectedClusterEvent.detail.cluster;
} }
private _onNodeSelected( private _onDeviceSelected(
selectedNodeEvent: HASSDomEvent<ZHANodeSelectedParams> selectedNodeEvent: HASSDomEvent<ZHADeviceSelectedParams>
): void { ): void {
this._selectedNode = selectedNodeEvent.detail.node; this._selectedDevice = selectedNodeEvent.detail.node;
this._selectedCluster = undefined; 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[] { static get styles(): CSSResult[] {
return [haStyle]; 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 { export interface PickerTarget extends EventTarget {
selected: number; selected: number;
@ -34,8 +34,8 @@ export interface IssueCommandServiceData {
command_type: string; command_type: string;
} }
export interface ZHANodeSelectedParams { export interface ZHADeviceSelectedParams {
node: ZHADeviceEntity; node: ZHADevice;
} }
export interface ZHAClusterSelectedParams { 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 "@material/mwc-button";
import "@polymer/paper-card/paper-card"; import "@polymer/paper-card/paper-card";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
@ -18,9 +19,13 @@ import "../../../components/ha-service-description";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../ha-config-section"; import "../ha-config-section";
import { ItemSelectedEvent, NodeServiceData } from "./types"; import { ItemSelectedEvent, NodeServiceData, ChangeEvent } from "./types";
import "./zha-clusters"; import "./zha-clusters";
import "./zha-device-card"; import "./zha-device-card";
import {
updateDeviceRegistryEntry,
DeviceRegistryEntryMutableParams,
} from "../../../data/device_registry";
import { reconfigureNode, fetchDevices, ZHADevice } from "../../../data/zha"; import { reconfigureNode, fetchDevices, ZHADevice } from "../../../data/zha";
declare global { declare global {
@ -40,6 +45,7 @@ export class ZHANode extends LitElement {
private _selectedNode?: ZHADevice; private _selectedNode?: ZHADevice;
private _serviceData?: {}; private _serviceData?: {};
private _nodes: ZHADevice[]; private _nodes: ZHADevice[];
private _userSelectedName?: string;
constructor() { constructor() {
super(); super();
@ -58,6 +64,7 @@ export class ZHANode extends LitElement {
_entities: {}, _entities: {},
_serviceData: {}, _serviceData: {},
_nodes: {}, _nodes: {},
_userSelectedName: {},
}; };
} }
@ -109,7 +116,11 @@ export class ZHANode extends LitElement {
> >
${this._nodes.map( ${this._nodes.map(
(entry) => html` (entry) => html`
<paper-item>${entry.name}</paper-item> <paper-item
>${entry.user_given_name
? entry.user_given_name
: entry.name}</paper-item
>
` `
)} )}
</paper-listbox> </paper-listbox>
@ -132,6 +143,18 @@ export class ZHANode extends LitElement {
></zha-device-card> ></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._selectedNodeIndex !== -1 ? this._renderNodeActions() : ""}
${this._selectedNode ? this._renderClusters() : ""} ${this._selectedNode ? this._renderClusters() : ""}
</paper-card> </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> </div>
`; `;
} }
@ -191,6 +229,7 @@ export class ZHANode extends LitElement {
private _selectedNodeChanged(event: ItemSelectedEvent): void { private _selectedNodeChanged(event: ItemSelectedEvent): void {
this._selectedNodeIndex = event!.target!.selected; this._selectedNodeIndex = event!.target!.selected;
this._selectedNode = this._nodes[this._selectedNodeIndex]; this._selectedNode = this._nodes[this._selectedNodeIndex];
this._userSelectedName = "";
fireEvent(this, "zha-node-selected", { node: this._selectedNode }); fireEvent(this, "zha-node-selected", { node: this._selectedNode });
this._serviceData = this._computeNodeServiceData(); 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 { private _computeNodeServiceData(): NodeServiceData {
return { return {
ieee_address: this._selectedNode!.ieee, ieee_address: this._selectedNode!.ieee,
@ -277,6 +337,7 @@ export class ZHANode extends LitElement {
padding-left: 28px; padding-left: 28px;
padding-right: 28px; padding-right: 28px;
padding-bottom: 10px; padding-bottom: 10px;
word-wrap: break-word;
} }
ha-service-description { ha-service-description {
@ -294,6 +355,12 @@ export class ZHANode extends LitElement {
right: 0; right: 0;
color: var(--primary-color); color: var(--primary-color);
} }
.input-text {
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
`, `,
]; ];
} }

View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
import { import {
html, html,
LitElement, LitElement,
PropertyDeclarations,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
CSSResult, CSSResult,
css, css,
customElement,
property,
} from "lit-element"; } from "lit-element";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
@ -34,6 +35,7 @@ export interface Config extends LovelaceCardConfig {
hold_action?: ActionConfig; hold_action?: ActionConfig;
} }
@customElement("hui-entity-button-card")
class HuiEntityButtonCard extends LitElement implements LovelaceCard { class HuiEntityButtonCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> { public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-entity-button-card-editor" */ "../editor/config-elements/hui-entity-button-card-editor"); 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; @property() public hass?: HomeAssistant;
private _config?: Config;
static get properties(): PropertyDeclarations { @property() private _config?: Config;
return {
hass: {},
_config: {},
};
}
public getCardSize(): number { public getCardSize(): number {
return 2; return 2;
@ -147,11 +143,13 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
padding: 4% 0; padding: 4% 0;
font-size: 1.2rem; font-size: 1.2rem;
} }
ha-icon { ha-icon {
width: 40%; width: 40%;
height: auto; height: auto;
color: var(--paper-item-icon-color, #44739e); color: var(--paper-item-icon-color, #44739e);
} }
ha-icon[data-domain="light"][data-state="on"], ha-icon[data-domain="light"][data-state="on"],
ha-icon[data-domain="switch"][data-state="on"], ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="binary_sensor"][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"] { ha-icon[data-domain="sun"][data-state="above_horizon"] {
color: var(--paper-item-icon-active-color, #fdd835); color: var(--paper-item-icon-active-color, #fdd835);
} }
ha-icon[data-state="unavailable"] { ha-icon[data-state="unavailable"] {
color: var(--state-icon-unavailable-color); color: var(--state-icon-unavailable-color);
} }
@ -198,5 +197,3 @@ declare global {
"hui-entity-button-card": HuiEntityButtonCard; "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 { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace"; import { LovelaceCardConfig } from "../../../data/lovelace";
@ -21,15 +29,11 @@ export const createErrorCardConfig = (error, origConfig) => ({
origConfig, origConfig,
}); });
@customElement("hui-error-card")
export class HuiErrorCard extends LitElement implements LovelaceCard { export class HuiErrorCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant; public hass?: HomeAssistant;
private _config?: Config;
static get properties() { @property() private _config?: Config;
return {
_config: {},
};
}
public getCardSize(): number { public getCardSize(): number {
return 4; return 4;
@ -45,22 +49,20 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
} }
return html` return html`
${this.renderStyle()} ${this._config.error} ${this._config.error}
<pre>${this._toStr(this._config.origConfig)}</pre> <pre>${this._toStr(this._config.origConfig)}</pre>
`; `;
} }
private renderStyle(): TemplateResult { static get styles(): CSSResult {
return html` return css`
<style> :host {
:host { display: block;
display: block; background-color: #ef5350;
background-color: #ef5350; color: white;
color: white; padding: 8px;
padding: 8px; font-weight: 500;
font-weight: 500; }
}
</style>
`; `;
} }
@ -74,5 +76,3 @@ declare global {
"hui-error-card": HuiErrorCard; "hui-error-card": HuiErrorCard;
} }
} }
customElements.define("hui-error-card", HuiErrorCard);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,11 @@
import { import {
html, html,
LitElement, LitElement,
PropertyDeclarations,
TemplateResult, TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element"; } from "lit-element";
import "../../../components/ha-card"; import "../../../components/ha-card";
@ -20,6 +23,7 @@ export interface Config extends LovelaceCardConfig {
hold_action?: ActionConfig; hold_action?: ActionConfig;
} }
@customElement("hui-picture-card")
export class HuiPictureCard extends LitElement implements LovelaceCard { export class HuiPictureCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> { public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-picture-card-editor" */ "../editor/config-elements/hui-picture-card-editor"); 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; public hass?: HomeAssistant;
protected _config?: Config;
static get properties(): PropertyDeclarations { @property() protected _config?: Config;
return { _config: {} };
}
public getCardSize(): number { public getCardSize(): number {
return 3; return 3;
@ -59,7 +60,6 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
} }
return html` return html`
${this.renderStyle()}
<ha-card <ha-card
@ha-click="${this._handleTap}" @ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}" @ha-hold="${this._handleHold}"
@ -75,20 +75,20 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
`; `;
} }
private renderStyle(): TemplateResult { static get styles(): CSSResult {
return html` return css`
<style> ha-card {
ha-card { overflow: hidden;
overflow: hidden; }
}
ha-card.clickable { ha-card.clickable {
cursor: pointer; cursor: pointer;
} }
img {
display: block; img {
width: 100%; display: block;
} width: 100%;
</style> }
`; `;
} }
@ -106,5 +106,3 @@ declare global {
"hui-picture-card": HuiPictureCard; "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"; import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
@ -17,15 +25,11 @@ interface Config extends LovelaceCardConfig {
elements: LovelaceElementConfig[]; elements: LovelaceElementConfig[];
} }
@customElement("hui-picture-elements-card")
class HuiPictureElementsCard extends LitElement implements LovelaceCard { class HuiPictureElementsCard extends LitElement implements LovelaceCard {
private _config?: Config; @property() private _config?: Config;
private _hass?: HomeAssistant;
static get properties() { private _hass?: HomeAssistant;
return {
_config: {},
};
}
set hass(hass: HomeAssistant) { set hass(hass: HomeAssistant) {
this._hass = hass; this._hass = hass;
@ -60,7 +64,6 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
} }
return html` return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}"> <ha-card .header="${this._config.title}">
<div id="root"> <div id="root">
<hui-image <hui-image
@ -84,20 +87,20 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
`; `;
} }
private renderStyle(): TemplateResult { static get styles(): CSSResult {
return html` return css`
<style> #root {
#root { position: relative;
position: relative; }
}
.element { .element {
position: absolute; position: absolute;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
ha-card {
overflow: hidden; ha-card {
} overflow: hidden;
</style> }
`; `;
} }
} }
@ -107,5 +110,3 @@ declare global {
"hui-picture-elements-card": HuiPictureElementsCard; "hui-picture-elements-card": HuiPictureElementsCard;
} }
} }
customElements.define("hui-picture-elements-card", HuiPictureElementsCard);

View File

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

View File

@ -1,8 +1,11 @@
import { import {
html, html,
LitElement, LitElement,
PropertyDeclarations,
TemplateResult, TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
@ -39,18 +42,15 @@ interface Config extends LovelaceCardConfig {
hold_action?: ActionConfig; hold_action?: ActionConfig;
} }
@customElement("hui-picture-glance-card")
class HuiPictureGlanceCard extends LitElement implements LovelaceCard { class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant; @property() public hass?: HomeAssistant;
private _config?: Config;
private _entitiesDialog?: EntityConfig[];
private _entitiesToggle?: EntityConfig[];
static get properties(): PropertyDeclarations { @property() private _config?: Config;
return {
hass: {}, private _entitiesDialog?: EntityConfig[];
_config: {},
}; private _entitiesToggle?: EntityConfig[];
}
public getCardSize(): number { public getCardSize(): number {
return 3; return 3;
@ -91,7 +91,6 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
} }
return html` return html`
${this.renderStyle()}
<ha-card> <ha-card>
<hui-image <hui-image
class="${classMap({ class="${classMap({
@ -177,44 +176,52 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
toggleEntity(this.hass!, (ev.target as any).entity); toggleEntity(this.hass!, (ev.target as any).entity);
} }
private renderStyle(): TemplateResult { static get styles(): CSSResult {
return html` return css`
<style> ha-card {
ha-card { position: relative;
position: relative; min-height: 48px;
min-height: 48px; overflow: hidden;
overflow: hidden; }
}
hui-image.clickable { hui-image.clickable {
cursor: pointer; cursor: pointer;
} }
.box {
@apply --paper-font-common-nowrap; .box {
position: absolute; /* start paper-font-common-nowrap style */
left: 0; white-space: nowrap;
right: 0; overflow: hidden;
bottom: 0; text-overflow: ellipsis;
background-color: rgba(0, 0, 0, 0.3); /* end paper-font-common-nowrap style */
padding: 4px 8px;
font-size: 16px; position: absolute;
line-height: 40px; left: 0;
color: white; right: 0;
display: flex; bottom: 0;
justify-content: space-between; background-color: rgba(0, 0, 0, 0.3);
} padding: 4px 8px;
.box .title { font-size: 16px;
font-weight: 500; line-height: 40px;
margin-left: 8px; color: white;
} display: flex;
ha-icon { justify-content: space-between;
cursor: pointer; }
padding: 8px;
color: #a9a9a9; .box .title {
} font-weight: 500;
ha-icon.state-on { margin-left: 8px;
color: white; }
}
</style> 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; "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, html,
svg, svg,
LitElement, LitElement,
PropertyDeclarations,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element"; } from "lit-element";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
@ -145,6 +148,7 @@ export interface Config extends LovelaceCardConfig {
hours_to_show?: number; hours_to_show?: number;
} }
@customElement("hui-sensor-card")
class HuiSensorCard extends LitElement implements LovelaceCard { class HuiSensorCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> { public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-sensor-card-editor" */ "../editor/config-elements/hui-sensor-card-editor"); 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 {}; return {};
} }
public hass?: HomeAssistant; @property() public hass?: HomeAssistant;
private _config?: Config;
private _history?: any;
private _date?: Date;
static get properties(): PropertyDeclarations { @property() private _config?: Config;
return {
hass: {}, @property() private _history?: any;
_config: {},
_history: {}, private _date?: Date;
};
}
public setConfig(config: Config): void { public setConfig(config: Config): void {
if (!config.entity || config.entity.split(".")[0] !== "sensor") { if (!config.entity || config.entity.split(".")[0] !== "sensor") {
@ -244,7 +243,6 @@ class HuiSensorCard extends LitElement implements LovelaceCard {
graph = ""; graph = "";
} }
return html` return html`
${this.renderStyle()}
<ha-card @click="${this._handleClick}"> <ha-card @click="${this._handleClick}">
<div class="flex"> <div class="flex">
<div class="icon"> <div class="icon">
@ -324,87 +322,95 @@ class HuiSensorCard extends LitElement implements LovelaceCard {
this._date = new Date(); this._date = new Date();
} }
private renderStyle(): TemplateResult { static get styles(): CSSResult {
return html` return css`
<style> :host {
:host { display: flex;
display: flex; flex-direction: column;
flex-direction: column; }
}
ha-card { ha-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
padding: 16px; padding: 16px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
.flex {
display: flex; .flex {
} display: flex;
.header { }
align-items: center;
display: flex; .header {
min-width: 0; align-items: center;
opacity: 0.8; display: flex;
position: relative; min-width: 0;
} opacity: 0.8;
.name { position: relative;
display: block; }
display: -webkit-box;
font-size: 1.2rem; .name {
font-weight: 500; display: block;
max-height: 1.4rem; display: -webkit-box;
margin-top: 2px; font-size: 1.2rem;
opacity: 0.8; font-weight: 500;
overflow: hidden; max-height: 1.4rem;
text-overflow: ellipsis; margin-top: 2px;
-webkit-line-clamp: 1; opacity: 0.8;
-webkit-box-orient: vertical; overflow: hidden;
word-wrap: break-word; text-overflow: ellipsis;
word-break: break-all; -webkit-line-clamp: 1;
} -webkit-box-orient: vertical;
.icon { word-wrap: break-word;
color: var(--paper-item-icon-color, #44739e); word-break: break-all;
display: inline-block; }
flex: 0 0 40px;
line-height: 40px; .icon {
position: relative; color: var(--paper-item-icon-color, #44739e);
text-align: center; display: inline-block;
width: 40px; flex: 0 0 40px;
} line-height: 40px;
.info { position: relative;
flex-wrap: wrap; text-align: center;
margin: 16px 0 16px 8px; width: 40px;
} }
#value {
display: inline-block; .info {
font-size: 2rem; flex-wrap: wrap;
font-weight: 400; margin: 16px 0 16px 8px;
line-height: 1em; }
margin-right: 4px;
} #value {
#measurement { display: inline-block;
align-self: flex-end; font-size: 2rem;
display: inline-block; font-weight: 400;
font-size: 1.3rem; line-height: 1em;
line-height: 1.2em; margin-right: 4px;
margin-top: 0.1em; }
opacity: 0.6;
vertical-align: bottom; #measurement {
} align-self: flex-end;
.graph { display: inline-block;
align-self: flex-end; font-size: 1.3rem;
margin: auto; line-height: 1.2em;
margin-bottom: 0px; margin-top: 0.1em;
position: relative; opacity: 0.6;
width: 100%; vertical-align: bottom;
} }
.graph > div {
align-self: flex-end; .graph {
margin: auto 8px; align-self: flex-end;
} margin: auto;
</style> 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; "hui-sensor-card": HuiSensorCard;
} }
} }
customElements.define("hui-sensor-card", HuiSensorCard);

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import { import {
html, html,
LitElement, LitElement,
PropertyDeclarations,
TemplateResult, TemplateResult,
customElement,
property,
} from "lit-element"; } from "lit-element";
import "@polymer/paper-input/paper-textarea"; import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
@ -31,15 +32,15 @@ declare global {
} }
} }
@customElement("hui-action-editor")
export class HuiActionEditor extends LitElement { export class HuiActionEditor extends LitElement {
public config?: ActionConfig; @property() public config?: ActionConfig;
public label?: string;
public actions?: string[];
protected hass?: HomeAssistant;
static get properties(): PropertyDeclarations { @property() public label?: string;
return { hass: {}, config: {}, label: {}, actions: {} };
} @property() public actions?: string[];
@property() protected hass?: HomeAssistant;
get _action(): string { get _action(): string {
return this.config!.action || ""; return this.config!.action || "";
@ -126,5 +127,3 @@ declare global {
"hui-action-editor": HuiActionEditor; "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 "@material/mwc-button";
import "@polymer/paper-menu-button/paper-menu-button"; import "@polymer/paper-menu-button/paper-menu-button";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
@ -12,63 +20,18 @@ import { Lovelace } from "../types";
import { swapCard } from "../editor/config-util"; import { swapCard } from "../editor/config-util";
import { showMoveCardViewDialog } from "../editor/card-editor/show-move-card-view-dialog"; import { showMoveCardViewDialog } from "../editor/card-editor/show-move-card-view-dialog";
@customElement("hui-card-options")
export class HuiCardOptions extends LitElement { export class HuiCardOptions extends LitElement {
public cardConfig?: LovelaceCardConfig; public cardConfig?: LovelaceCardConfig;
public hass?: HomeAssistant;
public lovelace?: Lovelace;
public path?: [number, number];
static get properties(): PropertyDeclarations { @property() public hass?: HomeAssistant;
return { hass: {}, lovelace: {}, path: {} };
}
protected render() { @property() public lovelace?: Lovelace;
@property() public path?: [number, number];
protected render(): TemplateResult | void {
return html` 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> <slot></slot>
<div class="options"> <div class="options">
<div class="primary-actions"> <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 { private _editCard(): void {
showEditCardDialog(this, { showEditCardDialog(this, {
lovelace: this.lovelace!, lovelace: this.lovelace!,
@ -162,5 +173,3 @@ declare global {
"hui-card-options": HuiCardOptions; "hui-card-options": HuiCardOptions;
} }
} }
customElements.define("hui-card-options", HuiCardOptions);

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import {
css, css,
PropertyValues, PropertyValues,
query, query,
customElement,
} from "lit-element"; } from "lit-element";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
@ -26,33 +27,43 @@ export interface StateSpecificConfig {
[state: string]: string; [state: string]: string;
} }
/* @customElement("hui-image")
* @appliesMixin LocalizeMixin
*/
class HuiImage extends LitElement { class HuiImage extends LitElement {
@property() public hass?: HomeAssistant; @property() public hass?: HomeAssistant;
@property() public entity?: string; @property() public entity?: string;
@property() public image?: string; @property() public image?: string;
@property() public stateImage?: StateSpecificConfig; @property() public stateImage?: StateSpecificConfig;
@property() public cameraImage?: string; @property() public cameraImage?: string;
@property() public aspectRatio?: string; @property() public aspectRatio?: string;
@property() public filter?: string; @property() public filter?: string;
@property() public stateFilter?: StateSpecificConfig; @property() public stateFilter?: StateSpecificConfig;
@property() private _loadError?: boolean; @property() private _loadError?: boolean;
@property() private _cameraImageSrc?: string; @property() private _cameraImageSrc?: string;
@query("img") private _image!: HTMLImageElement; @query("img") private _image!: HTMLImageElement;
private _lastImageHeight?: number; private _lastImageHeight?: number;
private _cameraUpdater?: number; private _cameraUpdater?: number;
private _attached?: boolean; private _attached?: boolean;
public connectedCallback() { public connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this._attached = true; this._attached = true;
this._startUpdateCameraInterval(); this._startUpdateCameraInterval();
} }
public disconnectedCallback() { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
this._attached = false; this._attached = false;
this._stopUpdateCameraInterval(); this._stopUpdateCameraInterval();
@ -137,7 +148,7 @@ class HuiImage extends LitElement {
} }
} }
private _startUpdateCameraInterval() { private _startUpdateCameraInterval(): void {
this._stopUpdateCameraInterval(); this._stopUpdateCameraInterval();
if (this.cameraImage && this._attached) { if (this.cameraImage && this._attached) {
this._cameraUpdater = window.setInterval( this._cameraUpdater = window.setInterval(
@ -147,23 +158,23 @@ class HuiImage extends LitElement {
} }
} }
private _stopUpdateCameraInterval() { private _stopUpdateCameraInterval(): void {
if (this._cameraUpdater) { if (this._cameraUpdater) {
clearInterval(this._cameraUpdater); clearInterval(this._cameraUpdater);
} }
} }
private _onImageError() { private _onImageError(): void {
this._loadError = true; this._loadError = true;
} }
private async _onImageLoad() { private async _onImageLoad(): Promise<void> {
this._loadError = false; this._loadError = false;
await this.updateComplete; await this.updateComplete;
this._lastImageHeight = this._image.offsetHeight; this._lastImageHeight = this._image.offsetHeight;
} }
private async _updateCameraImageSrc() { private async _updateCameraImageSrc(): Promise<void> {
if (!this.hass || !this.cameraImage) { if (!this.hass || !this.cameraImage) {
return; return;
} }
@ -221,5 +232,3 @@ declare global {
"hui-image": HuiImage; "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 { import {
html, html,
LitElement, LitElement,
PropertyDeclarations,
TemplateResult, TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element"; } from "lit-element";
import "@material/mwc-button"; import "@material/mwc-button";
@ -20,16 +23,11 @@ declare global {
} }
} }
export class HuiThemeSelectionEditor extends LitElement { @customElement("hui-theme-select-editor")
public value?: string; export class HuiThemeSelectEditor extends LitElement {
public hass?: HomeAssistant; @property() public value?: string;
static get properties(): PropertyDeclarations { @property() public hass?: HomeAssistant;
return {
hass: {},
value: {},
};
}
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
const themes = ["Backend-selected", "default"].concat( const themes = ["Backend-selected", "default"].concat(
@ -37,7 +35,6 @@ export class HuiThemeSelectionEditor extends LitElement {
); );
return html` return html`
${this.renderStyle()}
<paper-dropdown-menu <paper-dropdown-menu
label="Theme" label="Theme"
dynamic-align dynamic-align
@ -58,13 +55,11 @@ export class HuiThemeSelectionEditor extends LitElement {
`; `;
} }
private renderStyle(): TemplateResult { static get styles(): CSSResult {
return html` return css`
<style> paper-dropdown-menu {
paper-dropdown-menu { width: 100%;
width: 100%; }
}
</style>
`; `;
} }
@ -79,8 +74,6 @@ export class HuiThemeSelectionEditor extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { 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 { import {
html, html,
LitElement, LitElement,
PropertyDeclarations,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
customElement,
property,
} from "lit-element"; } from "lit-element";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
@ -19,30 +20,32 @@ const FORMATS: { [key: string]: (ts: Date, lang: string) => string } = {
}; };
const INTERVAL_FORMAT = ["relative", "total"]; const INTERVAL_FORMAT = ["relative", "total"];
@customElement("hui-timestamp-display")
class HuiTimestampDisplay extends LitElement { class HuiTimestampDisplay extends LitElement {
public hass?: HomeAssistant; @property() public hass?: HomeAssistant;
public ts?: Date;
public format?: "relative" | "total" | "date" | "datetime" | "time"; @property() public ts?: Date;
private _relative?: string;
@property() public format?:
| "relative"
| "total"
| "date"
| "datetime"
| "time";
@property() private _relative?: string;
private _connected?: boolean; private _connected?: boolean;
private _interval?: number; private _interval?: number;
static get properties(): PropertyDeclarations { public connectedCallback(): void {
return {
ts: {},
hass: {},
format: {},
_relative: {},
};
}
public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this._connected = true; this._connected = true;
this._startInterval(); this._startInterval();
} }
public disconnectedCallback() { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
this._connected = false; this._connected = false;
this._clearInterval(); this._clearInterval();
@ -65,18 +68,18 @@ class HuiTimestampDisplay extends LitElement {
return html` return html`
${this._relative} ${this._relative}
`; `;
} else if (format in FORMATS) { }
if (format in FORMATS) {
return html` return html`
${FORMATS[format](this.ts, this.hass.language)} ${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); super.updated(changedProperties);
if (!changedProperties.has("format") || !this._connected) { if (!changedProperties.has("format") || !this._connected) {
return; return;
@ -89,11 +92,11 @@ class HuiTimestampDisplay extends LitElement {
} }
} }
private get _format() { private get _format(): string {
return this.format || "relative"; return this.format || "relative";
} }
private _startInterval() { private _startInterval(): void {
this._clearInterval(); this._clearInterval();
if (this._connected && INTERVAL_FORMAT.includes(this._format)) { if (this._connected && INTERVAL_FORMAT.includes(this._format)) {
this._updateRelative(); this._updateRelative();
@ -101,14 +104,14 @@ class HuiTimestampDisplay extends LitElement {
} }
} }
private _clearInterval() { private _clearInterval(): void {
if (this._interval) { if (this._interval) {
clearInterval(this._interval); clearInterval(this._interval);
this._interval = undefined; this._interval = undefined;
} }
} }
private _updateRelative() { private _updateRelative(): void {
if (this.ts && this.hass!.localize) { if (this.ts && this.hass!.localize) {
this._relative = this._relative =
this._format === "relative" this._format === "relative"
@ -126,5 +129,3 @@ declare global {
"hui-timestamp-display": HuiTimestampDisplay; "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 { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeRTL } from "../../../common/util/compute_rtl"; import { computeRTL } from "../../../common/util/compute_rtl";
import { customElement } from "lit-element";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -16,9 +17,12 @@ declare global {
} }
} }
@customElement("hui-yaml-editor")
export class HuiYamlEditor extends HTMLElement { export class HuiYamlEditor extends HTMLElement {
public _hass?: HomeAssistant; public _hass?: HomeAssistant;
public codemirror: CodeMirror; public codemirror: CodeMirror;
private _value: string; private _value: string;
public constructor() { public constructor() {
@ -108,7 +112,7 @@ export class HuiYamlEditor extends HTMLElement {
fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() }); fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() });
} }
private setScrollBarDirection() { private setScrollBarDirection(): void {
if (!this.codemirror) { if (!this.codemirror) {
return; return;
} }
@ -124,5 +128,3 @@ declare global {
"hui-yaml-editor": HuiYamlEditor; "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 "@material/mwc-button";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@ -33,8 +40,10 @@ const cards = [
{ name: "Weather Forecast", type: "weather-forecast" }, { name: "Weather Forecast", type: "weather-forecast" },
]; ];
@customElement("hui-card-picker")
export class HuiCardPicker extends LitElement { export class HuiCardPicker extends LitElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
public cardPicked?: (cardConf: LovelaceCardConfig) => void; public cardPicked?: (cardConf: LovelaceCardConfig) => void;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
@ -97,5 +106,3 @@ declare global {
"hui-card-picker": HuiCardPicker; "hui-card-picker": HuiCardPicker;
} }
} }
customElements.define("hui-card-picker", HuiCardPicker);

View File

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

View File

@ -1,8 +1,11 @@
import { import {
html, html,
LitElement, LitElement,
PropertyDeclarations,
TemplateResult, TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element"; } from "lit-element";
import "@polymer/paper-dialog/paper-dialog"; import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
@ -13,14 +16,9 @@ import { moveCard } from "../config-util";
import { MoveCardViewDialogParams } from "./show-move-card-view-dialog"; import { MoveCardViewDialogParams } from "./show-move-card-view-dialog";
import { PolymerChangedEvent } from "../../../../polymer-types"; import { PolymerChangedEvent } from "../../../../polymer-types";
@customElement("hui-dialog-move-card-view")
export class HuiDialogMoveCardView extends LitElement { export class HuiDialogMoveCardView extends LitElement {
private _params?: MoveCardViewDialogParams; @property() private _params?: MoveCardViewDialogParams;
static get properties(): PropertyDeclarations {
return {
_params: {},
};
}
public async showDialog(params: MoveCardViewDialogParams): Promise<void> { public async showDialog(params: MoveCardViewDialogParams): Promise<void> {
this._params = params; this._params = params;
@ -32,29 +30,6 @@ export class HuiDialogMoveCardView extends LitElement {
return html``; return html``;
} }
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 <paper-dialog
with-backdrop with-backdrop
opened 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 { private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!; return this.shadowRoot!.querySelector("paper-dialog")!;
} }
@ -104,5 +105,3 @@ declare global {
"hui-dialog-move-card-view": HuiDialogMoveCardView; "hui-dialog-move-card-view": HuiDialogMoveCardView;
} }
} }
customElements.define("hui-dialog-move-card-view", HuiDialogMoveCardView);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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