Merge pull request #2736 from home-assistant/dev

20190212.0
This commit is contained in:
Paulus Schoutsen 2019-02-12 12:04:48 -08:00 committed by GitHub
commit 4058a0c8d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 3219 additions and 1657 deletions

View File

@ -3,7 +3,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
throw Error("latestBuild not defined for babel loader config");
}
return {
test: /\.m?js$|\.ts$/,
test: /\.m?js$|\.tsx?$/,
use: {
loader: "babel-loader",
options: {
@ -12,7 +12,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
require("@babel/preset-env").default,
{ modules: false },
],
require("@babel/preset-typescript").default,
[
require("@babel/preset-typescript").default,
{
jsxPragma: "h",
},
],
].filter(Boolean),
plugins: [
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
@ -28,6 +33,14 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
pragma: "h",
},
],
[
require("@babel/plugin-proposal-decorators").default,
{ decoratorsBeforeExport: true },
],
[
require("@babel/plugin-proposal-class-properties").default,
{ loose: true },
],
],
},
},

View File

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

60
config/webpack.js Normal file
View File

@ -0,0 +1,60 @@
const webpack = require("webpack");
const path = require("path");
const BabelMinifyPlugin = require("babel-minify-webpack-plugin");
module.exports.resolve = {
extensions: [".ts", ".js", ".json", ".tsx"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
};
module.exports.plugins = [
// Ignore moment.js locales
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Color.js is bloated, it contains all color definitions for all material color sets.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/paper-styles\/color\.js$/,
path.resolve(__dirname, "../src/util/empty.js")
),
// Ignore roboto pointing at CDN. We use local font-roboto-local.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/font-roboto\/roboto\.js$/,
path.resolve(__dirname, "../src/util/empty.js")
),
];
module.exports.optimization = {
minimizer: [
// Took options from Polymer build tool
// https://github.com/Polymer/tools/blob/master/packages/build/src/js-transform.ts
new BabelMinifyPlugin(
{
// Disable the minify-constant-folding plugin because it has a bug relating
// to invalid substitution of constant values into export specifiers:
// https://github.com/babel/minify/issues/820
evaluate: false,
// TODO(aomarks) Find out why we disabled this plugin.
simplifyComparisons: false,
// Prevent removal of things that babel thinks are unreachable, but sometimes
// gets wrong: https://github.com/Polymer/tools/issues/724
deadcode: false,
// Disable the simplify plugin because it can eat some statements preceeding
// loops. https://github.com/babel/minify/issues/824
simplify: false,
// This is breaking ES6 output. https://github.com/Polymer/tools/issues/261
mangle: false,
},
{}
),
],
};

11
demo/script/size_stats Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
# Analyze stats
# Stop on errors
set -e
cd "$(dirname "$0")/.."
STATS=1 NODE_ENV=production ../node_modules/.bin/webpack --profile --json > compilation-stats.json
npx webpack-bundle-analyzer compilation-stats.json dist
rm compilation-stats.json

View File

@ -3,7 +3,6 @@ import "../custom-cards/ha-demo-card";
// tslint:disable-next-line
import { HADemoCard } from "../custom-cards/ha-demo-card";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { HUIView } from "../../../src/panels/lovelace/hui-view";
import { selectedDemoConfig } from "../configs/demo-configs";
export const mockLovelace = (hass: MockHomeAssistant) => {
@ -16,13 +15,17 @@ export const mockLovelace = (hass: MockHomeAssistant) => {
hass.mockWS("lovelace/config/save", () => Promise.resolve());
};
// Patch HUI-VIEW to make the lovelace object available to the demo card
const oldCreateCard = HUIView.prototype.createCardElement;
customElements.whenDefined("hui-view").then(() => {
// tslint:disable-next-line
const HUIView = customElements.get("hui-view");
// Patch HUI-VIEW to make the lovelace object available to the demo card
const oldCreateCard = HUIView.prototype.createCardElement;
HUIView.prototype.createCardElement = function(config) {
const el = oldCreateCard.call(this, config);
if (el.tagName === "HA-DEMO-CARD") {
(el as HADemoCard).lovelace = this.lovelace;
}
return el;
};
HUIView.prototype.createCardElement = function(config) {
const el = oldCreateCard.call(this, config);
if (el.tagName === "HA-DEMO-CARD") {
(el as HADemoCard).lovelace = this.lovelace;
}
return el;
};
});

View File

@ -3,10 +3,12 @@ const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js");
const { minimizer } = require("../config/babel.js");
const webpackBase = require("../config/webpack.js");
const isProd = process.env.NODE_ENV === "production";
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
const isStatsBuild = process.env.STATS === "1";
const chunkFilename =
isProd && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const buildPath = path.resolve(__dirname, "dist");
const publicPath = "/";
@ -14,9 +16,7 @@ const latestBuild = false;
module.exports = {
mode: isProd ? "production" : "development",
// Disabled in prod while we make Home Assistant able to serve the right files.
// Was source-map
devtool: isProd ? "none" : "inline-source-map",
devtool: isProd ? "cheap-source-map" : "inline-source-map",
entry: {
main: "./src/entrypoint.ts",
compatibility: "../src/entrypoints/compatibility.js",
@ -39,9 +39,7 @@ module.exports = {
},
],
},
optimization: {
minimizer,
},
optimization: webpackBase.optimization,
plugins: [
new webpack.DefinePlugin({
__DEV__: false,
@ -71,23 +69,14 @@ module.exports = {
to: "static/images/leaflet/",
},
]),
...webpackBase.plugins,
isProd &&
new WorkboxPlugin.GenerateSW({
swDest: "service_worker_es5.js",
importWorkboxFrom: "local",
}),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
},
resolve: webpackBase.resolve,
output: {
filename: "[name].js",
chunkFilename: chunkFilename,

View File

@ -2,6 +2,19 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/demo-cards";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
const ENTITIES = [
getEntity("sensor", "brightness", "12", {}),
getEntity("plant", "bonsai", "ok", {}),
getEntity("sensor", "outside_humidity", "54", {
unit_of_measurement: "%",
}),
getEntity("sensor", "outside_temperature", "15.6", {
unit_of_measurement: "°C",
}),
];
const CONFIGS = [
{
@ -66,7 +79,7 @@ const CONFIGS = [
class DemoGaugeEntity extends PolymerElement {
static get template() {
return html`
<demo-cards configs="[[_configs]]"></demo-cards>
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
`;
}
@ -78,6 +91,12 @@ class DemoGaugeEntity extends PolymerElement {
},
};
}
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);
}
}
customElements.define("demo-hui-gauge-card", DemoGaugeEntity);

View File

@ -2,6 +2,17 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/demo-cards";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
const ENTITIES = [
getEntity("light", "kitchen_lights", "on", {
friendly_name: "Kitchen Lights",
}),
getEntity("light", "bed_light", "off", {
friendly_name: "Bed Light",
}),
];
const CONFIGS = [
{
@ -10,6 +21,8 @@ const CONFIGS = [
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
tap_action:
action: toggle
`,
},
{
@ -18,6 +31,8 @@ const CONFIGS = [
- type: picture-entity
image: /images/bed.png
entity: light.bed_light
tap_action:
action: toggle
`,
},
{
@ -68,7 +83,7 @@ const CONFIGS = [
class DemoPicEntity extends PolymerElement {
static get template() {
return html`
<demo-cards configs="[[_configs]]"></demo-cards>
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
`;
}
@ -80,6 +95,12 @@ class DemoPicEntity extends PolymerElement {
},
};
}
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);
}
}
customElements.define("demo-hui-picture-entity-card", DemoPicEntity);

View File

@ -2,6 +2,25 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/demo-cards";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
const ENTITIES = [
getEntity("switch", "decorative_lights", "on", {
friendly_name: "Decorative Lights",
}),
getEntity("light", "ceiling_lights", "on", {
friendly_name: "Ceiling Lights",
}),
getEntity("binary_sensor", "movement_backyard", "on", {
friendly_name: "Movement Backyard",
device_class: "moving",
}),
getEntity("binary_sensor", "basement_floor_wet", "off", {
friendly_name: "Basement Floor Wet",
device_class: "moisture",
}),
];
const CONFIGS = [
{
@ -105,7 +124,7 @@ const CONFIGS = [
class DemoPicGlance extends PolymerElement {
static get template() {
return html`
<demo-cards configs="[[_configs]]"></demo-cards>
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
`;
}
@ -117,6 +136,12 @@ class DemoPicGlance extends PolymerElement {
},
};
}
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);
}
}
customElements.define("demo-hui-picture-glance-card", DemoPicGlance);

View File

@ -1,7 +1,7 @@
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js");
const { minimizer } = require("../config/babel.js");
const webpackBase = require("../config/webpack.js");
const isProd = process.env.NODE_ENV === "production";
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
@ -32,9 +32,7 @@ module.exports = {
},
],
},
optimization: {
minimizer,
},
optimization: webpackBase.optimization,
plugins: [
new CopyWebpackPlugin([
"public",
@ -63,9 +61,7 @@ module.exports = {
},
}),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json"],
},
resolve: webpackBase.resolve,
output: {
filename: "[name].js",
chunkFilename: chunkFilename,

View File

@ -64,25 +64,24 @@
"@polymer/polymer": "^3.0.5",
"@vaadin/vaadin-combo-box": "^4.2.0",
"@vaadin/vaadin-date-picker": "^3.3.1",
"@webcomponents/shadycss": "^1.6.0",
"@webcomponents/webcomponentsjs": "^2.2.0",
"@webcomponents/shadycss": "^1.9.0",
"@webcomponents/webcomponentsjs": "^2.2.6",
"chart.js": "~2.7.2",
"chartjs-chart-timeline": "^0.2.1",
"codemirror": "^5.43.0",
"deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0",
"eslint-import-resolver-webpack": "^0.10.1",
"fecha": "^3.0.0",
"gulp-hash-filename": "^2.0.1",
"home-assistant-js-websocket": "^3.2.4",
"intl-messageformat": "^2.2.0",
"jquery": "^3.3.1",
"js-yaml": "^3.12.0",
"leaflet": "^1.3.4",
"lit-element": "2.0.0-rc.5",
"lit-html": "1.0.0-rc.2",
"lit-element": "^2.0.0",
"lit-html": "^1.0.0",
"marked": "^0.6.0",
"mdn-polyfills": "^5.12.0",
"memoize-one": "^5.0.0",
"moment": "^2.22.2",
"preact": "^8.3.1",
"preact-compat": "^3.18.4",
@ -97,6 +96,8 @@
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/plugin-external-helpers": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.3.0",
"@babel/plugin-proposal-decorators": "^7.3.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-react-jsx": "^7.0.0",
@ -105,6 +106,7 @@
"@gfx/zopfli": "^1.0.9",
"@types/chai": "^4.1.7",
"@types/codemirror": "^0.0.71",
"@types/memoize-one": "^4.1.0",
"@types/mocha": "^5.2.5",
"babel-eslint": "^10",
"babel-loader": "^8.0.4",
@ -116,12 +118,14 @@
"eslint": "^5.6.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^4.0.0",
"eslint-import-resolver-webpack": "^0.10.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-prettier": "^3.0.0",
"eslint-plugin-react": "^7.11.1",
"gulp": "^3.9.1",
"gulp-foreach": "^0.1.0",
"gulp-hash": "^4.2.2",
"gulp-hash-filename": "^2.0.1",
"gulp-insert": "^0.5.0",
"gulp-json-transform": "^0.4.5",
"gulp-jsonminify": "^1.1.0",
@ -156,8 +160,8 @@
},
"resolutions": {
"@polymer/polymer": "3.1.0",
"@webcomponents/webcomponentsjs": "2.2.1",
"@webcomponents/shadycss": "^1.6.0",
"@webcomponents/webcomponentsjs": "^2.2.6",
"@webcomponents/shadycss": "^1.9.0",
"@vaadin/vaadin-overlay": "3.2.2",
"@vaadin/vaadin-lumo-styles": "1.3.0"
},

View File

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

View File

@ -0,0 +1,20 @@
// https://stackoverflow.com/a/16245768
export const b64toBlob = (b64Data, contentType = "", sliceSize = 512) => {
const byteCharacters = atob(b64Data);
const byteArrays: Uint8Array[] = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: contentType });
};

View File

@ -0,0 +1,120 @@
import {
LitElement,
TemplateResult,
property,
html,
customElement,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button-light";
import { HomeAssistant } from "../../types";
import { PolymerChangedEvent } from "../../polymer-types";
import { fireEvent } from "../../common/dom/fire_event";
import isValidEntityId from "../../common/entity/valid_entity_id";
import "./ha-entity-picker";
// Not a duplicate, type import
// tslint:disable-next-line
import { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
import { HassEntity } from "home-assistant-js-websocket";
@customElement("ha-entities-picker")
class HaEntitiesPickerLight extends LitElement {
@property() public hass?: HomeAssistant;
@property() public value?: string[];
@property() public domainFilter?: string;
@property() public pickedEntityLabel?: string;
@property() public pickEntityLabel?: string;
protected render(): TemplateResult | void {
if (!this.hass) {
return;
}
const currentEntities = this._currentEntities;
return html`
${currentEntities.map(
(entityId) => html`
<div>
<ha-entity-picker
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.domainFilter=${this.domainFilter}
.entityFilter=${this._entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
@value-changed=${this._entityChanged}
></ha-entity-picker>
</div>
`
)}
<div>
<ha-entity-picker
.hass=${this.hass}
.domainFilter=${this.domainFilter}
.entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel}
@value-changed=${this._addEntity}
></ha-entity-picker>
</div>
`;
}
private _entityFilter: HaEntityPickerEntityFilterFunc = (
stateObj: HassEntity
) => !this.value || !this.value.includes(stateObj.entity_id);
private get _currentEntities() {
return this.value || [];
}
private async _updateEntities(entities) {
fireEvent(this, "value-changed", {
value: entities,
});
this.value = entities;
}
private _entityChanged(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const curValue = (event.currentTarget as any).curValue;
const newValue = event.detail.value;
if (
newValue === curValue ||
(newValue !== "" && !isValidEntityId(newValue))
) {
return;
}
if (newValue === "") {
this._updateEntities(
this._currentEntities.filter((ent) => ent !== curValue)
);
} else {
this._updateEntities(
this._currentEntities.map((ent) => (ent === curValue ? newValue : ent))
);
}
}
private async _addEntity(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
(event.currentTarget as any).value = "";
if (!toAdd) {
return;
}
const currentEntities = this._currentEntities;
if (currentEntities.includes(toAdd)) {
return;
}
this._updateEntities([...currentEntities, toAdd]);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-entities-picker": HaEntitiesPickerLight;
}
}

View File

@ -1,179 +0,0 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "./state-badge";
import computeStateName from "../../common/entity/compute_state_name";
import LocalizeMixin from "../../mixins/localize-mixin";
import EventsMixin from "../../mixins/events-mixin";
/*
* @appliesMixin LocalizeMixin
*/
class HaEntityPicker extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style>
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
</style>
<vaadin-combo-box-light
items="[[_states]]"
item-value-path="entity_id"
item-label-path="entity_id"
value="{{value}}"
opened="{{opened}}"
allow-custom-value="[[allowCustomEntity]]"
on-change="_fireChanged"
>
<paper-input
autofocus="[[autofocus]]"
label="[[_computeLabel(label, localize)]]"
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
value="[[value]]"
disabled="[[disabled]]"
>
<paper-icon-button
slot="suffix"
class="clear-button"
icon="hass:close"
no-ripple=""
hidden$="[[!value]]"
>Clear</paper-icon-button
>
<paper-icon-button
slot="suffix"
class="toggle-button"
icon="[[_computeToggleIcon(opened)]]"
hidden="[[!_states.length]]"
>Toggle</paper-icon-button
>
</paper-input>
<template>
<style>
paper-icon-item {
margin: -10px;
padding: 0;
}
</style>
<paper-icon-item>
<state-badge state-obj="[[item]]" slot="item-icon"></state-badge>
<paper-item-body two-line="">
<div>[[_computeStateName(item)]]</div>
<div secondary="">[[item.entity_id]]</div>
</paper-item-body>
</paper-icon-item>
</template>
</vaadin-combo-box-light>
`;
}
static get properties() {
return {
allowCustomEntity: {
type: Boolean,
value: false,
},
hass: {
type: Object,
observer: "_hassChanged",
},
_hass: Object,
_states: {
type: Array,
computed: "_computeStates(_hass, domainFilter, entityFilter)",
},
autofocus: Boolean,
label: {
type: String,
},
value: {
type: String,
notify: true,
},
opened: {
type: Boolean,
value: false,
observer: "_openedChanged",
},
domainFilter: {
type: String,
value: null,
},
entityFilter: {
type: Function,
value: null,
},
disabled: Boolean,
};
}
_computeLabel(label, localize) {
return label === undefined
? localize("ui.components.entity.entity-picker.entity")
: label;
}
_computeStates(hass, domainFilter, entityFilter) {
if (!hass) return [];
let entityIds = Object.keys(hass.states);
if (domainFilter) {
entityIds = entityIds.filter(
(eid) => eid.substr(0, eid.indexOf(".")) === domainFilter
);
}
let entities = entityIds.sort().map((key) => hass.states[key]);
if (entityFilter) {
entities = entities.filter(entityFilter);
}
return entities;
}
_computeStateName(state) {
return computeStateName(state);
}
_openedChanged(newVal) {
if (!newVal) {
this._hass = this.hass;
}
}
_hassChanged(newVal) {
if (!this.opened) {
this._hass = newVal;
}
}
_computeToggleIcon(opened) {
return opened ? "hass:menu-up" : "hass:menu-down";
}
_fireChanged(ev) {
ev.stopPropagation();
this.fire("change");
}
}
customElements.define("ha-entity-picker", HaEntityPicker);

View File

@ -0,0 +1,200 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import memoizeOne from "memoize-one";
import "./state-badge";
import computeStateName from "../../common/entity/compute_state_name";
import {
LitElement,
TemplateResult,
html,
css,
CSSResult,
property,
PropertyValues,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { HassEntity } from "home-assistant-js-websocket";
import { PolymerChangedEvent } from "../../polymer-types";
import { fireEvent } from "../../common/dom/fire_event";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: HassEntity }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-icon-item {
margin: -10px;
padding: 0;
}
</style>
<paper-icon-item>
<state-badge state-obj="[[item]]" slot="item-icon"></state-badge>
<paper-item-body two-line="">
<div class='name'>[[_computeStateName(item)]]</div>
<div secondary>[[item.entity_id]]</div>
</paper-item-body>
</paper-icon-item>
`;
}
root.querySelector("state-badge")!.stateObj = model.item;
root.querySelector(".name")!.textContent = computeStateName(model.item);
root.querySelector("[secondary]")!.textContent = model.item.entity_id;
};
class HaEntityPicker extends LitElement {
@property({ type: Boolean }) public autofocus?: boolean;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public allowCustomEntity;
@property() public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public domainFilter?: string;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) private _opened?: boolean;
@property() private _hass?: HomeAssistant;
private _getStates = memoizeOne(
(
hass: this["hass"],
domainFilter: this["domainFilter"],
entityFilter: this["entityFilter"]
) => {
let states: HassEntity[] = [];
if (!hass) {
return [];
}
let entityIds = Object.keys(hass.states);
if (domainFilter) {
entityIds = entityIds.filter(
(eid) => eid.substr(0, eid.indexOf(".")) === domainFilter
);
}
states = entityIds.sort().map((key) => hass!.states[key]);
if (entityFilter) {
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
stateObj.entity_id === this.value || entityFilter!(stateObj)
);
}
return states;
}
);
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("hass") && !this._opened) {
this._hass = this.hass;
}
}
protected render(): TemplateResult | void {
const states = this._getStates(
this._hass,
this.domainFilter,
this.entityFilter
);
return html`
<vaadin-combo-box-light
item-value-path="entity_id"
item-label-path="entity_id"
.items=${states}
.value=${this._value}
.allowCustomValue=${this.allowCustomEntity}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
<paper-input
.autofocus=${this.autofocus}
.label=${this.label === undefined && this._hass
? this._hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.value=${this._value}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<paper-icon-button
slot="suffix"
class="clear-button"
icon="hass:close"
no-ripple
>
Clear
</paper-icon-button>
`
: ""}
${states.length > 0
? html`
<paper-icon-button
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</paper-icon-button>
`
: ""}
</paper-input>
</vaadin-combo-box-light>
`;
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
const newValue = ev.detail.value;
if (newValue !== this._value) {
this.value = ev.detail.value;
setTimeout(() => {
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "change");
}, 0);
}
}
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
}
customElements.define("ha-entity-picker", HaEntityPicker);

View File

@ -9,7 +9,7 @@ import {
html,
CSSResult,
css,
PropertyDeclarations,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { HassEntity } from "home-assistant-js-websocket";
@ -17,7 +17,7 @@ import { HassEntity } from "home-assistant-js-websocket";
class HaEntityToggle extends LitElement {
// hass is not a property so that we only re-render on stateObj changes
public hass?: HomeAssistant;
public stateObj?: HassEntity;
@property() public stateObj?: HassEntity;
protected render(): TemplateResult | void {
if (!this.stateObj) {
@ -51,12 +51,6 @@ class HaEntityToggle extends LitElement {
`;
}
static get properties(): PropertyDeclarations {
return {
stateObj: {},
};
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.addEventListener("click", (ev) => ev.stopPropagation());

View File

@ -1,110 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../ha-icon";
import computeStateDomain from "../../common/entity/compute_state_domain";
import stateIcon from "../../common/entity/state_icon";
class StateBadge extends PolymerElement {
static get template() {
return html`
<style>
:host {
position: relative;
display: inline-block;
width: 40px;
color: var(--paper-item-icon-color, #44739e);
border-radius: 50%;
height: 40px;
text-align: center;
background-size: cover;
line-height: 40px;
}
ha-icon {
transition: color 0.3s ease-in-out, filter 0.3s ease-in-out;
}
/* Color the icon if light or sun is on */
ha-icon[data-domain="light"][data-state="on"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="binary_sensor"][data-state="on"],
ha-icon[data-domain="fan"][data-state="on"],
ha-icon[data-domain="sun"][data-state="above_horizon"] {
color: var(--paper-item-icon-active-color, #fdd835);
}
/* Color the icon if unavailable */
ha-icon[data-state="unavailable"] {
color: var(--state-icon-unavailable-color);
}
</style>
<ha-icon
id="icon"
data-domain$="[[_computeDomain(stateObj)]]"
data-state$="[[stateObj.state]]"
icon="[[_computeIcon(stateObj, overrideIcon)]]"
></ha-icon>
`;
}
static get properties() {
return {
stateObj: {
type: Object,
observer: "_updateIconAppearance",
},
overrideIcon: String,
};
}
_computeDomain(stateObj) {
return computeStateDomain(stateObj);
}
_computeIcon(stateObj, overrideIcon) {
return overrideIcon || stateIcon(stateObj);
}
_updateIconAppearance(newVal) {
var errorMessage = null;
const iconStyle = {
color: "",
filter: "",
};
const hostStyle = {
backgroundImage: "",
};
// hide icon if we have entity picture
if (newVal.attributes.entity_picture) {
hostStyle.backgroundImage =
"url(" + newVal.attributes.entity_picture + ")";
iconStyle.display = "none";
} else {
if (newVal.attributes.hs_color) {
const hue = newVal.attributes.hs_color[0];
const sat = newVal.attributes.hs_color[1];
if (sat > 10) iconStyle.color = `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
if (newVal.attributes.brightness) {
const brightness = newVal.attributes.brightness;
if (typeof brightness !== "number") {
errorMessage = `Type error: state-badge expected number, but type of ${
newVal.entity_id
}.attributes.brightness is ${typeof brightness} (${brightness})`;
// eslint-disable-next-line
console.warn(errorMessage);
}
// lowest brighntess will be around 50% (that's pretty dark)
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
}
}
Object.assign(this.$.icon.style, iconStyle);
Object.assign(this.style, hostStyle);
if (errorMessage) {
throw new Error(`Frontend error: ${errorMessage}`);
}
}
}
customElements.define("state-badge", StateBadge);

View File

@ -0,0 +1,127 @@
import {
LitElement,
TemplateResult,
css,
CSSResult,
html,
property,
PropertyValues,
query,
} from "lit-element";
import "../ha-icon";
import computeStateDomain from "../../common/entity/compute_state_domain";
import stateIcon from "../../common/entity/state_icon";
import { HassEntity } from "home-assistant-js-websocket";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { HaIcon } from "../ha-icon";
class StateBadge extends LitElement {
@property() public stateObj?: HassEntity;
@property() public overrideIcon?: string;
@query("ha-icon") private _icon!: HaIcon;
protected render(): TemplateResult | void {
const stateObj = this.stateObj;
if (!stateObj) {
return html``;
}
return html`
<ha-icon
id="icon"
data-domain=${computeStateDomain(stateObj)}
data-state=${stateObj.state}
.icon=${this.overrideIcon || stateIcon(stateObj)}
></ha-icon>
`;
}
protected updated(changedProps: PropertyValues) {
if (!changedProps.has("stateObj")) {
return;
}
const stateObj = this.stateObj;
const iconStyle: Partial<CSSStyleDeclaration> = {
color: "",
filter: "",
};
const hostStyle: Partial<CSSStyleDeclaration> = {
backgroundImage: "",
};
if (stateObj) {
// hide icon if we have entity picture
if (stateObj.attributes.entity_picture) {
hostStyle.backgroundImage =
"url(" + stateObj.attributes.entity_picture + ")";
iconStyle.display = "none";
} else {
if (stateObj.attributes.hs_color) {
const hue = stateObj.attributes.hs_color[0];
const sat = stateObj.attributes.hs_color[1];
if (sat > 10) {
iconStyle.color = `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
}
if (stateObj.attributes.brightness) {
const brightness = stateObj.attributes.brightness;
if (typeof brightness !== "number") {
const errorMessage = `Type error: state-badge expected number, but type of ${
stateObj.entity_id
}.attributes.brightness is ${typeof brightness} (${brightness})`;
// tslint:disable-next-line
console.warn(errorMessage);
}
// lowest brighntess will be around 50% (that's pretty dark)
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
}
}
}
Object.assign(this._icon.style, iconStyle);
Object.assign(this.style, hostStyle);
}
static get styles(): CSSResult {
return css`
:host {
position: relative;
display: inline-block;
width: 40px;
color: var(--paper-item-icon-color, #44739e);
border-radius: 50%;
height: 40px;
text-align: center;
background-size: cover;
line-height: 40px;
}
ha-icon {
transition: color 0.3s ease-in-out, filter 0.3s ease-in-out;
}
/* Color the icon if light or sun is on */
ha-icon[data-domain="light"][data-state="on"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="binary_sensor"][data-state="on"],
ha-icon[data-domain="fan"][data-state="on"],
ha-icon[data-domain="sun"][data-state="above_horizon"] {
color: var(--paper-item-icon-active-color, #fdd835);
}
/* Color the icon if unavailable */
ha-icon[data-state="unavailable"] {
color: var(--state-icon-unavailable-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"state-badge": StateBadge;
}
}
customElements.define("state-badge", StateBadge);

View File

@ -9,9 +9,12 @@ class HaCard extends PolymerElement {
:host {
@apply --paper-material-elevation-1;
display: block;
border-radius: 2px;
border-radius: var(--ha-card-border-radius, 2px);
transition: all 0.3s ease-out;
background-color: var(--paper-card-background-color, white);
background: var(
--ha-card-background,
var(--paper-card-background-color, white)
);
color: var(--primary-text-color);
}
.header {

View File

@ -1,18 +0,0 @@
import "@polymer/iron-icon/iron-icon";
const IronIconClass = customElements.get("iron-icon");
let loaded = false;
class HaIcon extends IronIconClass {
listen(...args) {
super.listen(...args);
if (!loaded && this._iconsetName === "mdi") {
loaded = true;
import(/* webpackChunkName: "mdi-icons" */ "../resources/mdi-icons");
}
}
}
customElements.define("ha-icon", HaIcon);

36
src/components/ha-icon.ts Normal file
View File

@ -0,0 +1,36 @@
import { Constructor } from "lit-element";
import "@polymer/iron-icon/iron-icon";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { IronIconElement } from "@polymer/iron-icon/iron-icon";
const ironIconClass = customElements.get("iron-icon") as Constructor<
IronIconElement
>;
let loaded = false;
export class HaIcon extends ironIconClass {
private _iconsetName?: string;
public listen(
node: EventTarget | null,
eventName: string,
methodName: string
): void {
super.listen(node, eventName, methodName);
if (!loaded && this._iconsetName === "mdi") {
loaded = true;
import(/* webpackChunkName: "mdi-icons" */ "../resources/mdi-icons");
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-icon": HaIcon;
}
}
customElements.define("ha-icon", HaIcon);

View File

@ -3,8 +3,8 @@ import {
html,
CSSResult,
css,
PropertyDeclarations,
PropertyValues,
property,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "@polymer/app-layout/app-toolbar/app-toolbar";
@ -82,13 +82,9 @@ const computePanels = (hass: HomeAssistant) => {
* @appliesMixin LocalizeMixin
*/
class HaSidebar extends LitElement {
public hass?: HomeAssistant;
public _defaultPage?: string;
constructor() {
super();
this._defaultPage = localStorage.defaultPage || DEFAULT_PANEL;
}
@property() public hass?: HomeAssistant;
@property() public _defaultPage?: string =
localStorage.defaultPage || DEFAULT_PANEL;
protected render() {
const hass = this.hass;
@ -217,13 +213,6 @@ class HaSidebar extends LitElement {
`;
}
static get properties(): PropertyDeclarations {
return {
hass: {},
_defaultPage: {},
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (!this.hass || !changedProps.has("hass")) {
return false;

17
src/data/automation.ts Normal file
View File

@ -0,0 +1,17 @@
import {
HassEntityBase,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
export interface AutomationEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & {
id?: string;
};
}
export interface AutomationConfig {
alias: string;
trigger: any[];
condition?: any[];
action: any[];
}

12
src/data/camera.ts Normal file
View File

@ -0,0 +1,12 @@
import { HomeAssistant } from "../types";
export interface CameraThumbnail {
content_type: string;
content: string;
}
export const fetchThumbnail = (hass: HomeAssistant, entityId: string) =>
hass.callWS<CameraThumbnail>({
type: "camera_thumbnail",
entity_id: entityId,
});

46
src/data/person.ts Normal file
View File

@ -0,0 +1,46 @@
import { HomeAssistant } from "../types";
export interface Person {
id: string;
name: string;
user_id?: string;
device_trackers?: string[];
}
export interface PersonMutableParams {
name: string;
user_id: string | null;
device_trackers: string[];
}
export const fetchPersons = (hass: HomeAssistant) =>
hass.callWS<{
storage: Person[];
config: Person[];
}>({ type: "person/list" });
export const createPerson = (
hass: HomeAssistant,
values: PersonMutableParams
) =>
hass.callWS<Person>({
type: "person/create",
...values,
});
export const updatePerson = (
hass: HomeAssistant,
personId: string,
updates: Partial<PersonMutableParams>
) =>
hass.callWS<Person>({
type: "person/update",
person_id: personId,
...updates,
});
export const deletePerson = (hass: HomeAssistant, personId: string) =>
hass.callWS({
type: "person/delete",
person_id: personId,
});

5
src/data/script.ts Normal file
View File

@ -0,0 +1,5 @@
export interface EventAction {
event: string;
event_data?: { [key: string]: any };
event_data_template?: { [key: string]: any };
}

View File

@ -7,8 +7,19 @@ export interface ZHADeviceEntity extends HassEntity {
};
}
export interface ZHAEntities {
[key: string]: HassEntity[];
export interface ZHAEntityReference extends HassEntity {
name: string;
}
export interface ZHADevice {
name: string;
ieee: string;
manufacturer: string;
model: string;
quirk_applied: boolean;
quirk_class: string;
entities: ZHAEntityReference[];
manufacturer_code: number;
}
export interface Attribute {
@ -19,6 +30,7 @@ export interface Attribute {
export interface Cluster {
name: string;
id: number;
endpoint_id: number;
type: string;
}
@ -29,7 +41,8 @@ export interface Command {
}
export interface ReadAttributeServiceData {
entity_id: string;
ieee: string;
endpoint_id: number;
cluster_id: number;
cluster_type: string;
attribute: number;
@ -41,64 +54,60 @@ export const reconfigureNode = (
ieeeAddress: string
): Promise<void> =>
hass.callWS({
type: "zha/nodes/reconfigure",
type: "zha/devices/reconfigure",
ieee: ieeeAddress,
});
export const fetchAttributesForCluster = (
hass: HomeAssistant,
entityId: string,
ieeeAddress: string,
endpointId: number,
clusterId: number,
clusterType: string
): Promise<Attribute[]> =>
hass.callWS({
type: "zha/entities/clusters/attributes",
entity_id: entityId,
type: "zha/devices/clusters/attributes",
ieee: ieeeAddress,
endpoint_id: endpointId,
cluster_id: clusterId,
cluster_type: clusterType,
});
export const fetchDevices = (hass: HomeAssistant): Promise<ZHADevice[]> =>
hass.callWS({
type: "zha/devices",
});
export const readAttributeValue = (
hass: HomeAssistant,
data: ReadAttributeServiceData
): Promise<string> => {
return hass.callWS({
...data,
type: "zha/entities/clusters/attributes/value",
type: "zha/devices/clusters/attributes/value",
});
};
export const fetchCommandsForCluster = (
hass: HomeAssistant,
entityId: string,
ieeeAddress: string,
endpointId: number,
clusterId: number,
clusterType: string
): Promise<Command[]> =>
hass.callWS({
type: "zha/entities/clusters/commands",
entity_id: entityId,
type: "zha/devices/clusters/commands",
ieee: ieeeAddress,
endpoint_id: endpointId,
cluster_id: clusterId,
cluster_type: clusterType,
});
export const fetchClustersForZhaNode = (
hass: HomeAssistant,
entityId: string,
ieeeAddress: string
): Promise<Cluster[]> =>
hass.callWS({
type: "zha/entities/clusters",
entity_id: entityId,
type: "zha/devices/clusters",
ieee: ieeeAddress,
});
export const fetchEntitiesForZhaNode = (
hass: HomeAssistant
): Promise<ZHAEntities> =>
hass.callWS({
type: "zha/entities",
});

View File

@ -18,6 +18,10 @@ class HaStoreAuth extends LocalizeMixin(PolymerElement) {
right: 16px;
}
.card-content {
color: var(--primary-text-color);
}
.card-actions {
text-align: right;
border-top: 0;

View File

@ -1,4 +1,9 @@
import { PolymerElement } from "@polymer/polymer/polymer-element";
import {
PropertyDeclarations,
PropertyValues,
UpdatingElement,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import "./more-info-alarm_control_panel";
import "./more-info-automation";
@ -23,26 +28,30 @@ import "./more-info-weather";
import stateMoreInfoType from "../../../common/entity/state_more_info_type";
import dynamicContentUpdater from "../../../common/dom/dynamic_content_updater";
import { HomeAssistant } from "../../../types";
class MoreInfoContent extends PolymerElement {
static get properties() {
class MoreInfoContent extends UpdatingElement {
public hass?: HomeAssistant;
public stateObj?: HassEntity;
private _detachedChild?: ChildNode;
static get properties(): PropertyDeclarations {
return {
hass: Object,
stateObj: Object,
hass: {},
stateObj: {},
};
}
static get observers() {
return ["stateObjChanged(stateObj, hass)"];
}
constructor() {
super();
protected firstUpdated(): void {
this.style.display = "block";
}
stateObjChanged(stateObj, hass) {
let moreInfoType;
// This is not a lit element, but an updating element, so we implement update
protected update(changedProps: PropertyValues): void {
super.update(changedProps);
const stateObj = this.stateObj;
const hass = this.hass;
if (!stateObj || !hass) {
if (this.lastChild) {
this._detachedChild = this.lastChild;
@ -51,18 +60,20 @@ class MoreInfoContent extends PolymerElement {
}
return;
}
if (this._detachedChild) {
this.appendChild(this._detachedChild);
this._detachedChild = null;
}
if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
moreInfoType = stateObj.attributes.custom_ui_more_info;
} else {
moreInfoType = "more-info-" + stateMoreInfoType(stateObj);
this._detachedChild = undefined;
}
const moreInfoType =
stateObj.attributes && "custom_ui_more_info" in stateObj.attributes
? stateObj.attributes.custom_ui_more_info
: "more-info-" + stateMoreInfoType(stateObj);
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
hass: hass,
stateObj: stateObj,
hass,
stateObj,
});
}
}

View File

@ -1,13 +1,14 @@
import { Constructor, LitElement } from "lit-element";
import { HassBaseEl } from "./hass-base-mixin";
import { HaToast } from "../../components/ha-toast";
import { computeRTL } from "../../common/util/compute_rtl";
export default (superClass: Constructor<LitElement & HassBaseEl>) =>
class extends superClass {
private _discToast?: HaToast;
protected hassConnected() {
super.hassConnected();
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
// Need to load in advance because when disconnected, can't dynamically load code.
import(/* webpackChunkName: "ha-toast" */ "../../components/ha-toast");
}
@ -24,10 +25,13 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
if (!this._discToast) {
const el = document.createElement("ha-toast");
el.duration = 0;
el.text = this.hass!.localize("ui.notification_toast.connection_lost");
this._discToast = el;
this.shadowRoot!.appendChild(el as any);
}
this._discToast.dir = computeRTL(this.hass!);
this._discToast.text = this.hass!.localize(
"ui.notification_toast.connection_lost"
);
this._discToast.opened = true;
}
};

View File

@ -1,4 +1,8 @@
import { Constructor } from "lit-element";
import {
Constructor,
// @ts-ignore
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
/* tslint:disable */
@ -17,14 +21,9 @@ export class HassBaseEl {
export default <T>(superClass: Constructor<T>): Constructor<T & HassBaseEl> =>
// @ts-ignore
class extends superClass {
private __provideHass: HTMLElement[];
private __provideHass: HTMLElement[] = [];
// @ts-ignore
protected hass: HomeAssistant;
constructor() {
super();
this.__provideHass = [];
}
@property() protected hass: HomeAssistant;
// Exists so all methods can safely call super method
protected hassConnected() {

View File

@ -1,11 +1,5 @@
import "@polymer/app-route/app-location";
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
css,
} from "lit-element";
import { html, LitElement, PropertyValues, css, property } from "lit-element";
import "../home-assistant-main";
import "../ha-init-page";
@ -43,19 +37,9 @@ export class HomeAssistantAppEl extends ext(HassBaseMixin(LitElement), [
NotificationMixin,
dialogManagerMixin,
]) {
private _route?: Route;
private _error?: boolean;
private _panelUrl?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
_route: {},
_routeData: {},
_panelUrl: {},
_error: {},
};
}
@property() private _route?: Route;
@property() private _error?: boolean;
@property() private _panelUrl?: string;
protected render() {
const hass = this.hass;

View File

@ -2,10 +2,10 @@ import {
LitElement,
html,
TemplateResult,
PropertyDeclarations,
CSSResult,
css,
PropertyValues,
property,
} from "lit-element";
import "@polymer/app-layout/app-drawer-layout/app-drawer-layout";
import "@polymer/app-layout/app-drawer/app-drawer";
@ -33,17 +33,9 @@ declare global {
}
class HomeAssistantMain extends LitElement {
public hass?: HomeAssistant;
public route?: Route;
private _narrow?: boolean;
static get properties(): PropertyDeclarations {
return {
hass: {},
_narrow: {},
route: {},
};
}
@property() public hass?: HomeAssistant;
@property() public route?: Route;
@property() private _narrow?: boolean;
protected render(): TemplateResult | void {
const hass = this.hass;

View File

@ -1,9 +1,4 @@
import {
LitElement,
html,
PropertyDeclarations,
PropertyValues,
} from "lit-element";
import { LitElement, html, PropertyValues, property } from "lit-element";
import "./hass-loading-screen";
import { HomeAssistant, Panel, PanelElement, Route } from "../types";
@ -112,34 +107,16 @@ function ensureLoaded(panel): Promise<void> | null {
}
class PartialPanelResolver extends LitElement {
public hass?: HomeAssistant;
public narrow?: boolean;
public showMenu?: boolean;
public route?: Route | null;
@property() public hass?: HomeAssistant;
@property() public narrow?: boolean;
@property() public showMenu?: boolean;
@property() public route?: Route | null;
private _routeTail?: Route | null;
@property() private _routeTail?: Route | null;
@property() private _panelEl?: PanelElement;
@property() private _error?: boolean;
private _panel?: Panel;
private _panelEl?: PanelElement;
private _error?: boolean;
private _cache: { [name: string]: PanelElement };
static get properties(): PropertyDeclarations {
return {
hass: {},
narrow: {},
showMenu: {},
route: {},
_routeTail: {},
_error: {},
_panelEl: {},
};
}
constructor() {
super();
this._cache = {};
}
private _cache: { [name: string]: PanelElement } = {};
protected render() {
if (this._error) {

View File

@ -51,7 +51,13 @@ class DialogAreaDetail extends LitElement {
opened
@opened-changed="${this._openedChanged}"
>
<h2>${this._params.entry ? this._params.entry.name : "New Area"}</h2>
<h2>
${this._params.entry
? this._params.entry.name
: this.hass.localize(
"ui.panel.config.area_registry.editor.default_name"
)}
</h2>
<paper-dialog-scrollable>
${this._error
? html`
@ -62,7 +68,7 @@ class DialogAreaDetail extends LitElement {
<paper-input
.value=${this._name}
@value-changed=${this._nameChanged}
label="Name"
.label=${this.hass.localize("ui.dialogs.more_info_settings.name")}
error-message="Name is required"
.invalid=${nameInvalid}
></paper-input>
@ -76,7 +82,9 @@ class DialogAreaDetail extends LitElement {
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
DELETE
${this.hass.localize(
"ui.panel.config.area_registry.editor.delete"
)}
</paper-button>
`
: html``}
@ -84,7 +92,13 @@ class DialogAreaDetail extends LitElement {
@click="${this._updateEntry}"
.disabled=${nameInvalid || this._submitting}
>
${this._params.entry ? "UPDATE" : "CREATE"}
${this._params.entry
? this.hass.localize(
"ui.panel.config.area_registry.editor.update"
)
: this.hass.localize(
"ui.panel.config.area_registry.editor.create"
)}
</paper-button>
</div>
</paper-dialog>

View File

@ -50,7 +50,9 @@ class HaConfigAreaRegistry extends LitElement {
return html`
<hass-subpage header="Area Registry">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Area Registry</span>
<span slot="header">
${this.hass.localize("ui.panel.config.area_registry.picker.header")}
</span>
<span slot="introduction">
Areas are used to organize where devices are. This information will
be used throughout Home Assistant to help you in organizing your
@ -74,10 +76,14 @@ class HaConfigAreaRegistry extends LitElement {
${this._items.length === 0
? html`
<div class="empty">
Looks like you have no areas yet!
${this.hass.localize(
"ui.panel.config.area_registry.picker.no_areas"
)}
<paper-button @click=${this._createArea}>
CREATE AREA</paper-button
>
${this.hass.localize(
"ui.panel.config.area_registry.picker.create_area"
)}
</paper-button>
</div>
`
: html``}

View File

@ -14,7 +14,7 @@ export interface AreaRegistryDetailDialogParams {
}
export const loadAreaRegistryDetailDialog = () =>
import(/* webpackChunkName: "entity-registry-detail-dialog" */ "./dialog-area-registry-detail");
import(/* webpackChunkName: "area-registry-detail-dialog" */ "./dialog-area-registry-detail");
export const showAreaRegistryDetailDialog = (
element: HTMLElement,

View File

@ -1,298 +0,0 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-fab/paper-fab";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { h, render } from "preact";
import "../../../layouts/ha-app-layout";
import Automation from "../js/automation";
import unmountPreact from "../../../common/preact/unmount";
import computeStateName from "../../../common/entity/compute_state_name";
import NavigateMixin from "../../../mixins/navigate-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
function AutomationEditor(mountEl, props, mergeEl) {
return render(h(Automation, props), mountEl, mergeEl);
}
/*
* @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin
*/
class HaAutomationEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
static get template() {
return html`
<style include="ha-style">
.errors {
padding: 20px;
font-weight: bold;
color: var(--google-red-500);
}
.content {
padding-bottom: 20px;
}
paper-card {
display: block;
}
.triggers,
.script {
margin-top: -16px;
}
.triggers paper-card,
.script paper-card {
margin-top: 16px;
}
.add-card paper-button {
display: block;
text-align: center;
}
.card-menu {
position: absolute;
top: 0;
right: 0;
z-index: 1;
color: var(--primary-text-color);
}
.card-menu paper-item {
cursor: pointer;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
paper-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
margin-bottom: -80px;
transition: margin-bottom 0.3s;
}
paper-fab[is-wide] {
bottom: 24px;
right: 24px;
}
paper-fab[dirty] {
margin-bottom: 0;
}
</style>
<ha-app-layout has-scrolling-region="">
<app-header slot="header" fixed="">
<app-toolbar>
<paper-icon-button
icon="hass:arrow-left"
on-click="backTapped"
></paper-icon-button>
<div main-title="">[[computeName(automation, localize)]]</div>
</app-toolbar>
</app-header>
<div class="content">
<template is="dom-if" if="[[errors]]">
<div class="errors">[[errors]]</div>
</template>
<div id="root"></div>
</div>
<paper-fab
slot="fab"
is-wide$="[[isWide]]"
dirty$="[[dirty]]"
icon="hass:content-save"
title="[[localize('ui.panel.config.automation.editor.save')]]"
on-click="saveAutomation"
></paper-fab>
</ha-app-layout>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "_updateComponent",
},
narrow: {
type: Boolean,
},
showMenu: {
type: Boolean,
value: false,
},
errors: {
type: Object,
value: null,
},
dirty: {
type: Boolean,
value: false,
},
config: {
type: Object,
value: null,
},
automation: {
type: Object,
observer: "automationChanged",
},
creatingNew: {
type: Boolean,
observer: "creatingNewChanged",
},
isWide: {
type: Boolean,
observer: "_updateComponent",
},
_rendered: {
type: Object,
value: null,
},
_renderScheduled: {
type: Boolean,
value: false,
},
};
}
ready() {
this.configChanged = this.configChanged.bind(this);
super.ready(); // This call will initialize preact.
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._rendered) {
unmountPreact(this._rendered);
this._rendered = null;
}
}
configChanged(config) {
// onChange gets called a lot during initial rendering causing recursing calls.
if (this._rendered === null) return;
this.config = config;
this.errors = null;
this.dirty = true;
this._updateComponent();
}
automationChanged(newVal, oldVal) {
if (!newVal) return;
if (!this.hass) {
setTimeout(() => this.automationChanged(newVal, oldVal), 0);
return;
}
if (oldVal && oldVal.attributes.id === newVal.attributes.id) {
return;
}
this.hass
.callApi("get", "config/automation/config/" + newVal.attributes.id)
.then(
function(config) {
// Normalize data: ensure trigger, action and condition are lists
// Happens when people copy paste their automations into the config
["trigger", "condition", "action"].forEach(function(key) {
var value = config[key];
if (value && !Array.isArray(value)) {
config[key] = [value];
}
});
this.dirty = false;
this.config = config;
this._updateComponent();
}.bind(this)
);
}
creatingNewChanged(newVal) {
if (!newVal) {
return;
}
this.dirty = false;
this.config = {
alias: this.localize("ui.panel.config.automation.editor.default_name"),
trigger: [{ platform: "state" }],
condition: [],
action: [{ service: "" }],
};
this._updateComponent();
}
backTapped() {
if (
this.dirty &&
// eslint-disable-next-line
!confirm(
this.localize("ui.panel.config.automation.editor.unsaved_confirm")
)
) {
return;
}
history.back();
}
async _updateComponent() {
if (this._renderScheduled || !this.hass || !this.config) return;
this._renderScheduled = true;
await 0;
if (!this._renderScheduled) return;
this._renderScheduled = false;
this._rendered = AutomationEditor(
this.$.root,
{
automation: this.config,
onChange: this.configChanged,
isWide: this.isWide,
hass: this.hass,
localize: this.localize,
},
this._rendered
);
}
saveAutomation() {
var id = this.creatingNew ? "" + Date.now() : this.automation.attributes.id;
this.hass
.callApi("post", "config/automation/config/" + id, this.config)
.then(
function() {
this.dirty = false;
if (this.creatingNew) {
this.navigate(`/config/automation/edit/${id}`, true);
}
}.bind(this),
function(errors) {
this.errors = errors.body.message;
throw errors;
}.bind(this)
);
}
computeName(automation, localize) {
return automation
? computeStateName(automation)
: localize("ui.panel.config.automation.editor.default_name");
}
}
customElements.define("ha-automation-editor", HaAutomationEditor);

View File

@ -0,0 +1,288 @@
import {
LitElement,
TemplateResult,
html,
CSSResult,
css,
PropertyDeclarations,
PropertyValues,
} from "lit-element";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-fab/paper-fab";
import { classMap } from "lit-html/directives/class-map";
import { h, render } from "preact";
import "../../../layouts/ha-app-layout";
import Automation from "../js/automation";
import unmountPreact from "../../../common/preact/unmount";
import computeStateName from "../../../common/entity/compute_state_name";
import { haStyle } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import { AutomationEntity, AutomationConfig } from "../../../data/automation";
import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
function AutomationEditor(mountEl, props, mergeEl) {
return render(h(Automation, props), mountEl, mergeEl);
}
class HaAutomationEditor extends LitElement {
public hass?: HomeAssistant;
public automation?: AutomationEntity;
public isWide?: boolean;
public creatingNew?: boolean;
private _config?: AutomationConfig;
private _dirty?: boolean;
private _rendered?: unknown;
private _errors?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
automation: {},
creatingNew: {},
isWide: {},
_errors: {},
_dirty: {},
_config: {},
};
}
constructor() {
super();
this._configChanged = this._configChanged.bind(this);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._rendered) {
unmountPreact(this._rendered);
this._rendered = undefined;
}
}
protected render(): TemplateResult | void {
if (!this.hass) {
return;
}
return html`
<ha-app-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<paper-icon-button
icon="hass:arrow-left"
@click=${this._backTapped}
></paper-icon-button>
<div main-title>
${this.automation
? computeStateName(this.automation)
: this.hass.localize(
"ui.panel.config.automation.editor.default_name"
)}
</div>
</app-toolbar>
</app-header>
<div class="content">
${this._errors
? html`
<div class="errors">${this._errors}</div>
`
: ""}
<div
id="root"
class="${classMap({
rtl: computeRTL(this.hass),
})}"
></div>
</div>
<paper-fab
slot="fab"
?is-wide="${this.isWide}"
?dirty="${this._dirty}"
icon="hass:content-save"
.title="${this.hass.localize(
"ui.panel.config.automation.editor.save"
)}"
@click=${this._saveAutomation}
></paper-fab>
</ha-app-layout>
`;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
const oldAutomation = changedProps.get("automation") as AutomationEntity;
if (
changedProps.has("automation") &&
this.automation &&
this.hass &&
// Only refresh config if we picked a new automation. If same ID, don't fetch it.
(!oldAutomation ||
oldAutomation.attributes.id !== this.automation.attributes.id)
) {
this.hass
.callApi<AutomationConfig>(
"GET",
`config/automation/config/${this.automation.attributes.id}`
)
.then((config) => {
// Normalize data: ensure trigger, action and condition are lists
// Happens when people copy paste their automations into the config
for (const key of ["trigger", "condition", "action"]) {
const value = config[key];
if (value && !Array.isArray(value)) {
config[key] = [value];
}
}
this._dirty = false;
this._config = config;
});
}
if (changedProps.has("creatingNew") && this.creatingNew && this.hass) {
this._dirty = false;
this._config = {
alias: this.hass.localize(
"ui.panel.config.automation.editor.default_name"
),
trigger: [{ platform: "state" }],
condition: [],
action: [{ service: "" }],
};
}
if (changedProps.has("_config") && this.hass) {
this._rendered = AutomationEditor(
this.shadowRoot!.querySelector("#root"),
{
automation: this._config,
onChange: this._configChanged,
isWide: this.isWide,
hass: this.hass,
localize: this.hass.localize,
},
this._rendered
);
}
}
private _configChanged(config: AutomationConfig): void {
// onChange gets called a lot during initial rendering causing recursing calls.
if (!this._rendered) {
return;
}
this._config = config;
this._errors = undefined;
this._dirty = true;
// this._updateComponent();
}
private _backTapped(): void {
if (
this._dirty &&
!confirm(
this.hass!.localize("ui.panel.config.automation.editor.unsaved_confirm")
)
) {
return;
}
history.back();
}
private _saveAutomation(): void {
const id = this.creatingNew
? "" + Date.now()
: this.automation!.attributes.id;
this.hass!.callApi(
"POST",
"config/automation/config/" + id,
this._config
).then(
() => {
this._dirty = false;
if (this.creatingNew) {
navigate(this, `/config/automation/edit/${id}`, true);
}
},
(errors) => {
this._errors = errors.body.message;
throw errors;
}
);
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.errors {
padding: 20px;
font-weight: bold;
color: var(--google-red-500);
}
.content {
padding-bottom: 20px;
}
paper-card {
display: block;
}
.triggers,
.script {
margin-top: -16px;
}
.triggers paper-card,
.script paper-card {
margin-top: 16px;
}
.add-card paper-button {
display: block;
text-align: center;
}
.card-menu {
position: absolute;
top: 0;
right: 0;
z-index: 1;
color: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
.card-menu paper-item {
cursor: pointer;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
paper-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
margin-bottom: -80px;
transition: margin-bottom 0.3s;
}
paper-fab[is-wide] {
bottom: 24px;
right: 24px;
}
paper-fab[dirty] {
margin-bottom: 0;
}
`,
];
}
}
customElements.define("ha-automation-editor", HaAutomationEditor);

View File

@ -39,6 +39,7 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
}
.content {
padding-bottom: 24px;
direction: ltr;
}
paper-card {
display: block;

View File

@ -17,6 +17,7 @@ class HaConfigCloudForgotPassword extends EventsMixin(PolymerElement) {
<style include="iron-flex ha-style">
.content {
padding-bottom: 24px;
direction: ltr;
}
paper-card {

View File

@ -26,6 +26,7 @@ class HaConfigCloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
<style include="iron-flex ha-style">
.content {
padding-bottom: 24px;
direction: ltr;
}
[slot="introduction"] {
margin: -1em 0;

View File

@ -16,6 +16,10 @@ class HaConfigCloudRegister extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
direction: ltr;
}
[slot=introduction] {
margin: -1em 0;
}

View File

@ -81,7 +81,9 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
slot="dropdown-content"
selected="[[_computeSelectedArea(areas, device)]]"
>
<paper-item>No Area</paper-item>
<paper-item>
[[localize('ui.panel.config.integrations.config_entry.no_area')]]
</paper-item>
<template is="dom-repeat" items="[[areas]]">
<paper-item area="[[item]]">[[item.name]]</paper-item>
</template>

View File

@ -39,11 +39,11 @@ class HaConfigCustomize extends LocalizeMixin(PolymerElement) {
<div class$="[[computeClasses(isWide)]]">
<ha-config-section is-wide="[[isWide]]">
<span slot="header">Customization</span>
<span slot="header">
[[localize('ui.panel.config.customize.picker.header')]]
</span>
<span slot="introduction">
Tweak per-entity attributes.<br />
Added/edited customizations will take effect immediately. Removed
customizations will take effect when the entity is updated.
[[localize('ui.panel.config.customize.picker.introduction')]]
</span>
<ha-entity-config
hass="[[hass]]"

View File

@ -52,13 +52,14 @@ class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
type: Array,
value: [
"core",
"customize",
"person",
"entity_registry",
"area_registry",
"automation",
"script",
"zha",
"zwave",
"customize",
],
},
};

View File

@ -65,7 +65,11 @@ class DialogEntityRegistryDetail extends LitElement {
<paper-dialog-scrollable>
${!stateObj
? html`
<div>This entity is not currently available.</div>
<div>
${this.hass!.localize(
"ui.panel.config.entity_registry.editor.unavailable"
)}
</div>
`
: ""}
${this._error
@ -99,13 +103,17 @@ class DialogEntityRegistryDetail extends LitElement {
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
DELETE
${this.hass.localize(
"ui.panel.config.entity_registry.editor.delete"
)}
</paper-button>
<paper-button
@click="${this._updateEntry}"
.disabled=${invalidDomainUpdate || this._submitting}
>
UPDATE
${this.hass.localize(
"ui.panel.config.entity_registry.editor.update"
)}
</paper-button>
</div>
</paper-dialog>

View File

@ -53,7 +53,11 @@ class HaConfigEntityRegistry extends LitElement {
return html`
<hass-subpage header="Entity Registry">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Entity Registry</span>
<span slot="header">
${this.hass.localize(
"ui.panel.config.entity_registry.picker.header"
)}
</span>
<span slot="introduction">
Home Assistant keeps a registry of every entity it has ever seen
that can be uniquely identified. Each of these entities will have an
@ -79,7 +83,9 @@ class HaConfigEntityRegistry extends LitElement {
<paper-item-body two-line>
<div class="name">
${computeEntityRegistryName(this.hass!, entry) ||
"(unavailable)"}
this.hass!.localize(
"ui.panel.config.entity_registry.picker.unavailable"
)}
</div>
<div class="secondary entity-id">
${entry.entity_id}
@ -150,6 +156,7 @@ Deleting an entry will not remove the entity from Home Assistant. To do this, yo
}
paper-card {
display: block;
direction: ltr;
}
paper-icon-item {
cursor: pointer;

View File

@ -15,6 +15,7 @@ class HaEntityConfig extends PolymerElement {
<style include="iron-flex ha-style">
paper-card {
display: block;
direction: ltr;
}
.device-picker {

View File

@ -9,19 +9,6 @@ import isComponentLoaded from "../../common/config/is_component_loaded";
import EventsMixin from "../../mixins/events-mixin";
import NavigateMixin from "../../mixins/navigate-mixin";
import(/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry");
import(/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation");
import(/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud");
import(/* webpackChunkName: "panel-config-config" */ "./config-entries/ha-config-entries");
import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core");
import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize");
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
import(/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry");
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
/*
* @appliesMixin EventsMixin
* @appliesMixin NavigateMixin
@ -136,6 +123,15 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
></ha-config-zwave>
</template>
<template is="dom-if" if='[[_equals(_routeData.page, "person")]]' restamp>
<ha-config-person
page-name="person"
route="[[route]]"
hass="[[hass]]"
is-wide="[[isWide]]"
></ha-config-person>
</template>
<template
is="dom-if"
if='[[_equals(_routeData.page, "customize")]]'
@ -207,6 +203,19 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
this.addEventListener("ha-refresh-cloud-status", () =>
this._updateCloudStatus()
);
import(/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry");
import(/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation");
import(/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud");
import(/* webpackChunkName: "panel-config-config" */ "./config-entries/ha-config-entries");
import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core");
import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize");
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
import(/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry");
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
import(/* webpackChunkName: "panel-config-person" */ "./person/ha-config-person");
}
async _updateCloudStatus() {

View File

@ -54,6 +54,7 @@ export default class NumericStateCondition extends Component {
name="value_template"
value={value_template}
onvalue-changed={this.onChange}
dir="ltr"
/>
</div>
);

View File

@ -22,6 +22,7 @@ export default class TemplateCondition extends Component {
name="value_template"
value={value_template}
onvalue-changed={this.onChange}
dir="ltr"
/>
</div>
);

View File

@ -53,6 +53,7 @@ export default class JSONTextArea extends Component {
value={value}
style={style}
onvalue-changed={this.onChange}
dir="ltr"
/>
);
}

View File

@ -0,0 +1,12 @@
import { PaperInputElement } from "@polymer/paper-input/paper-input";
// Force file to be a module to augment global scope.
export {};
declare global {
namespace JSX {
interface IntrinsicElements {
"paper-input": Partial<PaperInputElement>;
}
}
}

View File

@ -3,8 +3,26 @@ import "@polymer/paper-input/paper-input";
import JSONTextArea from "../json_textarea";
import { onChangeEvent } from "../../../../common/preact/event";
import { LocalizeFunc } from "../../../../common/translations/localize";
import { EventAction } from "../../../../data/script";
interface Props {
index: number;
action: EventAction;
localize: LocalizeFunc;
onChange: (index: number, action: EventAction) => void;
}
export default class EventActionForm extends Component<Props> {
private onChange: (event: Event) => void;
static get defaultConfig(): EventAction {
return {
event: "",
event_data: {},
};
}
export default class EventAction extends Component {
constructor() {
super();
@ -12,16 +30,11 @@ export default class EventAction extends Component {
this.serviceDataChanged = this.serviceDataChanged.bind(this);
}
serviceDataChanged(data) {
this.props.onChange(
this.props.index,
Object.assign({}, this.props.action, { data })
);
}
render({ action, localize }) {
/* eslint-disable camelcase */
const { event, event_data } = action;
public render() {
const {
action: { event, event_data },
localize,
} = this.props;
return (
<div>
<paper-input
@ -42,9 +55,11 @@ export default class EventAction extends Component {
</div>
);
}
}
EventAction.defaultConfig = {
event: "",
event_data: {},
};
private serviceDataChanged(eventData) {
this.props.onChange(this.props.index, {
...this.props.action,
event_data: eventData,
});
}
}

View File

@ -36,6 +36,7 @@ export default class WaitAction extends Component {
name="wait_template"
value={wait_template}
onvalue-changed={this.onTemplateChange}
dir="ltr"
/>
<paper-input
label={localize(

View File

@ -67,6 +67,7 @@ export default class NumericStateTrigger extends Component {
name="value_template"
value={value_template}
onvalue-changed={this.onChange}
dir="ltr"
/>
<paper-input
label={localize(

View File

@ -23,6 +23,7 @@ export default class TemplateTrigger extends Component {
name="value_template"
value={value_template}
onvalue-changed={this.onChange}
dir="ltr"
/>
</div>
);

View File

@ -0,0 +1,188 @@
import {
LitElement,
html,
css,
CSSResult,
TemplateResult,
property,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import "../../../components/entity/ha-entities-picker";
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import { PersonMutableParams } from "../../../data/person";
class DialogPersonDetail extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _name!: string;
@property() private _deviceTrackers!: string[];
@property() private _error?: string;
@property() private _params?: PersonDetailDialogParams;
@property() private _submitting?: boolean;
public async showDialog(params: PersonDetailDialogParams): Promise<void> {
this._params = params;
this._error = undefined;
this._name = this._params.entry ? this._params.entry.name : "";
this._deviceTrackers = this._params.entry
? this._params.entry.device_trackers || []
: [];
await this.updateComplete;
}
protected render(): TemplateResult | void {
if (!this._params) {
return html``;
}
const nameInvalid = this._name.trim() === "";
return html`
<paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
>
<h2>${this._params.entry ? this._params.entry.name : "New Person"}</h2>
<paper-dialog-scrollable>
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
<div class="form">
<paper-input
.value=${this._name}
@value-changed=${this._nameChanged}
label="Name"
error-message="Name is required"
.invalid=${nameInvalid}
></paper-input>
<p>
${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_intro"
)}
</p>
<ha-entities-picker
.hass=${this.hass}
.value=${this._deviceTrackers}
domainFilter="device_tracker"
.pickedEntityLabel=${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_picked"
)}
.pickEntityLabel=${this.hass.localize(
"ui.panel.config.person.detail.device_tracker_pick"
)}
@value-changed=${this._deviceTrackersChanged}
></ha-entities-picker>
</div>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${this._params.entry
? html`
<paper-button
class="danger"
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
DELETE
</paper-button>
`
: html``}
<paper-button
@click="${this._updateEntry}"
.disabled=${nameInvalid || this._submitting}
>
${this._params.entry ? "UPDATE" : "CREATE"}
</paper-button>
</div>
</paper-dialog>
`;
}
private _nameChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
this._name = ev.detail.value;
}
private _deviceTrackersChanged(ev: PolymerChangedEvent<string[]>) {
this._error = undefined;
this._deviceTrackers = ev.detail.value;
}
private async _updateEntry() {
this._submitting = true;
try {
const values: PersonMutableParams = {
name: this._name.trim(),
device_trackers: this._deviceTrackers,
// Temp, we will add this in a future PR.
user_id: null,
};
if (this._params!.entry) {
await this._params!.updateEntry(values);
} else {
await this._params!.createEntry(values);
}
this._params = undefined;
} catch (err) {
this._error = err;
} finally {
this._submitting = false;
}
}
private async _deleteEntry() {
this._submitting = true;
try {
if (await this._params!.removeEntry()) {
this._params = undefined;
}
} finally {
this._submitting = false;
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
paper-dialog {
min-width: 400px;
}
.form {
padding-bottom: 24px;
}
paper-button {
font-weight: 500;
}
paper-button.danger {
font-weight: 500;
color: var(--google-red-500);
margin-left: -12px;
margin-right: auto;
}
.error {
color: var(--google-red-500);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-person-detail": DialogPersonDetail;
}
}
customElements.define("dialog-person-detail", DialogPersonDetail);

View File

@ -0,0 +1,217 @@
import {
LitElement,
TemplateResult,
html,
css,
CSSResult,
PropertyDeclarations,
} from "lit-element";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-fab/paper-fab";
import { HomeAssistant } from "../../../types";
import {
Person,
fetchPersons,
updatePerson,
deletePerson,
createPerson,
} from "../../../data/person";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-loading-screen";
import compare from "../../../common/string/compare";
import "../ha-config-section";
import {
showPersonDetailDialog,
loadPersonDetailDialog,
} from "./show-dialog-person-detail";
class HaConfigPerson extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _storageItems?: Person[];
private _configItems?: Person[];
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_storageItems: {},
_configItems: {},
};
}
protected render(): TemplateResult | void {
if (
!this.hass ||
this._storageItems === undefined ||
this._configItems === undefined
) {
return html`
<hass-loading-screen></hass-loading-screen>
`;
}
return html`
<hass-subpage header="Persons">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Persons</span>
<span slot="introduction">
Here you can define each person of interest in Home Assistant.
${this._configItems.length > 0
? html`
<p>
Note: people configured via configuration.yaml cannot be
edited via the UI.
</p>
`
: ""}
</span>
<paper-card class="storage">
${this._storageItems.map((entry) => {
return html`
<paper-item @click=${this._openEditEntry} .entry=${entry}>
<paper-item-body>
${entry.name}
</paper-item-body>
</paper-item>
`;
})}
${this._storageItems.length === 0
? html`
<div class="empty">
Looks like you have no people yet!
<paper-button @click=${this._createPerson}>
CREATE PERSON</paper-button
>
</div>
`
: html``}
</paper-card>
${this._configItems.length > 0
? html`
<paper-card heading="Configuration.yaml people">
${this._configItems.map((entry) => {
return html`
<paper-item>
<paper-item-body>
${entry.name}
</paper-item-body>
</paper-item>
`;
})}
</paper-card>
`
: ""}
</ha-config-section>
</hass-subpage>
<paper-fab
?is-wide=${this.isWide}
icon="hass:plus"
title="Create Area"
@click=${this._createPerson}
></paper-fab>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._fetchData();
loadPersonDetailDialog();
}
private async _fetchData() {
const personData = await fetchPersons(this.hass!);
this._storageItems = personData.storage.sort((ent1, ent2) =>
compare(ent1.name, ent2.name)
);
this._configItems = personData.config.sort((ent1, ent2) =>
compare(ent1.name, ent2.name)
);
}
private _createPerson() {
this._openDialog();
}
private _openEditEntry(ev: MouseEvent) {
const entry: Person = (ev.currentTarget! as any).entry;
this._openDialog(entry);
}
private _openDialog(entry?: Person) {
showPersonDetailDialog(this, {
entry,
createEntry: async (values) => {
const created = await createPerson(this.hass!, values);
this._storageItems = this._storageItems!.concat(created).sort(
(ent1, ent2) => compare(ent1.name, ent2.name)
);
},
updateEntry: async (values) => {
const updated = await updatePerson(this.hass!, entry!.id, values);
this._storageItems = this._storageItems!.map((ent) =>
ent === entry ? updated : ent
);
},
removeEntry: async () => {
if (
!confirm(`Are you sure you want to delete this area?
All devices in this area will become unassigned.`)
) {
return false;
}
try {
await deletePerson(this.hass!, entry!.id);
this._storageItems = this._storageItems!.filter(
(ent) => ent !== entry
);
return true;
} catch (err) {
return false;
}
},
});
}
static get styles(): CSSResult {
return css`
a {
color: var(--primary-color);
}
paper-card {
display: block;
max-width: 600px;
margin: 16px auto;
}
.empty {
text-align: center;
}
paper-item {
padding-top: 4px;
padding-bottom: 4px;
}
paper-card.storage paper-item {
cursor: pointer;
}
paper-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
paper-fab[is-wide] {
bottom: 24px;
right: 24px;
}
`;
}
}
customElements.define("ha-config-person", HaConfigPerson);

View File

@ -0,0 +1,23 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { Person, PersonMutableParams } from "../../../data/person";
export interface PersonDetailDialogParams {
entry?: Person;
createEntry: (values: PersonMutableParams) => Promise<unknown>;
updateEntry: (updates: Partial<PersonMutableParams>) => Promise<unknown>;
removeEntry: () => Promise<boolean>;
}
export const loadPersonDetailDialog = () =>
import(/* webpackChunkName: "person-detail-dialog" */ "./dialog-person-detail");
export const showPersonDetailDialog = (
element: HTMLElement,
systemLogDetailParams: PersonDetailDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-person-detail",
dialogImport: loadPersonDetailDialog,
dialogParams: systemLogDetailParams,
});
};

View File

@ -30,6 +30,9 @@ class HaUserEditor extends EventsMixin(
paper-card:last-child {
margin-bottom: 16px;
}
hass-subpage paper-card:first-of-type {
direction: ltr;
}
</style>
<hass-subpage header="View user">

View File

@ -14,11 +14,7 @@ import { Cluster } from "../../../data/zha";
import "../../../layouts/ha-app-layout";
import { haStyle } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import {
ZHAClusterSelectedParams,
ZHAEntitySelectedParams,
ZHANodeSelectedParams,
} from "./types";
import { ZHAClusterSelectedParams, ZHANodeSelectedParams } from "./types";
import "./zha-cluster-attributes";
import "./zha-cluster-commands";
import "./zha-network";
@ -29,14 +25,12 @@ export class HaConfigZha extends LitElement {
public isWide?: boolean;
private _selectedNode?: HassEntity;
private _selectedCluster?: Cluster;
private _selectedEntity?: HassEntity;
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_selectedCluster: {},
_selectedEntity: {},
_selectedNode: {},
};
}
@ -64,7 +58,6 @@ export class HaConfigZha extends LitElement {
.hass="${this.hass}"
@zha-cluster-selected="${this._onClusterSelected}"
@zha-node-selected="${this._onNodeSelected}"
@zha-entity-selected="${this._onEntitySelected}"
></zha-node>
${this._selectedCluster
? html`
@ -72,7 +65,6 @@ export class HaConfigZha extends LitElement {
.isWide="${this.isWide}"
.hass="${this.hass}"
.selectedNode="${this._selectedNode}"
.selectedEntity="${this._selectedEntity}"
.selectedCluster="${this._selectedCluster}"
></zha-cluster-attributes>
@ -80,7 +72,6 @@ export class HaConfigZha extends LitElement {
.isWide="${this.isWide}"
.hass="${this.hass}"
.selectedNode="${this._selectedNode}"
.selectedEntity="${this._selectedEntity}"
.selectedCluster="${this._selectedCluster}"
></zha-cluster-commands>
`
@ -100,13 +91,6 @@ export class HaConfigZha extends LitElement {
): void {
this._selectedNode = selectedNodeEvent.detail.node;
this._selectedCluster = undefined;
this._selectedEntity = undefined;
}
private _onEntitySelected(
selectedEntityEvent: HASSDomEvent<ZHAEntitySelectedParams>
): void {
this._selectedEntity = selectedEntityEvent.detail.entity;
}
static get styles(): CSSResult[] {

View File

@ -1,4 +1,3 @@
import { HassEntity } from "home-assistant-js-websocket";
import { ZHADeviceEntity, Cluster } from "../../../data/zha";
export interface PickerTarget extends EventTarget {
@ -17,7 +16,8 @@ export interface ChangeEvent {
}
export interface SetAttributeServiceData {
entity_id: string;
ieee: string;
endpoint_id: number;
cluster_id: number;
cluster_type: string;
attribute: number;
@ -26,17 +26,14 @@ export interface SetAttributeServiceData {
}
export interface IssueCommandServiceData {
entity_id: string;
ieee: string;
endpoint_id: number;
cluster_id: number;
cluster_type: string;
command: number;
command_type: string;
}
export interface ZHAEntitySelectedParams {
entity: HassEntity;
}
export interface ZHANodeSelectedParams {
node: ZHADeviceEntity;
}

View File

@ -10,7 +10,6 @@ import {
import "@polymer/paper-button/paper-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-icon-button/paper-icon-button";
import { HassEntity } from "home-assistant-js-websocket";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import {
@ -19,7 +18,7 @@ import {
fetchAttributesForCluster,
ReadAttributeServiceData,
readAttributeValue,
ZHADeviceEntity,
ZHADevice,
} from "../../../data/zha";
import { haStyle } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
@ -34,8 +33,7 @@ export class ZHAClusterAttributes extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
public showHelp: boolean;
public selectedNode?: HassEntity;
public selectedEntity?: ZHADeviceEntity;
public selectedNode?: ZHADevice;
public selectedCluster?: Cluster;
private _attributes: Attribute[];
private _selectedAttributeIndex: number;
@ -57,7 +55,6 @@ export class ZHAClusterAttributes extends LitElement {
isWide: {},
showHelp: {},
selectedNode: {},
selectedEntity: {},
selectedCluster: {},
_attributes: {},
_selectedAttributeIndex: {},
@ -172,49 +169,54 @@ export class ZHAClusterAttributes extends LitElement {
}
private async _fetchAttributesForCluster(): Promise<void> {
if (this.selectedEntity && this.selectedCluster && this.hass) {
if (this.selectedNode && this.selectedCluster && this.hass) {
this._attributes = await fetchAttributesForCluster(
this.hass,
this.selectedEntity!.entity_id,
this.selectedEntity!.device_info!.identifiers[0][1],
this.selectedNode!.ieee,
this.selectedCluster!.endpoint_id,
this.selectedCluster!.id,
this.selectedCluster!.type
);
this._attributes.sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
}
private _computeReadAttributeServiceData():
| ReadAttributeServiceData
| undefined {
if (!this.selectedEntity || !this.selectedCluster || !this.selectedNode) {
if (!this.selectedCluster || !this.selectedNode) {
return;
}
return {
entity_id: this.selectedEntity!.entity_id,
ieee: this.selectedNode!.ieee,
endpoint_id: this.selectedCluster!.endpoint_id,
cluster_id: this.selectedCluster!.id,
cluster_type: this.selectedCluster!.type,
attribute: this._attributes[this._selectedAttributeIndex].id,
manufacturer: this._manufacturerCodeOverride
? parseInt(this._manufacturerCodeOverride as string, 10)
: this.selectedNode!.attributes.manufacturer_code,
: this.selectedNode!.manufacturer_code,
};
}
private _computeSetAttributeServiceData():
| SetAttributeServiceData
| undefined {
if (!this.selectedEntity || !this.selectedCluster || !this.selectedNode) {
if (!this.selectedCluster || !this.selectedNode) {
return;
}
return {
entity_id: this.selectedEntity!.entity_id,
ieee: this.selectedNode!.ieee,
endpoint_id: this.selectedCluster!.endpoint_id,
cluster_id: this.selectedCluster!.id,
cluster_type: this.selectedCluster!.type,
attribute: this._attributes[this._selectedAttributeIndex].id,
value: this._attributeValue,
manufacturer: this._manufacturerCodeOverride
? parseInt(this._manufacturerCodeOverride as string, 10)
: this.selectedNode!.attributes.manufacturer_code,
: this.selectedNode!.manufacturer_code,
};
}
@ -306,8 +308,7 @@ export class ZHAClusterAttributes extends LitElement {
[hidden] {
display: none;
}
</style>
`,
`,
];
}
}

View File

@ -8,14 +8,13 @@ import {
css,
} from "lit-element";
import "@polymer/paper-card/paper-card";
import { HassEntity } from "home-assistant-js-websocket";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import {
Cluster,
Command,
fetchCommandsForCluster,
ZHADeviceEntity,
ZHADevice,
} from "../../../data/zha";
import { haStyle } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
@ -29,8 +28,7 @@ import {
export class ZHAClusterCommands extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
public selectedNode?: HassEntity;
public selectedEntity?: ZHADeviceEntity;
public selectedNode?: ZHADevice;
public selectedCluster?: Cluster;
private _showHelp: boolean;
private _commands: Command[];
@ -50,7 +48,6 @@ export class ZHAClusterCommands extends LitElement {
hass: {},
isWide: {},
selectedNode: {},
selectedEntity: {},
selectedCluster: {},
_showHelp: {},
_commands: {},
@ -146,25 +143,29 @@ export class ZHAClusterCommands extends LitElement {
}
private async _fetchCommandsForCluster(): Promise<void> {
if (this.selectedEntity && this.selectedCluster && this.hass) {
if (this.selectedNode && this.selectedCluster && this.hass) {
this._commands = await fetchCommandsForCluster(
this.hass,
this.selectedEntity!.entity_id,
this.selectedEntity!.device_info!.identifiers[0][1],
this.selectedNode!.ieee,
this.selectedCluster!.endpoint_id,
this.selectedCluster!.id,
this.selectedCluster!.type
);
this._commands.sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
}
private _computeIssueClusterCommandServiceData():
| IssueCommandServiceData
| undefined {
if (!this.selectedEntity || !this.selectedCluster) {
if (!this.selectedNode || !this.selectedCluster) {
return;
}
return {
entity_id: this.selectedEntity!.entity_id,
ieee: this.selectedNode!.ieee,
endpoint_id: this.selectedCluster!.endpoint_id,
cluster_id: this.selectedCluster!.id,
cluster_type: this.selectedCluster!.type,
command: this._commands[this._selectedCommandIndex].id,
@ -257,8 +258,7 @@ export class ZHAClusterCommands extends LitElement {
[hidden] {
display: none;
}
</style>
`,
`,
];
}
}

View File

@ -11,11 +11,7 @@ import "@polymer/paper-card/paper-card";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import {
Cluster,
fetchClustersForZhaNode,
ZHADeviceEntity,
} from "../../../data/zha";
import { Cluster, fetchClustersForZhaNode, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
@ -31,14 +27,16 @@ declare global {
}
const computeClusterKey = (cluster: Cluster): string => {
return `${cluster.name} (id: ${cluster.id}, type: ${cluster.type})`;
return `${cluster.name} (Endpoint id: ${cluster.endpoint_id}, Id: ${
cluster.id
}, Type: ${cluster.type})`;
};
export class ZHAClusters extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
public showHelp: boolean;
public selectedEntity?: ZHADeviceEntity;
public selectedDevice?: ZHADevice;
private _selectedClusterIndex: number;
private _clusters: Cluster[];
@ -54,14 +52,14 @@ export class ZHAClusters extends LitElement {
hass: {},
isWide: {},
showHelp: {},
selectedEntity: {},
selectedDevice: {},
_selectedClusterIndex: {},
_clusters: {},
};
}
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedEntity")) {
if (changedProperties.has("selectedDevice")) {
this._clusters = [];
this._selectedClusterIndex = -1;
fireEvent(this, "zha-cluster-selected", {
@ -103,9 +101,11 @@ export class ZHAClusters extends LitElement {
if (this.hass) {
this._clusters = await fetchClustersForZhaNode(
this.hass,
this.selectedEntity!.entity_id,
this.selectedEntity!.device_info!.identifiers[0][1]
this.selectedDevice!.ieee
);
this._clusters.sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
}

View File

@ -0,0 +1,118 @@
import {
html,
LitElement,
property,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../common/dom/fire_event";
import { haStyle } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import "../../../components/entity/state-badge";
import { ZHADevice } from "../../../data/zha";
class ZHADeviceCard extends LitElement {
@property() public hass?: HomeAssistant;
@property() public narrow?: boolean;
@property() public device?: ZHADevice;
protected render(): TemplateResult | void {
return html`
<paper-card>
<div class="card-content">
<dl>
<dt class="label">IEEE:</dt>
<dd class="info">${this.device!.ieee}</dd>
<dt class="label">Quirk applied:</dt>
<dd class="info">${this.device!.quirk_applied}</dd>
<dt class="label">Quirk:</dt>
<dd class="info">${this.device!.quirk_class}</dd>
</dl>
</div>
<div class="device-entities">
${this.device!.entities.map(
(entity) => html`
<paper-icon-item
@click="${this._openMoreInfo}"
.entity="${entity}"
>
<state-badge
.stateObj="${this.hass!.states[entity.entity_id]}"
slot="item-icon"
></state-badge>
<paper-item-body>
<div class="name">${entity.name}</div>
<div class="secondary entity-id">${entity.entity_id}</div>
</paper-item-body>
</paper-icon-item>
`
)}
</div>
</paper-card>
`;
}
private _openMoreInfo(ev: MouseEvent): void {
fireEvent(this, "hass-more-info", {
entityId: (ev.currentTarget as any).entity.entity_id,
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host(:not([narrow])) .device-entities {
max-height: 225px;
overflow: auto;
}
paper-card {
flex: 1 0 100%;
padding-bottom: 10px;
min-width: 0;
}
.device {
width: 30%;
}
.label {
font-weight: bold;
}
.info {
color: var(--secondary-text-color);
font-weight: bold;
}
dl dt {
float: left;
width: 100px;
text-align: left;
}
dt dd {
margin-left: 10px;
text-align: left;
}
paper-icon-item {
cursor: pointer;
padding-top: 4px;
padding-bottom: 4px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-device-card": ZHADeviceCard;
}
}
customElements.define("zha-device-card", ZHADeviceCard);

View File

@ -1,170 +0,0 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { HassEntity } from "home-assistant-js-websocket";
import { fireEvent } from "../../../common/dom/fire_event";
import { fetchEntitiesForZhaNode } from "../../../data/zha";
import { haStyle } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import { ItemSelectedEvent } from "./types";
declare global {
// for fire event
interface HASSDomEvents {
"zha-entity-selected": {
entity?: HassEntity;
};
}
}
export class ZHAEntities extends LitElement {
public hass?: HomeAssistant;
public showHelp?: boolean;
public selectedNode?: HassEntity;
private _selectedEntityIndex: number;
private _entities: HassEntity[];
constructor() {
super();
this._entities = [];
this._selectedEntityIndex = -1;
}
static get properties(): PropertyDeclarations {
return {
hass: {},
showHelp: {},
selectedNode: {},
_selectedEntityIndex: {},
_entities: {},
};
}
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedNode")) {
this._entities = [];
this._selectedEntityIndex = -1;
fireEvent(this, "zha-entity-selected", {
entity: undefined,
});
this._fetchEntitiesForZhaNode();
}
super.update(changedProperties);
}
protected render(): TemplateResult | void {
return html`
<div class="node-picker">
<paper-dropdown-menu label="Entities" class="flex">
<paper-listbox
slot="dropdown-content"
.selected="${this._selectedEntityIndex}"
@iron-select="${this._selectedEntityChanged}"
>
${this._entities.map(
(entry) => html`
<paper-item>${entry.entity_id}</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div>
${this.showHelp
? html`
<div class="helpText">
Select entity to view per-entity options
</div>
`
: ""}
${this._selectedEntityIndex !== -1
? html`
<div class="actions">
<paper-button @click="${this._showEntityInformation}"
>Entity Information</paper-button
>
</div>
`
: ""}
`;
}
private async _fetchEntitiesForZhaNode(): Promise<void> {
if (this.hass) {
const fetchedEntities = await fetchEntitiesForZhaNode(this.hass);
this._entities = fetchedEntities[this.selectedNode!.attributes.ieee];
}
}
private _selectedEntityChanged(event: ItemSelectedEvent): void {
this._selectedEntityIndex = event.target!.selected;
fireEvent(this, "zha-entity-selected", {
entity: this._entities[this._selectedEntityIndex],
});
}
private _showEntityInformation(): void {
fireEvent(this, "hass-more-info", {
entityId: this._entities[this._selectedEntityIndex].entity_id,
});
}
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;
}
.node-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;
}
.actions {
border-top: 1px solid #e8e8e8;
padding: 5px 16px;
position: relative;
}
.actions paper-button:not([disabled]) {
color: var(--primary-color);
font-weight: 500;
}
.helpText {
color: grey;
padding: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-entities": ZHAEntities;
}
}
customElements.define("zha-entities", ZHAEntities);

View File

@ -102,8 +102,7 @@ export class ZHANetwork extends LitElement {
[hidden] {
display: none;
}
</style>
`,
`,
];
}
}

View File

@ -4,6 +4,7 @@ import {
PropertyDeclarations,
TemplateResult,
CSSResult,
PropertyValues,
css,
} from "lit-element";
import "@polymer/paper-button/paper-button";
@ -11,29 +12,22 @@ import "@polymer/paper-card/paper-card";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { HassEntity } from "home-assistant-js-websocket";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import computeStateName from "../../../common/entity/compute_state_name";
import sortByName from "../../../common/entity/states_sort_by_name";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import { haStyle } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import {
ItemSelectedEvent,
NodeServiceData,
ZHAEntitySelectedParams,
} from "./types";
import { ItemSelectedEvent, NodeServiceData } from "./types";
import "./zha-clusters";
import "./zha-entities";
import { reconfigureNode } from "../../../data/zha";
import "./zha-device-card";
import { reconfigureNode, fetchDevices, ZHADevice } from "../../../data/zha";
declare global {
// for fire event
interface HASSDomEvents {
"zha-node-selected": {
node?: HassEntity;
node?: ZHADevice;
};
}
}
@ -43,10 +37,9 @@ export class ZHANode extends LitElement {
public isWide?: boolean;
private _showHelp: boolean;
private _selectedNodeIndex: number;
private _selectedNode?: HassEntity;
private _selectedEntity?: HassEntity;
private _selectedNode?: ZHADevice;
private _serviceData?: {};
private _nodes: HassEntity[];
private _nodes: ZHADevice[];
constructor() {
super();
@ -62,13 +55,31 @@ export class ZHANode extends LitElement {
_showHelp: {},
_selectedNodeIndex: {},
_selectedNode: {},
_entities: {},
_serviceData: {},
_selectedEntity: {},
_nodes: {},
};
}
public firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this._nodes.length === 0) {
this._fetchDevices();
}
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
}
protected serviceCalled(ev): void {
// Check if this is for us
if (ev.detail.success && ev.detail.service === "remove") {
this._selectedNodeIndex = -1;
this._fetchDevices();
}
}
protected render(): TemplateResult | void {
this._nodes = this._computeNodes(this.hass);
return html`
<ha-config-section .isWide="${this.isWide}">
<div class="sectionHeader" slot="header">
@ -94,12 +105,11 @@ export class ZHANode extends LitElement {
<paper-listbox
slot="dropdown-content"
@iron-select="${this._selectedNodeChanged}"
.selected="${this._selectedNodeIndex}"
>
${this._nodes.map(
(entry) => html`
<paper-item
>${this._computeSelectCaption(entry)}</paper-item
>
<paper-item>${entry.name}</paper-item>
`
)}
</paper-listbox>
@ -112,9 +122,18 @@ export class ZHANode extends LitElement {
</div>
`
: ""}
${this._selectedNodeIndex !== -1
? html`
<zha-device-card
class="card"
.hass="${this.hass}"
.device="${this._selectedNode}"
.narrow="${!this.isWide}"
></zha-device-card>
`
: ""}
${this._selectedNodeIndex !== -1 ? this._renderNodeActions() : ""}
${this._selectedNodeIndex !== -1 ? this._renderEntities() : ""}
${this._selectedEntity ? this._renderClusters() : ""}
${this._selectedNode ? this._renderClusters() : ""}
</paper-card>
</ha-config-section>
`;
@ -123,9 +142,6 @@ export class ZHANode extends LitElement {
private _renderNodeActions(): TemplateResult {
return html`
<div class="card-actions">
<paper-button @click="${this._showNodeInformation}"
>Node Information</paper-button
>
<paper-button @click="${this._onReconfigureNodeClick}"
>Reconfigure Node</paper-button
>
@ -158,22 +174,11 @@ export class ZHANode extends LitElement {
`;
}
private _renderEntities(): TemplateResult {
return html`
<zha-entities
.hass="${this.hass}"
.selectedNode="${this._selectedNode}"
.showHelp="${this._showHelp}"
@zha-entity-selected="${this._onEntitySelected}"
></zha-entities>
`;
}
private _renderClusters(): TemplateResult {
return html`
<zha-clusters
.hass="${this.hass}"
.selectedEntity="${this._selectedEntity}"
.selectedDevice="${this._selectedNode}"
.showHelp="${this._showHelp}"
></zha-clusters>
`;
@ -186,50 +191,26 @@ export class ZHANode extends LitElement {
private _selectedNodeChanged(event: ItemSelectedEvent): void {
this._selectedNodeIndex = event!.target!.selected;
this._selectedNode = this._nodes[this._selectedNodeIndex];
this._selectedEntity = undefined;
fireEvent(this, "zha-node-selected", { node: this._selectedNode });
this._serviceData = this._computeNodeServiceData();
}
private async _onReconfigureNodeClick(): Promise<void> {
if (this.hass) {
await reconfigureNode(this.hass, this._selectedNode!.attributes.ieee);
await reconfigureNode(this.hass, this._selectedNode!.ieee);
}
}
private _showNodeInformation(): void {
fireEvent(this, "hass-more-info", {
entityId: this._selectedNode!.entity_id,
});
}
private _computeNodeServiceData(): NodeServiceData {
return {
ieee_address: this._selectedNode!.attributes.ieee,
ieee_address: this._selectedNode!.ieee,
};
}
private _computeSelectCaption(stateObj: HassEntity): string {
return (
computeStateName(stateObj) + " (Node:" + stateObj.attributes.ieee + ")"
);
}
private _computeNodes(hass?: HomeAssistant): HassEntity[] {
if (hass) {
return Object.keys(hass.states)
.map((key) => hass.states[key])
.filter((ent) => ent.entity_id.match("zha[.]"))
.sort(sortByName);
} else {
return [];
}
}
private _onEntitySelected(
entitySelectedEvent: HASSDomEvent<ZHAEntitySelectedParams>
): void {
this._selectedEntity = entitySelectedEvent.detail.entity;
private async _fetchDevices() {
this._nodes = (await fetchDevices(this.hass!)).sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
static get styles(): CSSResult[] {
@ -287,6 +268,17 @@ export class ZHANode extends LitElement {
padding-bottom: 10px;
}
.card {
box-sizing: border-box;
display: flex;
flex: 1 0 300px;
min-width: 0;
max-width: 600px;
padding-left: 28px;
padding-right: 28px;
padding-bottom: 10px;
}
ha-service-description {
display: block;
color: grey;

View File

@ -2,8 +2,10 @@ import {
html,
LitElement,
PropertyValues,
PropertyDeclarations,
TemplateResult,
CSSResult,
css,
property,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
@ -50,20 +52,22 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
return { states: ["arm_home", "arm_away"] };
}
public hass?: HomeAssistant;
private _config?: Config;
private _code?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
_code: {},
};
}
@property() public hass?: HomeAssistant;
@property() private _config?: Config;
@property() private _code?: string;
public getCardSize(): number {
return 4;
if (!this._config || !this.hass) {
return 0;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return 0;
}
return stateObj.attributes.code_format !== FORMAT_NUMBER ? 3 : 8;
}
public setConfig(config: Config): void {
@ -114,7 +118,6 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
}
return html`
${this.renderStyle()}
<ha-card .header="${this._config.name || this._label(stateObj.state)}">
<ha-label-badge
class="${classMap({ [stateObj.state]: true })}"
@ -204,9 +207,9 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
this._code = "";
}
private renderStyle(): TemplateResult {
return html`
<style>
static get styles(): CSSResult[] {
return [
css`
ha-card {
padding-bottom: 16px;
position: relative;
@ -293,8 +296,8 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
paper-button#disarm {
color: var(--google-red-500);
}
</style>
`;
`,
];
}
}

View File

@ -2,8 +2,8 @@ import {
html,
LitElement,
PropertyValues,
PropertyDeclarations,
TemplateResult,
property,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
@ -53,20 +53,11 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
@property() public hass?: HomeAssistant;
@property() private _config?: Config;
@property() private _roundSliderStyle?: TemplateResult;
@property() private _jQuery?: any;
private _brightnessTimout?: number;
private _roundSliderStyle?: TemplateResult;
private _jQuery?: any;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
roundSliderStyle: {},
_jQuery: {},
};
}
public getCardSize(): number {
return 2;

View File

@ -1,4 +1,11 @@
import { html, LitElement, TemplateResult } from "lit-element";
import {
html,
LitElement,
TemplateResult,
css,
CSSResult,
property,
} from "lit-element";
import { repeat } from "lit-html/directives/repeat";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@polymer/paper-checkbox/paper-checkbox";
@ -26,25 +33,17 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
await import(/* webpackChunkName: "hui-shopping-list-editor" */ "../editor/config-elements/hui-shopping-list-editor");
return document.createElement("hui-shopping-list-card-editor");
}
public static getStubConfig(): object {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
private _uncheckedItems?: ShoppingListItem[];
private _checkedItems?: ShoppingListItem[];
@property() public hass?: HomeAssistant;
@property() private _config?: Config;
@property() private _uncheckedItems?: ShoppingListItem[];
@property() private _checkedItems?: ShoppingListItem[];
private _unsubEvents?: Promise<() => Promise<void>>;
static get properties() {
return {
hass: {},
_config: {},
_uncheckedItems: {},
_checkedItems: {},
};
}
public getCardSize(): number {
return (this._config ? (this._config.title ? 1 : 0) : 0) + 3;
}
@ -82,7 +81,6 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
}
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<div class="addRow">
<ha-icon
@ -180,9 +178,9 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
static get styles(): CSSResult[] {
return [
css`
.editRow,
.addRow {
display: flex;
@ -192,6 +190,9 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
padding: 9px 15px 11px 15px;
cursor: pointer;
}
paper-item-body {
width: 75%;
}
paper-checkbox {
padding: 11px 11px 11px 18px;
}
@ -230,8 +231,8 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
.addRow > ha-icon {
color: var(--secondary-text-color);
}
</style>
`;
`,
];
}
private async _fetchData(): Promise<void> {

View File

@ -46,8 +46,9 @@ const DOMAIN_TO_ELEMENT_TYPE = {
input_select: "input-select",
input_text: "input-text",
light: "toggle",
media_player: "media-player",
lock: "lock",
media_player: "media-player",
remote: "toggle",
scene: "scene",
script: "script",
sensor: "sensor",

View File

@ -98,7 +98,11 @@ export class HuiCardOptions extends LitElement {
slot="dropdown-trigger"
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @click="${this._moveCard}">Move Card</paper-item>
<paper-item @click="${this._moveCard}"
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.move"
)}</paper-item
>
<paper-item @click="${this._deleteCard}"
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.delete"

View File

@ -5,25 +5,20 @@ import "../../../components/ha-icon";
import computeStateName from "../../../common/entity/compute_state_name";
import {
LitElement,
PropertyDeclarations,
html,
css,
CSSResult,
PropertyValues,
property,
} from "lit-element";
import { HomeAssistant } from "../../../types";
import { EntitiesCardEntityConfig } from "../cards/hui-entities-card";
import { computeRTL } from "../../../common/util/compute_rtl";
class HuiGenericEntityRow extends LitElement {
public hass?: HomeAssistant;
public config?: EntitiesCardEntityConfig;
public showSecondary: boolean;
constructor() {
super();
this.showSecondary = true;
}
@property() public hass?: HomeAssistant;
@property() public config?: EntitiesCardEntityConfig;
@property() public showSecondary: boolean = true;
protected render() {
if (!this.hass || !this.config) {
@ -70,14 +65,6 @@ class HuiGenericEntityRow extends LitElement {
`;
}
static get properties(): PropertyDeclarations {
return {
hass: {},
config: {},
showSecondary: {},
};
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("hass")) {

View File

@ -1,199 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { STATES_OFF } from "../../../common/const";
import LocalizeMixin from "../../../mixins/localize-mixin";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
const UPDATE_INTERVAL = 10000;
const DEFAULT_FILTER = "grayscale(100%)";
/*
* @appliesMixin LocalizeMixin
*/
class HuiImage extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
${this.styleTemplate}
<div id="wrapper">
<img
id="image"
src="[[_imageSrc]]"
on-error="_onImageError"
on-load="_onImageLoad"
/>
<div id="brokenImage"></div>
</div>
`;
}
static get styleTemplate() {
return html`
<style>
img {
display: block;
height: auto;
transition: filter 0.2s linear;
width: 100%;
}
.error {
text-align: center;
}
.hidden {
display: none;
}
.ratio {
position: relative;
width: 100%;
height: 0;
}
.ratio img,
.ratio div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#brokenImage {
background: grey url("/static/images/image-broken.svg") center/36px
no-repeat;
}
</style>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "_hassChanged",
},
entity: String,
image: String,
stateImage: Object,
cameraImage: String,
aspectRatio: String,
filter: String,
stateFilter: Object,
_imageSrc: String,
};
}
static get observers() {
return ["_configChanged(image, stateImage, cameraImage, aspectRatio)"];
}
connectedCallback() {
super.connectedCallback();
if (this.cameraImage) {
this.timer = setInterval(
() => this._updateCameraImageSrc(),
UPDATE_INTERVAL
);
}
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timer);
}
_configChanged(image, stateImage, cameraImage, aspectRatio) {
const ratio = parseAspectRatio(aspectRatio);
if (ratio && ratio.w > 0 && ratio.h > 0) {
this.$.wrapper.style.paddingBottom = `${(
(100 * ratio.h) /
ratio.w
).toFixed(2)}%`;
this.$.wrapper.classList.add("ratio");
}
if (cameraImage) {
this._updateCameraImageSrc();
} else if (image && !stateImage) {
this._imageSrc = image;
}
}
_onImageError() {
this._imageSrc = null;
this.$.image.classList.add("hidden");
if (!this.$.wrapper.classList.contains("ratio")) {
this.$.brokenImage.style.setProperty(
"height",
`${this._lastImageHeight || "100"}px`
);
}
this.$.brokenImage.classList.remove("hidden");
}
_onImageLoad() {
this.$.image.classList.remove("hidden");
this.$.brokenImage.classList.add("hidden");
if (!this.$.wrapper.classList.contains("ratio")) {
this._lastImageHeight = this.$.image.offsetHeight;
}
}
_hassChanged(hass) {
if (this.cameraImage || !this.entity) {
return;
}
const stateObj = hass.states[this.entity];
const newState = !stateObj ? "unavailable" : stateObj.state;
if (newState === this._currentState) return;
this._currentState = newState;
this._updateStateImage();
this._updateStateFilter(stateObj);
}
_updateStateImage() {
if (!this.stateImage) {
this._imageFallback = true;
return;
}
const stateImg = this.stateImage[this._currentState];
this._imageSrc = stateImg || this.image;
this._imageFallback = !stateImg;
}
_updateStateFilter(stateObj) {
let filter;
if (!this.stateFilter) {
filter = this.filter;
} else {
filter = this.stateFilter[this._currentState] || this.filter;
}
const isOff = !stateObj || STATES_OFF.includes(stateObj.state);
this.$.image.style.filter =
filter || (isOff && this._imageFallback && DEFAULT_FILTER) || "";
}
async _updateCameraImageSrc() {
try {
const { content_type: contentType, content } = await this.hass.callWS({
type: "camera_thumbnail",
entity_id: this.cameraImage,
});
this._imageSrc = `data:${contentType};base64, ${content}`;
this._onImageLoad();
} catch (err) {
this._onImageError();
}
}
}
customElements.define("hui-image", HuiImage);

View File

@ -0,0 +1,226 @@
import "@polymer/paper-toggle-button/paper-toggle-button";
import { STATES_OFF } from "../../../common/const";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
import {
LitElement,
TemplateResult,
html,
property,
CSSResult,
css,
PropertyValues,
query,
} from "lit-element";
import { HomeAssistant } from "../../../types";
import { styleMap } from "lit-html/directives/style-map";
import { classMap } from "lit-html/directives/class-map";
import { b64toBlob } from "../../../common/file/b64-to-blob";
import { fetchThumbnail } from "../../../data/camera";
const UPDATE_INTERVAL = 10000;
const DEFAULT_FILTER = "grayscale(100%)";
export interface StateSpecificConfig {
[state: string]: string;
}
/*
* @appliesMixin LocalizeMixin
*/
class HuiImage extends LitElement {
@property() public hass?: HomeAssistant;
@property() public entity?: string;
@property() public image?: string;
@property() public stateImage?: StateSpecificConfig;
@property() public cameraImage?: string;
@property() public aspectRatio?: string;
@property() public filter?: string;
@property() public stateFilter?: StateSpecificConfig;
@property() private _loadError?: boolean;
@property() private _cameraImageSrc?: string;
@query("img") private _image!: HTMLImageElement;
private _lastImageHeight?: number;
private _cameraUpdater?: number;
private _attached?: boolean;
public connectedCallback() {
super.connectedCallback();
this._attached = true;
this._startUpdateCameraInterval();
}
public disconnectedCallback() {
super.disconnectedCallback();
this._attached = false;
this._stopUpdateCameraInterval();
}
protected render(): TemplateResult | void {
const ratio = this.aspectRatio ? parseAspectRatio(this.aspectRatio) : null;
const stateObj =
this.hass && this.entity ? this.hass.states[this.entity] : undefined;
const state = stateObj ? stateObj.state : "unavailable";
// Figure out image source to use
let imageSrc: string | undefined;
// Track if we are we using a fallback image, used for filter.
let imageFallback = !this.stateImage;
if (this.cameraImage) {
imageSrc = this._cameraImageSrc;
} else if (this.stateImage) {
const stateImage = this.stateImage[state];
if (stateImage) {
imageSrc = stateImage;
} else {
imageSrc = this.image;
imageFallback = true;
}
} else {
imageSrc = this.image;
}
// Figure out filter to use
let filter = this.filter || "";
if (this.stateFilter && this.stateFilter[state]) {
filter = this.stateFilter[state];
}
if (!filter && this.entity) {
const isOff = !stateObj || STATES_OFF.includes(state);
filter = isOff && imageFallback ? DEFAULT_FILTER : "";
}
return html`
<div
style=${styleMap({
paddingBottom:
ratio && ratio.w > 0 && ratio.h > 0
? `${((100 * ratio.h) / ratio.w).toFixed(2)}%`
: "",
})}
class=${classMap({
ratio: Boolean(ratio && ratio.w > 0 && ratio.h > 0),
})}
>
<img
id="image"
src=${imageSrc}
@error=${this._onImageError}
@load=${this._onImageLoad}
style=${styleMap({
filter,
display: this._loadError ? "none" : "block",
})}
/>
<div
id="brokenImage"
style=${styleMap({
height: `${this._lastImageHeight || "100"}px`,
display: this._loadError ? "block" : "none",
})}
></div>
</div>
`;
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("cameraImage")) {
this._updateCameraImageSrc();
this._startUpdateCameraInterval();
return;
}
}
private _startUpdateCameraInterval() {
this._stopUpdateCameraInterval();
if (this.cameraImage && this._attached) {
this._cameraUpdater = window.setInterval(
() => this._updateCameraImageSrc(),
UPDATE_INTERVAL
);
}
}
private _stopUpdateCameraInterval() {
if (this._cameraUpdater) {
clearInterval(this._cameraUpdater);
}
}
private _onImageError() {
this._loadError = true;
}
private async _onImageLoad() {
this._loadError = false;
await this.updateComplete;
this._lastImageHeight = this._image.offsetHeight;
}
private async _updateCameraImageSrc() {
if (!this.hass || !this.cameraImage) {
return;
}
if (this._cameraImageSrc) {
URL.revokeObjectURL(this._cameraImageSrc);
this._cameraImageSrc = undefined;
}
try {
const { content_type: contentType, content } = await fetchThumbnail(
this.hass,
this.cameraImage
);
this._cameraImageSrc = URL.createObjectURL(
b64toBlob(content, contentType)
);
this._onImageLoad();
} catch (err) {
this._onImageError();
}
}
static get styles(): CSSResult {
return css`
img {
display: block;
height: auto;
transition: filter 0.2s linear;
width: 100%;
}
.ratio {
position: relative;
width: 100%;
height: 0;
}
.ratio img,
.ratio div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#brokenImage {
background: grey url("/static/images/image-broken.svg") center/36px
no-repeat;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-image": HuiImage;
}
}
customElements.define("hui-image", HuiImage);

View File

@ -3,44 +3,67 @@ import CodeMirror from "codemirror";
import "codemirror/mode/yaml/yaml";
// @ts-ignore
import codeMirrorCSS from "codemirror/lib/codemirror.css";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeRTL } from "../../../common/util/compute_rtl";
declare global {
interface HASSDomEvents {
"yaml-changed": {
value: string;
};
"yaml-save": undefined;
}
}
export class HuiYamlEditor extends HTMLElement {
public _hass?: HomeAssistant;
public codemirror: CodeMirror;
private _value: string;
public constructor() {
super();
CodeMirror.commands.save = (cm: CodeMirror) => {
fireEvent(cm.getWrapperElement(), "yaml-save");
};
this._value = "";
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>
${codeMirrorCSS}
.CodeMirror {
height: var(--code-mirror-height, 300px);
direction: var(--code-mirror-direction, ltr);
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));;
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--primary-text-color));
}
${codeMirrorCSS}
.CodeMirror {
height: var(--code-mirror-height, 300px);
direction: var(--code-mirror-direction, ltr);
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));;
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--primary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
</style>`;
}
set hass(hass: HomeAssistant) {
this._hass = hass;
if (this._hass) {
this.setScrollBarDirection();
}
}
set value(value: string) {
if (this.codemirror) {
if (value !== this.codemirror.getValue()) {
@ -72,7 +95,12 @@ export class HuiYamlEditor extends HTMLElement {
cm.replaceSelection(spaces);
},
},
gutters:
this._hass && computeRTL(this._hass!)
? ["rtl-gutter", "CodeMirror-linenumbers"]
: [],
});
this.setScrollBarDirection();
this.codemirror.on("changes", () => this._onChange());
} else {
this.codemirror.refresh();
@ -82,6 +110,16 @@ export class HuiYamlEditor extends HTMLElement {
private _onChange(): void {
fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() });
}
private setScrollBarDirection() {
if (!this.codemirror) {
return;
}
this.codemirror
.getWrapperElement()
.classList.toggle("rtl", computeRTL(this._hass!));
}
}
declare global {

View File

@ -119,8 +119,10 @@ export class HuiEditCard extends LitElement {
? this._configElement
: html`
<hui-yaml-editor
.hass="${this.hass}"
.value="${this._configValue!.value}"
@yaml-changed="${this._handleYamlChanged}"
@yaml-save="${this._save}"
></hui-yaml-editor>
`}
</div>

View File

@ -57,7 +57,7 @@ export class HuiAlarmPanelCardEditor extends LitElement
return html``;
}
const states = ["arm_home", "arm_away", "arm_night", "arm_custom_bypass"];
const states = ["arm_home", "arm_away", "arm_night", "armed_custom_bypass"];
return html`
${configElementStyle} ${this.renderStyle()}

View File

@ -95,6 +95,7 @@ class LovelacePanel extends LitElement {
if (state === "yaml-editor") {
return html`
<hui-editor
.hass="${this.hass}"
.lovelace="${this.lovelace}"
.closeEditor="${this._closeEditor}"
></hui-editor>

View File

@ -18,6 +18,7 @@ import "./components/hui-yaml-editor";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { HuiYamlEditor } from "./components/hui-yaml-editor";
import { HomeAssistant } from "../../types";
const lovelaceStruct = struct.interface({
title: "string?",
@ -26,6 +27,7 @@ const lovelaceStruct = struct.interface({
});
class LovelaceFullConfigEditor extends LitElement {
public hass?: HomeAssistant;
public lovelace?: Lovelace;
public closeEditor?: () => void;
private _saving?: boolean;
@ -34,6 +36,7 @@ class LovelaceFullConfigEditor extends LitElement {
static get properties() {
return {
hass: {},
lovelace: {},
_saving: {},
_changed: {},
@ -61,7 +64,11 @@ class LovelaceFullConfigEditor extends LitElement {
</app-toolbar>
</app-header>
<div class="content">
<hui-yaml-editor @yaml-changed="${this._yamlChanged}">
<hui-yaml-editor
.hass="${this.hass}"
@yaml-changed="${this._yamlChanged}"
@yaml-save="${this._handleSave}"
>
</hui-yaml-editor>
</div>
</app-header-layout>
@ -99,7 +106,6 @@ class LovelaceFullConfigEditor extends LitElement {
.content {
height: calc(100vh - 68px);
direction: ltr;
}
hui-code-editor {

View File

@ -49,6 +49,7 @@ import { showEditLovelaceDialog } from "./editor/lovelace-editor/show-edit-lovel
import { Lovelace } from "./types";
import { afterNextRender } from "../../common/util/render-status";
import { haStyle } from "../../resources/ha-style";
import { computeRTL, computeRTLDirection } from "../../common/util/compute_rtl";
// CSS and JS should only be imported once. Modules and HTML are safe.
const CSS_CACHE = {};
@ -243,6 +244,7 @@ class HUIRoot extends LitElement {
scrollable
.selected="${this._curView}"
@iron-activate="${this._handleViewSelected}"
dir="${computeRTLDirection(this.hass!)}"
>
${this.lovelace!.config.views.map(
(view) => html`
@ -252,7 +254,9 @@ class HUIRoot extends LitElement {
<paper-icon-button
title="Move view left"
class="edit-icon view"
icon="hass:arrow-left"
icon="${computeRTL(this.hass!)
? "hass:arrow-right"
: "hass:arrow-left"}"
@click="${this._moveViewLeft}"
?disabled="${this._curView === 0}"
></paper-icon-button>
@ -277,7 +281,9 @@ class HUIRoot extends LitElement {
<paper-icon-button
title="Move view right"
class="edit-icon view"
icon="hass:arrow-right"
icon="${computeRTL(this.hass!)
? "hass:arrow-left"
: "hass:arrow-right"}"
@click="${this._moveViewRight}"
?disabled="${(this._curView! as number) +
1 ===

View File

@ -12,11 +12,12 @@ import { createCardElement } from "./common/create-card-element";
import { HomeAssistant } from "../../types";
import { LovelaceCard } from "./types";
import { LovelaceConfig } from "../../data/lovelace";
import computeDomain from "../../common/entity/compute_domain";
export class HuiUnusedEntities extends LitElement {
private _hass?: HomeAssistant;
private _config?: LovelaceConfig;
private _element?: LovelaceCard;
private _elements?: LovelaceCard[];
static get properties(): PropertyDeclarations {
return {
@ -27,16 +28,18 @@ export class HuiUnusedEntities extends LitElement {
set hass(hass: HomeAssistant) {
this._hass = hass;
if (!this._element) {
this._createElement();
if (!this._elements) {
this._createElements();
return;
}
this._element.hass = this._hass;
for (const element of this._elements) {
element.hass = this._hass;
}
}
public setConfig(config: LovelaceConfig): void {
this._config = config;
this._createElement();
this._createElements();
}
protected render(): TemplateResult | void {
@ -46,7 +49,7 @@ export class HuiUnusedEntities extends LitElement {
return html`
${this.renderStyle()}
<div id="root">${this._element}</div>
<div id="root">${this._elements}</div>
`;
}
@ -54,30 +57,47 @@ export class HuiUnusedEntities extends LitElement {
return html`
<style>
#root {
max-width: 600px;
margin: 0 auto;
padding: 8px 0;
padding: 4px;
display: flex;
flex-wrap: wrap;
}
hui-entities-card {
max-width: 400px;
padding: 4px;
flex: 1;
}
</style>
`;
}
private _createElement(): void {
if (this._hass) {
const entities = computeUnusedEntities(this._hass, this._config!).map(
(entity) => ({
entity,
secondary_info: "entity-id",
})
);
this._element = createCardElement({
type: "entities",
title: "Unused entities",
entities,
show_header_toggle: false,
});
this._element!.hass = this._hass;
private _createElements(): void {
if (!this._hass) {
return;
}
const domains: { [domain: string]: string[] } = {};
computeUnusedEntities(this._hass, this._config!).forEach((entity) => {
const domain = computeDomain(entity);
if (!(domain in domains)) {
domains[domain] = [];
}
domains[domain].push(entity);
});
this._elements = Object.keys(domains)
.sort()
.map((domain) => {
const el = createCardElement({
type: "entities",
title: this._hass!.localize(`domain.${domain}`) || domain,
entities: domains[domain].map((entity) => ({
entity,
secondary_info: "entity-id",
})),
show_header_toggle: false,
});
el.hass = this._hass;
return el;
});
}
}

View File

@ -530,7 +530,18 @@
"introduction": "Here it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.",
"area_registry": {
"caption": "Area Registry",
"description": "Overview of all areas in your home."
"description": "Overview of all areas in your home.",
"picker": {
"header": "Area Registry"
},
"no_areas": "Looks like you have no areas yet!",
"create_area": "CREATE AREA",
"editor": {
"default_name": "New Area",
"delete": "DELETE",
"update": "UPDATE",
"create": "CREATE"
}
},
"core": {
"caption": "General",
@ -565,7 +576,11 @@
},
"customize": {
"caption": "Customization",
"description": "Customize your entities"
"description": "Customize your entities",
"picker": {
"header": "Customization",
"introduction": "Tweak per-entity attributes. Added/edited customizations will take effect immediately. Removed customizations will take effect when the entity is updated."
}
},
"automation": {
"caption": "Automation",
@ -755,7 +770,27 @@
},
"entity_registry": {
"caption": "Entity Registry",
"description": "Overview of all known entities."
"description": "Overview of all known entities.",
"picker": {
"header": "Entity Registry",
"unavailable": "(unavailable)"
},
"editor": {
"unavailable": "This entity is not currently available.",
"default_name": "New Area",
"delete": "DELETE",
"update": "UPDATE"
}
},
"person": {
"caption": "Persons",
"description": "Manage the persons that Home Assistant tracks.",
"detail": {
"name": "Name",
"device_tracker_intro": "Select the devices that belong to this person.",
"device_tracker_picked": "Track Device",
"device_tracker_pick": "Pick device to track"
}
},
"integrations": {
"caption": "Integrations",
@ -774,7 +809,8 @@
"hub": "Connected via",
"firmware": "Firmware: {version}",
"device_unavailable": "device unavailable",
"entity_unavailable": "entity unavailable"
"entity_unavailable": "entity unavailable",
"no_area": "No Area"
}
},
"users": {
@ -847,7 +883,8 @@
"toggle_editor": "Toggle Editor",
"add": "Add Card",
"edit": "Edit",
"delete": "Delete"
"delete": "Delete",
"move": "Move"
},
"save_config": {
"header": "Take control of your Lovelace UI",
@ -956,6 +993,29 @@
"working": "Please wait",
"unknown_error": "Something went wrong",
"providers": {
"command_line": {
"step": {
"init": {
"data": {
"username": "[%key:ui::panel::page-authorize::form::providers::homeassistant::step::init::data::username%]",
"password": "[%key:ui::panel::page-authorize::form::providers::homeassistant::step::init::data::password%]"
}
},
"mfa": {
"data": {
"code": "[%key:ui::panel::page-authorize::form::providers::homeassistant::step::mfa::data::code%]"
},
"description": "[%key:ui::panel::page-authorize::form::providers::homeassistant::step::mfa::description%]"
}
},
"error": {
"invalid_auth": "[%key:ui::panel::page-authorize::form::providers::homeassistant::error::invalid_auth%]",
"invalid_code": "[%key:ui::panel::page-authorize::form::providers::homeassistant::error::invalid_code%]"
},
"abort": {
"login_expired": "[%key:ui::panel::page-authorize::form::providers::homeassistant::abort::login_expired%]"
}
},
"homeassistant": {
"step": {
"init": {

View File

@ -29,6 +29,13 @@ declare global {
getComputedStyleValue(element, propertyName);
};
}
// for fire event
interface HASSDomEvents {
"value-changed": {
value: unknown;
};
change: undefined;
}
}
export interface WebhookError {

View File

@ -28,7 +28,7 @@
"disarmed": "Desactivada",
"armed_home": "Activada, mode a casa",
"armed_away": "Activada, mode fora",
"armed_night": "Activada, mode nit",
"armed_night": "Activada, mode nocturn",
"pending": "Pendent",
"arming": "Activant",
"disarming": "Desactivant",
@ -301,7 +301,8 @@
"period": "Període"
},
"logbook": {
"showing_entries": "Mostrant entrades de"
"showing_entries": "Mostrant entrades de",
"period": "Període"
},
"mailbox": {
"empty": "No tens missatges",
@ -789,10 +790,16 @@
"para_sure": "Estàs segur que vols prendre el control de la interfície d'usuari?",
"cancel": "M'ho he repensat",
"save": "Prendre el control"
},
"menu": {
"raw_editor": "Editor de codi"
}
},
"menu": {
"configure_ui": "Configurar la interfície d'usuari"
"configure_ui": "Configurar la interfície d'usuari",
"unused_entities": "Entitats sense utilitzar",
"help": "Ajuda",
"refresh": "Actualitzar"
}
}
},
@ -864,7 +871,7 @@
"disarm": "Desactivar",
"arm_home": "Activar, a casa",
"arm_away": "Activar, fora",
"arm_night": "Activar, nit",
"arm_night": "Activar, nocturn",
"armed_custom_bypass": "Bypass personalitzat"
},
"automation": {
@ -1028,7 +1035,8 @@
"zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace"
"lovelace": "Lovelace",
"system_health": "Estat del sistema"
},
"attribute": {
"weather": {

View File

@ -427,6 +427,10 @@
"webhook": {
"label": "Webhook",
"webhook_id": "Webhook ID"
},
"geo_location": {
"label": "Geolokace",
"zone": "Zóna"
}
}
},
@ -556,6 +560,12 @@
"device_unavailable": "zařízení není k dispozici",
"entity_unavailable": "entita není k dispozici"
}
},
"zha": {
"caption": "ZHA"
},
"area_registry": {
"description": "Přehled všech oblastí ve vaší domácnosti."
}
},
"profile": {
@ -758,7 +768,9 @@
}
},
"menu": {
"configure_ui": "Konfigurovat UI"
"configure_ui": "Konfigurovat UI",
"help": "Pomoc",
"refresh": "Obnovit"
}
}
},
@ -928,6 +940,18 @@
"save": "Uložit",
"name": "Název",
"entity_id": "Entity ID"
},
"more_info_control": {
"script": {
"last_action": "Poslední akce"
},
"sun": {
"rising": "Vychází",
"setting": "Zapadá"
},
"updater": {
"title": "Pokyny pro aktualizaci"
}
}
},
"auth_store": {
@ -978,7 +1002,10 @@
"weblink": "Webový odkaz",
"zwave": "Z-Wave",
"vacuum": "Vysavač",
"zha": "ZHA"
"zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace"
},
"attribute": {
"weather": {

View File

@ -301,7 +301,8 @@
"period": "Periode"
},
"logbook": {
"showing_entries": "Viser emner for"
"showing_entries": "Viser emner for",
"period": "Periode"
},
"mailbox": {
"empty": "Du har ingen beskeder",
@ -420,13 +421,27 @@
"label": "Zone",
"entity": "Enhed med placering",
"zone": "Zone",
"event": "Begivenhed",
"event": "Begivenhed:",
"enter": "Ankom",
"leave": "Forlade"
},
"webhook": {
"label": "Webhook",
"webhook_id": "Webhook-ID"
},
"time_pattern": {
"label": "Tidsmønster",
"hours": "Timer",
"minutes": "Minutter",
"seconds": "Sekunder"
},
"geo_location": {
"label": "Geolokation",
"source": "Kilde",
"zone": "Zone",
"event": "Begivenhed",
"enter": "Ankommer",
"leave": "Forlad"
}
}
},
@ -437,7 +452,7 @@
"duplicate": "Kopier",
"delete": "Slet",
"delete_confirm": "Er du sikker på du vil slette?",
"unsupported_condition": "Ikke-understøttet betingelse: {betingelse}",
"unsupported_condition": "Ikke-understøttet betingelse: {condition}",
"type_select": "Betingelsestype",
"type": {
"state": {
@ -448,7 +463,7 @@
"label": "Numerisk stadie",
"above": "Over",
"below": "Under",
"value_template": "Værdi skabelon (ikke krævet)"
"value_template": "Værdi skabelon (valgfri)"
},
"sun": {
"label": "Sol",
@ -556,12 +571,27 @@
"device_unavailable": "enhed utilgængelig",
"entity_unavailable": "entitet utilgængelig"
}
},
"zha": {
"caption": "ZHA",
"description": "Zigbee Home Automation opsætning",
"services": {
"reconfigure": "Genkonfigurer ZHA-enhed (helbred enhed). Brug dette hvis du har problemer med enheden. Hvis den pågældende enhed er en batteridrevet enhed skal du sørge for at den er vågen og accepterer kommandoer når du bruger denne service."
}
},
"area_registry": {
"caption": "Område registrering",
"description": "Oversigt over alle områder i dit hjem."
},
"entity_registry": {
"caption": "Enheds registrering",
"description": "Oversigt over alle kendte enheder."
}
},
"profile": {
"push_notifications": {
"header": "Push notifikationer",
"description": "Send meddelelser til denne enhed",
"description": "Send meddelelser til denne enhed.",
"error_load_platform": "Konfigurer notify.html5",
"error_use_https": "Kræver SSL aktiveret til frontend.",
"push_notifications": "Push-meddelelser",
@ -724,6 +754,11 @@
"checked_items": "Markerede elementer",
"clear_items": "Ryd markerede elementer",
"add_item": "Tilføj element"
},
"empty_state": {
"title": "Velkommen hjem",
"no_devices": "Denne side giver dig mulighed for at styre dine enheder, men det ser ud til, at du endnu ikke har konfigureret enheder. Gå til integrationssiden for at komme i gang.",
"go_to_integrations_page": "Gå til integrationssiden."
}
},
"editor": {
@ -755,10 +790,16 @@
"para_sure": "Er du sikker på du ønsker at tage kontrol over din brugergrænseflade?",
"cancel": "Glem det",
"save": "tag kontrol"
},
"menu": {
"raw_editor": "Tekstbaseret redigering"
}
},
"menu": {
"configure_ui": "Konfigurer UI"
"configure_ui": "Konfigurer UI",
"unused_entities": "Ubrugte enheder",
"help": "Hjælp",
"refresh": "Opdater"
}
}
},
@ -928,6 +969,19 @@
"save": "Gem",
"name": "Navn",
"entity_id": "Enheds ID"
},
"more_info_control": {
"script": {
"last_action": "Senest udløst"
},
"sun": {
"elevation": "Elevation",
"rising": "Solopgang",
"setting": "Indstilling"
},
"updater": {
"title": "Opdateringsvejledning"
}
}
},
"auth_store": {
@ -977,7 +1031,12 @@
"updater": "Opdater",
"weblink": "Link",
"zwave": "Z-Wave",
"vacuum": "Støvsuger"
"vacuum": "Støvsuger",
"zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "System sundhed"
},
"attribute": {
"weather": {

View File

@ -301,7 +301,8 @@
"period": "Zeitraum"
},
"logbook": {
"showing_entries": "Zeige Einträge für"
"showing_entries": "Zeige Einträge für",
"period": "Zeitraum"
},
"mailbox": {
"empty": "Du hast keine Nachrichten",
@ -433,6 +434,14 @@
"hours": "Stunden",
"minutes": "Minuten",
"seconds": "Sekunden"
},
"geo_location": {
"label": "Geolokalisierung",
"source": "Quelle",
"zone": "Zone",
"event": "Ereignis:",
"enter": "Betreten",
"leave": "Verlassen"
}
}
},
@ -566,6 +575,14 @@
"zha": {
"caption": "ZHA",
"description": "Zigbee Home Automation Netzwerkmanagement"
},
"area_registry": {
"caption": "Gebietsregister",
"description": "Überblick über alle Bereiche in Deinem Haus."
},
"entity_registry": {
"caption": "Entitätsregister",
"description": "Überblick aller bekannten Elemente."
}
},
"profile": {
@ -734,6 +751,11 @@
"checked_items": "Markierte Artikel",
"clear_items": "Markierte Elemente löschen",
"add_item": "Artikel hinzufügen"
},
"empty_state": {
"title": "Willkommen zu Hause",
"no_devices": "Auf dieser Seite kannst du deine Geräte steuern, es sieht jedoch so aus, als hättest du noch keine eingerichtet. Gehe zur Integrationsseite, um damit zu beginnen.",
"go_to_integrations_page": "Gehe zur Integrationsseite."
}
},
"editor": {
@ -765,10 +787,16 @@
"para_sure": "Bist du dir sicher, dass du die Benutzeroberfläche selbst verwalten möchtest?",
"cancel": "Abbrechen",
"save": "Kontrolle übernehmen"
},
"menu": {
"raw_editor": "Raw-Konfigurationseditor"
}
},
"menu": {
"configure_ui": "Benutzeroberfläche konfigurieren"
"configure_ui": "Benutzeroberfläche konfigurieren",
"unused_entities": "Ungenutzte Elemente",
"help": "Hilfe",
"refresh": "Aktualisieren"
}
}
},
@ -945,8 +973,8 @@
},
"sun": {
"elevation": "Höhe",
"rising": "Aufgehend",
"setting": "Einstellung"
"rising": "Aufgang",
"setting": "Untergang"
},
"updater": {
"title": "Update-Anweisungen"
@ -1001,7 +1029,11 @@
"weblink": "Weblink",
"zwave": "Z-Wave",
"vacuum": "Staubsauger",
"zha": "ZHA"
"zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "Systemzustand"
},
"attribute": {
"weather": {

View File

@ -301,7 +301,8 @@
"period": "Περίοδος"
},
"logbook": {
"showing_entries": "Εμφανίζοντα καταχωρήσεις για"
"showing_entries": "Εμφανίζοντα καταχωρήσεις για",
"period": "Περίοδος"
},
"mailbox": {
"empty": "Δεν έχετε μηνύματα",
@ -433,6 +434,13 @@
"hours": "Ώρες",
"minutes": "Λεπτά",
"seconds": "Δευτερόλεπτα"
},
"geo_location": {
"label": "Γεωγραφική θέση",
"source": "Πηγή",
"zone": "Ζώνη",
"enter": "Είσοδος",
"leave": "Αποχώρηση"
}
}
},
@ -556,7 +564,7 @@
"no_device": "Οντότητες χωρίς συσκευές",
"delete_confirm": "Είστε σίγουρος ότι θέλετε να διαγραφεί αυτή η ενοποίηση;",
"restart_confirm": "Επανεκκινήστε το Home Assistant για να ολοκληρώσετε την κατάργηση αυτής της ενοποίησης",
"manuf": "από {κατασκευαστής}",
"manuf": "από {manufacturer}",
"hub": "Συνδεδεμένο μέσω",
"firmware": "Υλικολογισμικό: {έκδοση}",
"device_unavailable": "συσκευή μη διαθέσιμη",
@ -566,6 +574,14 @@
"zha": {
"caption": "ZHA",
"description": "Διαχείριση του δικτύου ZigBee Home Automation"
},
"area_registry": {
"caption": "Περιοχή Μητρώου",
"description": "Επισκόπηση όλων των περιοχών στο σπίτι σας."
},
"entity_registry": {
"caption": "Μητρώο οντοτήτων",
"description": "Επισκόπηση όλων των γνωστών οντοτήτων."
}
},
"profile": {
@ -734,6 +750,11 @@
"checked_items": "Επιλεγμένα στοιχεία",
"clear_items": "Εκκαθάριση επιλεγμένων στοιχείων",
"add_item": "Προσθήκη στοιχείου"
},
"empty_state": {
"title": "Καλωσορίσατε στην αρχική σελίδα",
"no_devices": "Αυτή η σελίδα σάς επιτρέπει να ελέγχετε τις συσκευές σας, ωστόσο φαίνεται ότι δεν έχετε ακόμα ρυθμίσει συσκευές. Μεταβείτε στη σελίδα ενοποίησης για να ξεκινήσετε.",
"go_to_integrations_page": "Μεταβείτε στη σελίδα ενοποίησης."
}
},
"editor": {
@ -768,7 +789,10 @@
}
},
"menu": {
"configure_ui": "Διαμορφώστε το περιβάλλον χρήστη"
"configure_ui": "Διαμορφώστε το περιβάλλον χρήστη",
"unused_entities": "Αχρησιμοποίητες οντότητες",
"help": "Βοήθεια",
"refresh": "Ανανέωση"
}
}
},
@ -1001,7 +1025,11 @@
"weblink": "Σύνδεσμος",
"zwave": "Z-Wave",
"vacuum": "Εκκένωση ",
"zha": "ΖΗΑ"
"zha": "ΖΗΑ",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "Υγεία Συστήματος"
},
"attribute": {
"weather": {

View File

@ -423,6 +423,17 @@
"event": "Evento:",
"enter": "Entrar",
"leave": "Salir"
},
"webhook": {
"label": "Webhook"
},
"time_pattern": {
"hours": "Horas",
"minutes": "Minutos",
"seconds": "Hass.io"
},
"geo_location": {
"event": "Evento:"
}
}
},
@ -552,6 +563,9 @@
"device_unavailable": "dispositivo no disponible",
"entity_unavailable": "entidad no disponible"
}
},
"zha": {
"caption": "ZHA"
}
},
"profile": {
@ -726,7 +740,8 @@
"edit_card": {
"header": "Configuración de la tarjeta",
"save": "Guardar",
"toggle_editor": "Cambiar editor"
"toggle_editor": "Cambiar editor",
"edit": "Editar"
},
"migrate": {
"header": "Configuración inválida",
@ -734,6 +749,9 @@
"para_migrate": "Home Assistant puede agregar ID a todas sus tarjetas y vistas automáticamente por usted presionando el botón 'Migrar configuración'.",
"migrate": "Migrar configuración"
}
},
"menu": {
"help": "Ayuda"
}
}
},
@ -951,7 +969,10 @@
"updater": "Actualizador",
"weblink": "Enlace web",
"zwave": "",
"vacuum": "Aspiradora"
"vacuum": "Aspiradora",
"zha": "ZHA",
"lovelace": "Lovelace",
"system_health": "Estado del sistema"
},
"attribute": {
"weather": {

View File

@ -301,7 +301,8 @@
"period": "Periodo"
},
"logbook": {
"showing_entries": "Mostrando entradas del"
"showing_entries": "Mostrando entradas del",
"period": "Periodo"
},
"mailbox": {
"empty": "No tiene ningún mensaje",
@ -433,6 +434,14 @@
"hours": "Horas",
"minutes": "Minutos",
"seconds": "Segundos"
},
"geo_location": {
"label": "Geolocalización",
"source": "Fuente",
"zone": "Zona",
"event": "Evento:",
"enter": "Entrar",
"leave": "Salir"
}
}
},
@ -565,7 +574,18 @@
},
"zha": {
"caption": "ZHA",
"description": "Gestión de red de Zigbee Home Automation"
"description": "Gestión de red de Zigbee Home Automation",
"services": {
"reconfigure": "Reconfigura el dispositivo ZHA (curar dispositivo). Usa esto si tienes problemas con el dispositivo. Si el dispositivo en cuestión es un dispositivo alimentado por batería, asegurate de que está activo y aceptando comandos cuando uses este servicio."
}
},
"area_registry": {
"caption": "Registro de área",
"description": "Visión general de todas las áreas de tu casa."
},
"entity_registry": {
"caption": "Registro de Entidades",
"description": "Resumen de todas las entidades conocidas."
}
},
"profile": {
@ -734,6 +754,11 @@
"checked_items": "Elementos marcados",
"clear_items": "Borrar elementos marcados",
"add_item": "Añadir artículo"
},
"empty_state": {
"title": "Bienvenido a casa",
"no_devices": "Esta página te permite controlar tus dispositivos, aunque parece que aún no has configurado ninguno. Dirígete a la página de integraciones para empezar.",
"go_to_integrations_page": "Ir a la página de integraciones."
}
},
"editor": {
@ -765,10 +790,16 @@
"para_sure": "¿Está seguro de que desea tomar el control de su interfaz de usuario?",
"cancel": "No importa",
"save": "Tomar el control"
},
"menu": {
"raw_editor": "Editor de configuración en bruto"
}
},
"menu": {
"configure_ui": "Configurar la interfaz de usuario"
"configure_ui": "Configurar la interfaz de usuario",
"unused_entities": "Entidades no utilizadas",
"help": "Ayuda",
"refresh": "Refrescar"
}
}
},
@ -1001,7 +1032,11 @@
"weblink": "Enlace web",
"zwave": "Z-Wave",
"vacuum": "Aspiradora",
"zha": "ZHA"
"zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "Salud del sistema"
},
"attribute": {
"weather": {

View File

@ -301,7 +301,8 @@
"period": "Période"
},
"logbook": {
"showing_entries": "Afficher les entrées pour le"
"showing_entries": "Afficher les entrées pour le",
"period": "Période"
},
"mailbox": {
"empty": "Vous n'avez aucun message",
@ -433,6 +434,14 @@
"hours": "Heures",
"minutes": "Minutes",
"seconds": "Secondes"
},
"geo_location": {
"label": "Géolocalisation",
"source": "Source",
"zone": "Zone",
"event": "Événement :",
"enter": "Entre",
"leave": "Quitte"
}
}
},
@ -562,6 +571,21 @@
"device_unavailable": "appareil indisponible",
"entity_unavailable": "entité indisponible"
}
},
"zha": {
"caption": "ZHA",
"description": "Gestion de réseau domotique ZigBee",
"services": {
"reconfigure": "Reconfigurer le périphérique ZHA. Utilisez cette option si vous rencontrez des problèmes avec le périphérique. Si l'appareil en question est un appareil alimenté par batterie, assurez-vous qu'il soit allumé et qu'il accepte les commandes lorsque vous utilisez ce service."
}
},
"area_registry": {
"caption": "Registre des pièces",
"description": "Vue d'ensemble de toutes les pièces de votre maison."
},
"entity_registry": {
"caption": "Registre des entités",
"description": "Vue d'ensemble de toutes les entités connues."
}
},
"profile": {
@ -730,6 +754,11 @@
"checked_items": "Éléments cochés",
"clear_items": "Effacer éléments cochés",
"add_item": "Ajouter un élément"
},
"empty_state": {
"title": "Bienvenue à la maison",
"no_devices": "Cette page vous permet de contrôler vos périphériques. Toutefois, il semble que vous nayez pas encore configuré de périphériques. Rendez-vous sur la page des intégrations pour commencer.",
"go_to_integrations_page": "Aller à la page des intégrations."
}
},
"editor": {
@ -761,10 +790,16 @@
"para_sure": "Êtes-vous sûr de vouloir prendre le controle de l'interface utilisateur?",
"cancel": "Oublie ce que j'ai dit, c'est pas grave.",
"save": "Prenez le contrôle"
},
"menu": {
"raw_editor": "Éditeur de configuration"
}
},
"menu": {
"configure_ui": "Configurer l'interface utilisateur"
"configure_ui": "Configurer l'interface utilisateur",
"unused_entities": "Entités inutilisées",
"help": "Aide",
"refresh": "Actualiser"
}
}
},
@ -941,7 +976,8 @@
},
"sun": {
"elevation": "Élévation",
"rising": "Lever"
"rising": "Lever",
"setting": "Coucher"
},
"updater": {
"title": "Instructions de mise à jour"
@ -995,7 +1031,12 @@
"updater": "Mise à jour",
"weblink": "Lien",
"zwave": "Z-Wave",
"vacuum": "Aspirateur"
"vacuum": "Aspirateur",
"zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "Santé du système"
},
"attribute": {
"weather": {

View File

@ -353,20 +353,20 @@
"description": "צור וערוך אוטומציות",
"picker": {
"header": "עורך אוטומציה",
"introduction": "עורך אוטומציה מאפשר לך ליצור ולערוך אוטומציה. אנא קרא את [ההוראות] (https:\/\/home-assistant.io\/docs\/automation\/editor\/) כדי לוודא שהגדרת את ה - Home Assistant כהלכה.",
"introduction": "עורך אוטומציה מאפשר לך ליצור ולערוך אוטומציות. אנא קרא את [ההוראות](https:\/\/home-assistant.io\/docs\/automation\/editor\/) כדי לוודא שהגדרת את ה - Home Assistant כהלכה.",
"pick_automation": "בחר אוטומציה לעריכה",
"no_automations": "לא הצלחנו למצוא שום אוטומציה הניתנת לעריכה",
"add_automation": "הוסף אוטומציה"
},
"editor": {
"introduction": "השתמש אוטומציות להביא את החיים לבית שלך",
"introduction": "השתמש באוטומציות להביא את הבית שלך לחיים",
"default_name": "אוטומציה חדשה",
"save": "שמור",
"unsaved_confirm": "יש לך שינויים שלא נשמרו. אתה בטוח שאתה רוצה לעזוב?",
"alias": "שם",
"triggers": {
"header": "טריגרים",
"introduction": "טריגרים הם מה שמתחיל כל אוטומציה. ניתן לציין מספר טריגרים עבור אותו כלל. לאחר הפעלת טריגר, ה - Assistant Assistant יאמת את התנאים, אם קיימים, ויקרא לפעולה. \n\n [למידע נוסף על גורמים טריגרים.) (Https:\/\/home-assistant.io\/docs\/automation\/trigger\/)",
"introduction": "טריגרים הם מה שמתחיל כל אוטומציה. ניתן לציין מספר טריגרים עבור אותו כלל. לאחר הפעלת טריגר, ה - Home Assistant יאמת את התנאים, אם קיימים, ויקרא לפעולה. \n\n[למד עוד על טריגרים](https:\/\/home-assistant.io\/docs\/automation\/trigger\/)",
"add": "הוספת טריגר",
"duplicate": "שכפל",
"delete": "מחק",
@ -447,7 +447,7 @@
},
"conditions": {
"header": "תנאים",
"introduction": "התנאים הם חלק אופציונלי של כלל אוטומציה, וניתן להשתמש בהם כדי למנוע פעולה כלשהי בעת הפעלתה. התנאים נראים דומים מאוד לטריגרים אך הם שונים מאוד. הטריגר יסתכל על האירועים המתרחשים במערכת בעוד תנאי רק מסתכל על איך המערכת נראית עכשיו. הטריגר יכול שמתג נדלק. תנאי יכול לראות רק אם מתג מופעל או כבוי. \n\n [למידע נוסף על תנאים.] (Https:\/\/home-assistant.io\/docs\/scripts\/conditions\/)",
"introduction": "התנאים הם חלק אופציונלי של כלל אוטומציה, וניתן להשתמש בהם כדי למנוע פעולה כלשהי בעת הפעלתה. התנאים נראים דומים מאוד לטריגרים אך הם שונים מאוד. הטריגר יסתכל על האירועים המתרחשים במערכת בעוד תנאי רק מסתכל על איך המערכת נראית עכשיו. הטריגר יכול שמתג נדלק. תנאי יכול לראות רק אם מתג מופעל או כבוי. \n\n[למידע נוסף על תנאים](Https:\/\/home-assistant.io\/docs\/scripts\/conditions\/)",
"add": "הוסף תנאי",
"duplicate": "שכפל",
"delete": "מחק",
@ -492,7 +492,7 @@
},
"actions": {
"header": "פעולות",
"introduction": "הפעולות הן מה שHome Assistant יעשה כאשר אוטומציה מופעלת. \n\n [למידע נוסף על פעולות.] (https:\/\/home-assistant.io\/docs\/automation\/action\/)",
"introduction": "הפעולות הן מה שHome Assistant יעשה כאשר אוטומציה מופעלת. \n\n[למידע נוסף על פעולות](https:\/\/home-assistant.io\/docs\/automation\/action\/)",
"add": "הוסף פעולה",
"duplicate": "שכפל",
"delete": "מחק",
@ -506,7 +506,7 @@
},
"delay": {
"label": "עיכוב",
"delay": "עיקוב"
"delay": "עיכוב"
},
"wait_template": {
"label": "לחכות",
@ -517,7 +517,7 @@
"label": "תנאי"
},
"event": {
"label": "אירוע אש",
"label": "ירה אירוע",
"event": "ארוע",
"service_data": "נתוני שירות"
}
@ -557,7 +557,7 @@
"description": "ניהול התקנים ושירותים מחוברים",
"discovered": "זוהו",
"configured": "הוגדר",
"new": "הגדר אינטגריצה",
"new": "הגדר אינטגרציה",
"configure": "הגדר",
"none": "כלום אינו הוגדר עדיין",
"config_entry": {

View File

@ -576,10 +576,12 @@
"description": "Zigbee Home Automation hálózat menedzsment"
},
"area_registry": {
"description": "Az összes otthoni terület áttekintése."
"caption": "Terület Nyilvántartás",
"description": "Az összes otthoni terület áttekintése"
},
"entity_registry": {
"description": "Az összes ismert entitás áttekintése."
"caption": "Entitás Nyilvántartás",
"description": "Az összes ismert entitás áttekintése"
}
},
"profile": {
@ -750,7 +752,9 @@
"add_item": "Tétel hozzáadása"
},
"empty_state": {
"title": "Üdv Itthon"
"title": "Üdv Itthon",
"no_devices": "Ez az oldal lehetővé teszi az eszközeid vezérlését, de úgy tűnik, hogy még nincs beállítva egy sem. A kezdéshez lépj át az integrációs oldalra.",
"go_to_integrations_page": "Ugrás az 'integrációk' oldalra."
}
},
"editor": {
@ -782,6 +786,9 @@
"para_sure": "Biztosan át szeretnéd venni az irányítást a felhasználói felületed felett?",
"cancel": "Mégsem",
"save": "Irányítás átvétele"
},
"menu": {
"raw_editor": "Konfiguráció szerkesztő"
}
},
"menu": {
@ -956,7 +963,7 @@
"dialogs": {
"more_info_settings": {
"save": "Mentés",
"name": "Név",
"name": "Név felülbírálása",
"entity_id": "Entitás ID"
},
"more_info_control": {
@ -1024,7 +1031,8 @@
"zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace"
"lovelace": "Lovelace",
"system_health": "Rendszer Állapot"
},
"attribute": {
"weather": {

View File

@ -301,7 +301,8 @@
"period": "Periodo"
},
"logbook": {
"showing_entries": "Mostra registrazioni per"
"showing_entries": "Mostra registrazioni per",
"period": "Periodo"
},
"mailbox": {
"empty": "Non hai nessun messaggio",
@ -426,13 +427,21 @@
},
"webhook": {
"label": "Webhook",
"webhook_id": "ID Webhook"
"webhook_id": "Webhook ID"
},
"time_pattern": {
"label": "Pattern temporale",
"hours": "Ore",
"minutes": "Minuti",
"seconds": "Secondi"
},
"geo_location": {
"label": "Geolocalizzazione",
"source": "Fonte",
"zone": "Zona",
"event": "Evento:",
"enter": "Ingresso",
"leave": "Uscita"
}
}
},
@ -544,7 +553,7 @@
"description_not_login": "Accesso non effettuato"
},
"integrations": {
"caption": "integrazioni",
"caption": "Integrazioni",
"description": "Gestisci dispositivi e servizi connessi",
"discovered": "Scoperto",
"configured": "Configurato",
@ -565,7 +574,18 @@
},
"zha": {
"caption": "ZHA",
"description": "Gestione rete Zigbee Home Automation"
"description": "Gestione rete Zigbee Home Automation",
"services": {
"reconfigure": "Riconfigurare il dispositivo ZHA (dispositivo di guarigione). Utilizzare questa opzione se si verificano problemi con il dispositivo. Se il dispositivo in questione è un dispositivo alimentato a batteria, assicurarsi che sia sveglio e che accetti i comandi quando si utilizza questo servizio."
}
},
"area_registry": {
"caption": "Registro di area",
"description": "Panoramica di tutte le aree della tua casa."
},
"entity_registry": {
"caption": "Registro delle entità",
"description": "Panoramica di tutte le entità conosciute."
}
},
"profile": {
@ -702,7 +722,7 @@
"data": {
"user": "Utente"
},
"description": "Perfavore, scegli l'utente con cui vuoi effettuare l'accesso:"
"description": "Per favore, scegli l'utente con cui vuoi effettuare l'accesso:"
}
},
"abort": {
@ -734,6 +754,11 @@
"checked_items": "Elementi selezionati",
"clear_items": "Cancella gli elementi selezionati",
"add_item": "Aggiungi elemento"
},
"empty_state": {
"title": "Benvenuto a casa",
"no_devices": "Questa pagina ti consente di controllare i tuoi dispositivi, tuttavia sembra che tu non abbia ancora configurato uno. Vai alla pagina delle integrazioni per iniziare.",
"go_to_integrations_page": "Vai alla pagina delle integrazioni."
}
},
"editor": {
@ -765,10 +790,16 @@
"para_sure": "Sei sicuro di voler prendere il controllo della tua interfaccia utente?",
"cancel": "Rinuncia",
"save": "Prendere il controllo"
},
"menu": {
"raw_editor": "Editor di configurazione grezzo"
}
},
"menu": {
"configure_ui": "Configurare l'interfaccia utente"
"configure_ui": "Configurare l'interfaccia utente",
"unused_entities": "Entità non utilizzate",
"help": "Aiuto",
"refresh": "Aggiorna"
}
}
},
@ -936,7 +967,7 @@
"dialogs": {
"more_info_settings": {
"save": "Salva",
"name": "Nome",
"name": "Sovrascrittura del nome",
"entity_id": "ID Entità"
},
"more_info_control": {
@ -1001,7 +1032,11 @@
"weblink": "Link Web",
"zwave": "Z-Wave",
"vacuum": "Aspirapolvere",
"zha": "ZHA"
"zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "Salute del sistema"
},
"attribute": {
"weather": {

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