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"); throw Error("latestBuild not defined for babel loader config");
} }
return { return {
test: /\.m?js$|\.ts$/, test: /\.m?js$|\.tsx?$/,
use: { use: {
loader: "babel-loader", loader: "babel-loader",
options: { options: {
@ -12,7 +12,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
require("@babel/preset-env").default, require("@babel/preset-env").default,
{ modules: false }, { modules: false },
], ],
require("@babel/preset-typescript").default, [
require("@babel/preset-typescript").default,
{
jsxPragma: "h",
},
],
].filter(Boolean), ].filter(Boolean),
plugins: [ plugins: [
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2}) // Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
@ -28,6 +33,14 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
pragma: "h", 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 // tslint:disable-next-line
import { HADemoCard } from "../custom-cards/ha-demo-card"; import { HADemoCard } from "../custom-cards/ha-demo-card";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { HUIView } from "../../../src/panels/lovelace/hui-view";
import { selectedDemoConfig } from "../configs/demo-configs"; import { selectedDemoConfig } from "../configs/demo-configs";
export const mockLovelace = (hass: MockHomeAssistant) => { export const mockLovelace = (hass: MockHomeAssistant) => {
@ -16,13 +15,17 @@ export const mockLovelace = (hass: MockHomeAssistant) => {
hass.mockWS("lovelace/config/save", () => Promise.resolve()); hass.mockWS("lovelace/config/save", () => Promise.resolve());
}; };
// Patch HUI-VIEW to make the lovelace object available to the demo card customElements.whenDefined("hui-view").then(() => {
const oldCreateCard = HUIView.prototype.createCardElement; // 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) { HUIView.prototype.createCardElement = function(config) {
const el = oldCreateCard.call(this, config); const el = oldCreateCard.call(this, config);
if (el.tagName === "HA-DEMO-CARD") { if (el.tagName === "HA-DEMO-CARD") {
(el as HADemoCard).lovelace = this.lovelace; (el as HADemoCard).lovelace = this.lovelace;
} }
return el; return el;
}; };
});

View File

@ -3,10 +3,12 @@ const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin"); const WorkboxPlugin = require("workbox-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js"); const { babelLoaderConfig } = require("../config/babel.js");
const { minimizer } = require("../config/babel.js"); const webpackBase = require("../config/webpack.js");
const isProd = process.env.NODE_ENV === "production"; 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 buildPath = path.resolve(__dirname, "dist");
const publicPath = "/"; const publicPath = "/";
@ -14,9 +16,7 @@ const latestBuild = false;
module.exports = { module.exports = {
mode: isProd ? "production" : "development", mode: isProd ? "production" : "development",
// Disabled in prod while we make Home Assistant able to serve the right files. devtool: isProd ? "cheap-source-map" : "inline-source-map",
// Was source-map
devtool: isProd ? "none" : "inline-source-map",
entry: { entry: {
main: "./src/entrypoint.ts", main: "./src/entrypoint.ts",
compatibility: "../src/entrypoints/compatibility.js", compatibility: "../src/entrypoints/compatibility.js",
@ -39,9 +39,7 @@ module.exports = {
}, },
], ],
}, },
optimization: { optimization: webpackBase.optimization,
minimizer,
},
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
__DEV__: false, __DEV__: false,
@ -71,23 +69,14 @@ module.exports = {
to: "static/images/leaflet/", to: "static/images/leaflet/",
}, },
]), ]),
...webpackBase.plugins,
isProd && isProd &&
new WorkboxPlugin.GenerateSW({ new WorkboxPlugin.GenerateSW({
swDest: "service_worker_es5.js", swDest: "service_worker_es5.js",
importWorkboxFrom: "local", importWorkboxFrom: "local",
}), }),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: webpackBase.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",
},
},
output: { output: {
filename: "[name].js", filename: "[name].js",
chunkFilename: chunkFilename, 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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/demo-cards"; 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 = [ const CONFIGS = [
{ {
@ -66,7 +79,7 @@ const CONFIGS = [
class DemoGaugeEntity extends PolymerElement { class DemoGaugeEntity extends PolymerElement {
static get template() { static get template() {
return html` 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); 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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/demo-cards"; 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 = [ const CONFIGS = [
{ {
@ -10,6 +21,8 @@ const CONFIGS = [
- type: picture-entity - type: picture-entity
image: /images/kitchen.png image: /images/kitchen.png
entity: light.kitchen_lights entity: light.kitchen_lights
tap_action:
action: toggle
`, `,
}, },
{ {
@ -18,6 +31,8 @@ const CONFIGS = [
- type: picture-entity - type: picture-entity
image: /images/bed.png image: /images/bed.png
entity: light.bed_light entity: light.bed_light
tap_action:
action: toggle
`, `,
}, },
{ {
@ -68,7 +83,7 @@ const CONFIGS = [
class DemoPicEntity extends PolymerElement { class DemoPicEntity extends PolymerElement {
static get template() { static get template() {
return html` 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); 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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/demo-cards"; 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 = [ const CONFIGS = [
{ {
@ -105,7 +124,7 @@ const CONFIGS = [
class DemoPicGlance extends PolymerElement { class DemoPicGlance extends PolymerElement {
static get template() { static get template() {
return html` 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); customElements.define("demo-hui-picture-glance-card", DemoPicGlance);

View File

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

View File

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

View File

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

View File

@ -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, html,
CSSResult, CSSResult,
css, css,
PropertyDeclarations, property,
} from "lit-element"; } from "lit-element";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
@ -17,7 +17,7 @@ import { HassEntity } from "home-assistant-js-websocket";
class HaEntityToggle extends LitElement { class HaEntityToggle extends LitElement {
// hass is not a property so that we only re-render on stateObj changes // hass is not a property so that we only re-render on stateObj changes
public hass?: HomeAssistant; public hass?: HomeAssistant;
public stateObj?: HassEntity; @property() public stateObj?: HassEntity;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (!this.stateObj) { if (!this.stateObj) {
@ -51,12 +51,6 @@ class HaEntityToggle extends LitElement {
`; `;
} }
static get properties(): PropertyDeclarations {
return {
stateObj: {},
};
}
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.addEventListener("click", (ev) => ev.stopPropagation()); 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 { :host {
@apply --paper-material-elevation-1; @apply --paper-material-elevation-1;
display: block; display: block;
border-radius: 2px; border-radius: var(--ha-card-border-radius, 2px);
transition: all 0.3s ease-out; 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); color: var(--primary-text-color);
} }
.header { .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, html,
CSSResult, CSSResult,
css, css,
PropertyDeclarations,
PropertyValues, PropertyValues,
property,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
@ -82,13 +82,9 @@ const computePanels = (hass: HomeAssistant) => {
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
class HaSidebar extends LitElement { class HaSidebar extends LitElement {
public hass?: HomeAssistant; @property() public hass?: HomeAssistant;
public _defaultPage?: string; @property() public _defaultPage?: string =
localStorage.defaultPage || DEFAULT_PANEL;
constructor() {
super();
this._defaultPage = localStorage.defaultPage || DEFAULT_PANEL;
}
protected render() { protected render() {
const hass = this.hass; const hass = this.hass;
@ -217,13 +213,6 @@ class HaSidebar extends LitElement {
`; `;
} }
static get properties(): PropertyDeclarations {
return {
hass: {},
_defaultPage: {},
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
if (!this.hass || !changedProps.has("hass")) { if (!this.hass || !changedProps.has("hass")) {
return false; 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 { export interface ZHAEntityReference extends HassEntity {
[key: string]: 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 { export interface Attribute {
@ -19,6 +30,7 @@ export interface Attribute {
export interface Cluster { export interface Cluster {
name: string; name: string;
id: number; id: number;
endpoint_id: number;
type: string; type: string;
} }
@ -29,7 +41,8 @@ export interface Command {
} }
export interface ReadAttributeServiceData { export interface ReadAttributeServiceData {
entity_id: string; ieee: string;
endpoint_id: number;
cluster_id: number; cluster_id: number;
cluster_type: string; cluster_type: string;
attribute: number; attribute: number;
@ -41,64 +54,60 @@ export const reconfigureNode = (
ieeeAddress: string ieeeAddress: string
): Promise<void> => ): Promise<void> =>
hass.callWS({ hass.callWS({
type: "zha/nodes/reconfigure", type: "zha/devices/reconfigure",
ieee: ieeeAddress, ieee: ieeeAddress,
}); });
export const fetchAttributesForCluster = ( export const fetchAttributesForCluster = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string,
ieeeAddress: string, ieeeAddress: string,
endpointId: number,
clusterId: number, clusterId: number,
clusterType: string clusterType: string
): Promise<Attribute[]> => ): Promise<Attribute[]> =>
hass.callWS({ hass.callWS({
type: "zha/entities/clusters/attributes", type: "zha/devices/clusters/attributes",
entity_id: entityId,
ieee: ieeeAddress, ieee: ieeeAddress,
endpoint_id: endpointId,
cluster_id: clusterId, cluster_id: clusterId,
cluster_type: clusterType, cluster_type: clusterType,
}); });
export const fetchDevices = (hass: HomeAssistant): Promise<ZHADevice[]> =>
hass.callWS({
type: "zha/devices",
});
export const readAttributeValue = ( export const readAttributeValue = (
hass: HomeAssistant, hass: HomeAssistant,
data: ReadAttributeServiceData data: ReadAttributeServiceData
): Promise<string> => { ): Promise<string> => {
return hass.callWS({ return hass.callWS({
...data, ...data,
type: "zha/entities/clusters/attributes/value", type: "zha/devices/clusters/attributes/value",
}); });
}; };
export const fetchCommandsForCluster = ( export const fetchCommandsForCluster = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string,
ieeeAddress: string, ieeeAddress: string,
endpointId: number,
clusterId: number, clusterId: number,
clusterType: string clusterType: string
): Promise<Command[]> => ): Promise<Command[]> =>
hass.callWS({ hass.callWS({
type: "zha/entities/clusters/commands", type: "zha/devices/clusters/commands",
entity_id: entityId,
ieee: ieeeAddress, ieee: ieeeAddress,
endpoint_id: endpointId,
cluster_id: clusterId, cluster_id: clusterId,
cluster_type: clusterType, cluster_type: clusterType,
}); });
export const fetchClustersForZhaNode = ( export const fetchClustersForZhaNode = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string,
ieeeAddress: string ieeeAddress: string
): Promise<Cluster[]> => ): Promise<Cluster[]> =>
hass.callWS({ hass.callWS({
type: "zha/entities/clusters", type: "zha/devices/clusters",
entity_id: entityId,
ieee: ieeeAddress, 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; right: 16px;
} }
.card-content {
color: var(--primary-text-color);
}
.card-actions { .card-actions {
text-align: right; text-align: right;
border-top: 0; 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-alarm_control_panel";
import "./more-info-automation"; import "./more-info-automation";
@ -23,26 +28,30 @@ import "./more-info-weather";
import stateMoreInfoType from "../../../common/entity/state_more_info_type"; import stateMoreInfoType from "../../../common/entity/state_more_info_type";
import dynamicContentUpdater from "../../../common/dom/dynamic_content_updater"; import dynamicContentUpdater from "../../../common/dom/dynamic_content_updater";
import { HomeAssistant } from "../../../types";
class MoreInfoContent extends PolymerElement { class MoreInfoContent extends UpdatingElement {
static get properties() { public hass?: HomeAssistant;
public stateObj?: HassEntity;
private _detachedChild?: ChildNode;
static get properties(): PropertyDeclarations {
return { return {
hass: Object, hass: {},
stateObj: Object, stateObj: {},
}; };
} }
static get observers() { protected firstUpdated(): void {
return ["stateObjChanged(stateObj, hass)"];
}
constructor() {
super();
this.style.display = "block"; this.style.display = "block";
} }
stateObjChanged(stateObj, hass) { // This is not a lit element, but an updating element, so we implement update
let moreInfoType; protected update(changedProps: PropertyValues): void {
super.update(changedProps);
const stateObj = this.stateObj;
const hass = this.hass;
if (!stateObj || !hass) { if (!stateObj || !hass) {
if (this.lastChild) { if (this.lastChild) {
this._detachedChild = this.lastChild; this._detachedChild = this.lastChild;
@ -51,18 +60,20 @@ class MoreInfoContent extends PolymerElement {
} }
return; return;
} }
if (this._detachedChild) { if (this._detachedChild) {
this.appendChild(this._detachedChild); this.appendChild(this._detachedChild);
this._detachedChild = null; this._detachedChild = undefined;
}
if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
moreInfoType = stateObj.attributes.custom_ui_more_info;
} else {
moreInfoType = "more-info-" + stateMoreInfoType(stateObj);
} }
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(), { dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
hass: hass, hass,
stateObj: stateObj, stateObj,
}); });
} }
} }

View File

@ -1,13 +1,14 @@
import { Constructor, LitElement } from "lit-element"; import { Constructor, LitElement } from "lit-element";
import { HassBaseEl } from "./hass-base-mixin"; import { HassBaseEl } from "./hass-base-mixin";
import { HaToast } from "../../components/ha-toast"; import { HaToast } from "../../components/ha-toast";
import { computeRTL } from "../../common/util/compute_rtl";
export default (superClass: Constructor<LitElement & HassBaseEl>) => export default (superClass: Constructor<LitElement & HassBaseEl>) =>
class extends superClass { class extends superClass {
private _discToast?: HaToast; private _discToast?: HaToast;
protected hassConnected() { protected firstUpdated(changedProps) {
super.hassConnected(); super.firstUpdated(changedProps);
// Need to load in advance because when disconnected, can't dynamically load code. // Need to load in advance because when disconnected, can't dynamically load code.
import(/* webpackChunkName: "ha-toast" */ "../../components/ha-toast"); import(/* webpackChunkName: "ha-toast" */ "../../components/ha-toast");
} }
@ -24,10 +25,13 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
if (!this._discToast) { if (!this._discToast) {
const el = document.createElement("ha-toast"); const el = document.createElement("ha-toast");
el.duration = 0; el.duration = 0;
el.text = this.hass!.localize("ui.notification_toast.connection_lost");
this._discToast = el; this._discToast = el;
this.shadowRoot!.appendChild(el as any); 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; 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"; import { HomeAssistant } from "../../types";
/* tslint:disable */ /* tslint:disable */
@ -17,14 +21,9 @@ export class HassBaseEl {
export default <T>(superClass: Constructor<T>): Constructor<T & HassBaseEl> => export default <T>(superClass: Constructor<T>): Constructor<T & HassBaseEl> =>
// @ts-ignore // @ts-ignore
class extends superClass { class extends superClass {
private __provideHass: HTMLElement[]; private __provideHass: HTMLElement[] = [];
// @ts-ignore // @ts-ignore
protected hass: HomeAssistant; @property() protected hass: HomeAssistant;
constructor() {
super();
this.__provideHass = [];
}
// Exists so all methods can safely call super method // Exists so all methods can safely call super method
protected hassConnected() { protected hassConnected() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ export interface AreaRegistryDetailDialogParams {
} }
export const loadAreaRegistryDetailDialog = () => 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 = ( export const showAreaRegistryDetailDialog = (
element: HTMLElement, 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 { .content {
padding-bottom: 24px; padding-bottom: 24px;
direction: ltr;
} }
paper-card { paper-card {
display: block; display: block;

View File

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

View File

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

View File

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

View File

@ -81,7 +81,9 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
slot="dropdown-content" slot="dropdown-content"
selected="[[_computeSelectedArea(areas, device)]]" 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]]"> <template is="dom-repeat" items="[[areas]]">
<paper-item area="[[item]]">[[item.name]]</paper-item> <paper-item area="[[item]]">[[item.name]]</paper-item>
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,19 +9,6 @@ import isComponentLoaded from "../../common/config/is_component_loaded";
import EventsMixin from "../../mixins/events-mixin"; import EventsMixin from "../../mixins/events-mixin";
import NavigateMixin from "../../mixins/navigate-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 EventsMixin
* @appliesMixin NavigateMixin * @appliesMixin NavigateMixin
@ -136,6 +123,15 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
></ha-config-zwave> ></ha-config-zwave>
</template> </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 <template
is="dom-if" is="dom-if"
if='[[_equals(_routeData.page, "customize")]]' if='[[_equals(_routeData.page, "customize")]]'
@ -207,6 +203,19 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
this.addEventListener("ha-refresh-cloud-status", () => this.addEventListener("ha-refresh-cloud-status", () =>
this._updateCloudStatus() 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() { async _updateCloudStatus() {

View File

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

View File

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

View File

@ -53,6 +53,7 @@ export default class JSONTextArea extends Component {
value={value} value={value}
style={style} style={style}
onvalue-changed={this.onChange} 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 JSONTextArea from "../json_textarea";
import { onChangeEvent } from "../../../../common/preact/event"; 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() { constructor() {
super(); super();
@ -12,16 +30,11 @@ export default class EventAction extends Component {
this.serviceDataChanged = this.serviceDataChanged.bind(this); this.serviceDataChanged = this.serviceDataChanged.bind(this);
} }
serviceDataChanged(data) { public render() {
this.props.onChange( const {
this.props.index, action: { event, event_data },
Object.assign({}, this.props.action, { data }) localize,
); } = this.props;
}
render({ action, localize }) {
/* eslint-disable camelcase */
const { event, event_data } = action;
return ( return (
<div> <div>
<paper-input <paper-input
@ -42,9 +55,11 @@ export default class EventAction extends Component {
</div> </div>
); );
} }
}
EventAction.defaultConfig = { private serviceDataChanged(eventData) {
event: "", this.props.onChange(this.props.index, {
event_data: {}, ...this.props.action,
}; event_data: eventData,
});
}
}

View File

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

View File

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

View File

@ -23,6 +23,7 @@ export default class TemplateTrigger extends Component {
name="value_template" name="value_template"
value={value_template} value={value_template}
onvalue-changed={this.onChange} onvalue-changed={this.onChange}
dir="ltr"
/> />
</div> </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 { paper-card:last-child {
margin-bottom: 16px; margin-bottom: 16px;
} }
hass-subpage paper-card:first-of-type {
direction: ltr;
}
</style> </style>
<hass-subpage header="View user"> <hass-subpage header="View user">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import {
PropertyDeclarations, PropertyDeclarations,
TemplateResult, TemplateResult,
CSSResult, CSSResult,
PropertyValues,
css, css,
} from "lit-element"; } from "lit-element";
import "@polymer/paper-button/paper-button"; 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-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { HassEntity } from "home-assistant-js-websocket"; import { fireEvent } from "../../../common/dom/fire_event";
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 "../../../components/buttons/ha-call-service-button"; import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description"; import "../../../components/ha-service-description";
import { haStyle } from "../../../resources/ha-style"; import { haStyle } from "../../../resources/ha-style";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../ha-config-section"; import "../ha-config-section";
import { import { ItemSelectedEvent, NodeServiceData } from "./types";
ItemSelectedEvent,
NodeServiceData,
ZHAEntitySelectedParams,
} from "./types";
import "./zha-clusters"; import "./zha-clusters";
import "./zha-entities"; import "./zha-device-card";
import { reconfigureNode } from "../../../data/zha"; import { reconfigureNode, fetchDevices, ZHADevice } from "../../../data/zha";
declare global { declare global {
// for fire event // for fire event
interface HASSDomEvents { interface HASSDomEvents {
"zha-node-selected": { "zha-node-selected": {
node?: HassEntity; node?: ZHADevice;
}; };
} }
} }
@ -43,10 +37,9 @@ export class ZHANode extends LitElement {
public isWide?: boolean; public isWide?: boolean;
private _showHelp: boolean; private _showHelp: boolean;
private _selectedNodeIndex: number; private _selectedNodeIndex: number;
private _selectedNode?: HassEntity; private _selectedNode?: ZHADevice;
private _selectedEntity?: HassEntity;
private _serviceData?: {}; private _serviceData?: {};
private _nodes: HassEntity[]; private _nodes: ZHADevice[];
constructor() { constructor() {
super(); super();
@ -62,13 +55,31 @@ export class ZHANode extends LitElement {
_showHelp: {}, _showHelp: {},
_selectedNodeIndex: {}, _selectedNodeIndex: {},
_selectedNode: {}, _selectedNode: {},
_entities: {},
_serviceData: {}, _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 { protected render(): TemplateResult | void {
this._nodes = this._computeNodes(this.hass);
return html` return html`
<ha-config-section .isWide="${this.isWide}"> <ha-config-section .isWide="${this.isWide}">
<div class="sectionHeader" slot="header"> <div class="sectionHeader" slot="header">
@ -94,12 +105,11 @@ export class ZHANode extends LitElement {
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
@iron-select="${this._selectedNodeChanged}" @iron-select="${this._selectedNodeChanged}"
.selected="${this._selectedNodeIndex}"
> >
${this._nodes.map( ${this._nodes.map(
(entry) => html` (entry) => html`
<paper-item <paper-item>${entry.name}</paper-item>
>${this._computeSelectCaption(entry)}</paper-item
>
` `
)} )}
</paper-listbox> </paper-listbox>
@ -112,9 +122,18 @@ export class ZHANode extends LitElement {
</div> </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._renderNodeActions() : ""}
${this._selectedNodeIndex !== -1 ? this._renderEntities() : ""} ${this._selectedNode ? this._renderClusters() : ""}
${this._selectedEntity ? this._renderClusters() : ""}
</paper-card> </paper-card>
</ha-config-section> </ha-config-section>
`; `;
@ -123,9 +142,6 @@ export class ZHANode extends LitElement {
private _renderNodeActions(): TemplateResult { private _renderNodeActions(): TemplateResult {
return html` return html`
<div class="card-actions"> <div class="card-actions">
<paper-button @click="${this._showNodeInformation}"
>Node Information</paper-button
>
<paper-button @click="${this._onReconfigureNodeClick}" <paper-button @click="${this._onReconfigureNodeClick}"
>Reconfigure Node</paper-button >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 { private _renderClusters(): TemplateResult {
return html` return html`
<zha-clusters <zha-clusters
.hass="${this.hass}" .hass="${this.hass}"
.selectedEntity="${this._selectedEntity}" .selectedDevice="${this._selectedNode}"
.showHelp="${this._showHelp}" .showHelp="${this._showHelp}"
></zha-clusters> ></zha-clusters>
`; `;
@ -186,50 +191,26 @@ export class ZHANode extends LitElement {
private _selectedNodeChanged(event: ItemSelectedEvent): void { private _selectedNodeChanged(event: ItemSelectedEvent): void {
this._selectedNodeIndex = event!.target!.selected; this._selectedNodeIndex = event!.target!.selected;
this._selectedNode = this._nodes[this._selectedNodeIndex]; this._selectedNode = this._nodes[this._selectedNodeIndex];
this._selectedEntity = undefined;
fireEvent(this, "zha-node-selected", { node: this._selectedNode }); fireEvent(this, "zha-node-selected", { node: this._selectedNode });
this._serviceData = this._computeNodeServiceData(); this._serviceData = this._computeNodeServiceData();
} }
private async _onReconfigureNodeClick(): Promise<void> { private async _onReconfigureNodeClick(): Promise<void> {
if (this.hass) { 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 { private _computeNodeServiceData(): NodeServiceData {
return { return {
ieee_address: this._selectedNode!.attributes.ieee, ieee_address: this._selectedNode!.ieee,
}; };
} }
private _computeSelectCaption(stateObj: HassEntity): string { private async _fetchDevices() {
return ( this._nodes = (await fetchDevices(this.hass!)).sort((a, b) => {
computeStateName(stateObj) + " (Node:" + stateObj.attributes.ieee + ")" return a.name.localeCompare(b.name);
); });
}
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;
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -287,6 +268,17 @@ export class ZHANode extends LitElement {
padding-bottom: 10px; 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 { ha-service-description {
display: block; display: block;
color: grey; color: grey;

View File

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

View File

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

View File

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

View File

@ -98,7 +98,11 @@ export class HuiCardOptions extends LitElement {
slot="dropdown-trigger" slot="dropdown-trigger"
></paper-icon-button> ></paper-icon-button>
<paper-listbox slot="dropdown-content"> <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}" <paper-item @click="${this._deleteCard}"
>${this.hass!.localize( >${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.delete" "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 computeStateName from "../../../common/entity/compute_state_name";
import { import {
LitElement, LitElement,
PropertyDeclarations,
html, html,
css, css,
CSSResult, CSSResult,
PropertyValues, PropertyValues,
property,
} from "lit-element"; } from "lit-element";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { EntitiesCardEntityConfig } from "../cards/hui-entities-card"; import { EntitiesCardEntityConfig } from "../cards/hui-entities-card";
import { computeRTL } from "../../../common/util/compute_rtl"; import { computeRTL } from "../../../common/util/compute_rtl";
class HuiGenericEntityRow extends LitElement { class HuiGenericEntityRow extends LitElement {
public hass?: HomeAssistant; @property() public hass?: HomeAssistant;
public config?: EntitiesCardEntityConfig; @property() public config?: EntitiesCardEntityConfig;
public showSecondary: boolean; @property() public showSecondary: boolean = true;
constructor() {
super();
this.showSecondary = true;
}
protected render() { protected render() {
if (!this.hass || !this.config) { 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) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("hass")) { 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"; import "codemirror/mode/yaml/yaml";
// @ts-ignore // @ts-ignore
import codeMirrorCSS from "codemirror/lib/codemirror.css"; import codeMirrorCSS from "codemirror/lib/codemirror.css";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeRTL } from "../../../common/util/compute_rtl";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
"yaml-changed": { "yaml-changed": {
value: string; value: string;
}; };
"yaml-save": undefined;
} }
} }
export class HuiYamlEditor extends HTMLElement { export class HuiYamlEditor extends HTMLElement {
public _hass?: HomeAssistant;
public codemirror: CodeMirror; public codemirror: CodeMirror;
private _value: string; private _value: string;
public constructor() { public constructor() {
super(); super();
CodeMirror.commands.save = (cm: CodeMirror) => {
fireEvent(cm.getWrapperElement(), "yaml-save");
};
this._value = ""; this._value = "";
const shadowRoot = this.attachShadow({ mode: "open" }); const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = ` shadowRoot.innerHTML = `
<style> <style>
${codeMirrorCSS} ${codeMirrorCSS}
.CodeMirror { .CodeMirror {
height: var(--code-mirror-height, 300px); height: var(--code-mirror-height, 300px);
direction: var(--code-mirror-direction, ltr); direction: var(--code-mirror-direction, ltr);
} }
.CodeMirror-gutters { .CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color)); border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color)); background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right; transition: 0.2s ease border-right;
} }
.CodeMirror-focused .CodeMirror-gutters { .CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));; border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));;
} }
.CodeMirror-linenumber { .CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--primary-text-color)); color: var(--paper-dialog-color, var(--primary-text-color));
} }
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
</style>`; </style>`;
} }
set hass(hass: HomeAssistant) {
this._hass = hass;
if (this._hass) {
this.setScrollBarDirection();
}
}
set value(value: string) { set value(value: string) {
if (this.codemirror) { if (this.codemirror) {
if (value !== this.codemirror.getValue()) { if (value !== this.codemirror.getValue()) {
@ -72,7 +95,12 @@ export class HuiYamlEditor extends HTMLElement {
cm.replaceSelection(spaces); cm.replaceSelection(spaces);
}, },
}, },
gutters:
this._hass && computeRTL(this._hass!)
? ["rtl-gutter", "CodeMirror-linenumbers"]
: [],
}); });
this.setScrollBarDirection();
this.codemirror.on("changes", () => this._onChange()); this.codemirror.on("changes", () => this._onChange());
} else { } else {
this.codemirror.refresh(); this.codemirror.refresh();
@ -82,6 +110,16 @@ export class HuiYamlEditor extends HTMLElement {
private _onChange(): void { private _onChange(): void {
fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() }); 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 { declare global {

View File

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

View File

@ -57,7 +57,7 @@ export class HuiAlarmPanelCardEditor extends LitElement
return html``; 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` return html`
${configElementStyle} ${this.renderStyle()} ${configElementStyle} ${this.renderStyle()}

View File

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

View File

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

View File

@ -12,11 +12,12 @@ import { createCardElement } from "./common/create-card-element";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { LovelaceCard } from "./types"; import { LovelaceCard } from "./types";
import { LovelaceConfig } from "../../data/lovelace"; import { LovelaceConfig } from "../../data/lovelace";
import computeDomain from "../../common/entity/compute_domain";
export class HuiUnusedEntities extends LitElement { export class HuiUnusedEntities extends LitElement {
private _hass?: HomeAssistant; private _hass?: HomeAssistant;
private _config?: LovelaceConfig; private _config?: LovelaceConfig;
private _element?: LovelaceCard; private _elements?: LovelaceCard[];
static get properties(): PropertyDeclarations { static get properties(): PropertyDeclarations {
return { return {
@ -27,16 +28,18 @@ export class HuiUnusedEntities extends LitElement {
set hass(hass: HomeAssistant) { set hass(hass: HomeAssistant) {
this._hass = hass; this._hass = hass;
if (!this._element) { if (!this._elements) {
this._createElement(); this._createElements();
return; return;
} }
this._element.hass = this._hass; for (const element of this._elements) {
element.hass = this._hass;
}
} }
public setConfig(config: LovelaceConfig): void { public setConfig(config: LovelaceConfig): void {
this._config = config; this._config = config;
this._createElement(); this._createElements();
} }
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
@ -46,7 +49,7 @@ export class HuiUnusedEntities extends LitElement {
return html` return html`
${this.renderStyle()} ${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` return html`
<style> <style>
#root { #root {
max-width: 600px; padding: 4px;
margin: 0 auto; display: flex;
padding: 8px 0; flex-wrap: wrap;
}
hui-entities-card {
max-width: 400px;
padding: 4px;
flex: 1;
} }
</style> </style>
`; `;
} }
private _createElement(): void { private _createElements(): void {
if (this._hass) { if (!this._hass) {
const entities = computeUnusedEntities(this._hass, this._config!).map( return;
(entity) => ({
entity,
secondary_info: "entity-id",
})
);
this._element = createCardElement({
type: "entities",
title: "Unused entities",
entities,
show_header_toggle: false,
});
this._element!.hass = this._hass;
} }
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.", "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": { "area_registry": {
"caption": "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": { "core": {
"caption": "General", "caption": "General",
@ -565,7 +576,11 @@
}, },
"customize": { "customize": {
"caption": "Customization", "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": { "automation": {
"caption": "Automation", "caption": "Automation",
@ -755,7 +770,27 @@
}, },
"entity_registry": { "entity_registry": {
"caption": "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": { "integrations": {
"caption": "Integrations", "caption": "Integrations",
@ -774,7 +809,8 @@
"hub": "Connected via", "hub": "Connected via",
"firmware": "Firmware: {version}", "firmware": "Firmware: {version}",
"device_unavailable": "device unavailable", "device_unavailable": "device unavailable",
"entity_unavailable": "entity unavailable" "entity_unavailable": "entity unavailable",
"no_area": "No Area"
} }
}, },
"users": { "users": {
@ -847,7 +883,8 @@
"toggle_editor": "Toggle Editor", "toggle_editor": "Toggle Editor",
"add": "Add Card", "add": "Add Card",
"edit": "Edit", "edit": "Edit",
"delete": "Delete" "delete": "Delete",
"move": "Move"
}, },
"save_config": { "save_config": {
"header": "Take control of your Lovelace UI", "header": "Take control of your Lovelace UI",
@ -956,6 +993,29 @@
"working": "Please wait", "working": "Please wait",
"unknown_error": "Something went wrong", "unknown_error": "Something went wrong",
"providers": { "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": { "homeassistant": {
"step": { "step": {
"init": { "init": {

View File

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

View File

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

View File

@ -427,6 +427,10 @@
"webhook": { "webhook": {
"label": "Webhook", "label": "Webhook",
"webhook_id": "Webhook ID" "webhook_id": "Webhook ID"
},
"geo_location": {
"label": "Geolokace",
"zone": "Zóna"
} }
} }
}, },
@ -556,6 +560,12 @@
"device_unavailable": "zařízení není k dispozici", "device_unavailable": "zařízení není k dispozici",
"entity_unavailable": "entita 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": { "profile": {
@ -758,7 +768,9 @@
} }
}, },
"menu": { "menu": {
"configure_ui": "Konfigurovat UI" "configure_ui": "Konfigurovat UI",
"help": "Pomoc",
"refresh": "Obnovit"
} }
} }
}, },
@ -928,6 +940,18 @@
"save": "Uložit", "save": "Uložit",
"name": "Název", "name": "Název",
"entity_id": "Entity ID" "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": { "auth_store": {
@ -978,7 +1002,10 @@
"weblink": "Webový odkaz", "weblink": "Webový odkaz",
"zwave": "Z-Wave", "zwave": "Z-Wave",
"vacuum": "Vysavač", "vacuum": "Vysavač",
"zha": "ZHA" "zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace"
}, },
"attribute": { "attribute": {
"weather": { "weather": {

View File

@ -301,7 +301,8 @@
"period": "Periode" "period": "Periode"
}, },
"logbook": { "logbook": {
"showing_entries": "Viser emner for" "showing_entries": "Viser emner for",
"period": "Periode"
}, },
"mailbox": { "mailbox": {
"empty": "Du har ingen beskeder", "empty": "Du har ingen beskeder",
@ -420,13 +421,27 @@
"label": "Zone", "label": "Zone",
"entity": "Enhed med placering", "entity": "Enhed med placering",
"zone": "Zone", "zone": "Zone",
"event": "Begivenhed", "event": "Begivenhed:",
"enter": "Ankom", "enter": "Ankom",
"leave": "Forlade" "leave": "Forlade"
}, },
"webhook": { "webhook": {
"label": "Webhook", "label": "Webhook",
"webhook_id": "Webhook-ID" "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", "duplicate": "Kopier",
"delete": "Slet", "delete": "Slet",
"delete_confirm": "Er du sikker på du vil slette?", "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_select": "Betingelsestype",
"type": { "type": {
"state": { "state": {
@ -448,7 +463,7 @@
"label": "Numerisk stadie", "label": "Numerisk stadie",
"above": "Over", "above": "Over",
"below": "Under", "below": "Under",
"value_template": "Værdi skabelon (ikke krævet)" "value_template": "Værdi skabelon (valgfri)"
}, },
"sun": { "sun": {
"label": "Sol", "label": "Sol",
@ -556,12 +571,27 @@
"device_unavailable": "enhed utilgængelig", "device_unavailable": "enhed utilgængelig",
"entity_unavailable": "entitet 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": { "profile": {
"push_notifications": { "push_notifications": {
"header": "Push notifikationer", "header": "Push notifikationer",
"description": "Send meddelelser til denne enhed", "description": "Send meddelelser til denne enhed.",
"error_load_platform": "Konfigurer notify.html5", "error_load_platform": "Konfigurer notify.html5",
"error_use_https": "Kræver SSL aktiveret til frontend.", "error_use_https": "Kræver SSL aktiveret til frontend.",
"push_notifications": "Push-meddelelser", "push_notifications": "Push-meddelelser",
@ -724,6 +754,11 @@
"checked_items": "Markerede elementer", "checked_items": "Markerede elementer",
"clear_items": "Ryd markerede elementer", "clear_items": "Ryd markerede elementer",
"add_item": "Tilføj element" "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": { "editor": {
@ -755,10 +790,16 @@
"para_sure": "Er du sikker på du ønsker at tage kontrol over din brugergrænseflade?", "para_sure": "Er du sikker på du ønsker at tage kontrol over din brugergrænseflade?",
"cancel": "Glem det", "cancel": "Glem det",
"save": "tag kontrol" "save": "tag kontrol"
},
"menu": {
"raw_editor": "Tekstbaseret redigering"
} }
}, },
"menu": { "menu": {
"configure_ui": "Konfigurer UI" "configure_ui": "Konfigurer UI",
"unused_entities": "Ubrugte enheder",
"help": "Hjælp",
"refresh": "Opdater"
} }
} }
}, },
@ -928,6 +969,19 @@
"save": "Gem", "save": "Gem",
"name": "Navn", "name": "Navn",
"entity_id": "Enheds ID" "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": { "auth_store": {
@ -977,7 +1031,12 @@
"updater": "Opdater", "updater": "Opdater",
"weblink": "Link", "weblink": "Link",
"zwave": "Z-Wave", "zwave": "Z-Wave",
"vacuum": "Støvsuger" "vacuum": "Støvsuger",
"zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "System sundhed"
}, },
"attribute": { "attribute": {
"weather": { "weather": {

View File

@ -301,7 +301,8 @@
"period": "Zeitraum" "period": "Zeitraum"
}, },
"logbook": { "logbook": {
"showing_entries": "Zeige Einträge für" "showing_entries": "Zeige Einträge für",
"period": "Zeitraum"
}, },
"mailbox": { "mailbox": {
"empty": "Du hast keine Nachrichten", "empty": "Du hast keine Nachrichten",
@ -433,6 +434,14 @@
"hours": "Stunden", "hours": "Stunden",
"minutes": "Minuten", "minutes": "Minuten",
"seconds": "Sekunden" "seconds": "Sekunden"
},
"geo_location": {
"label": "Geolokalisierung",
"source": "Quelle",
"zone": "Zone",
"event": "Ereignis:",
"enter": "Betreten",
"leave": "Verlassen"
} }
} }
}, },
@ -566,6 +575,14 @@
"zha": { "zha": {
"caption": "ZHA", "caption": "ZHA",
"description": "Zigbee Home Automation Netzwerkmanagement" "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": { "profile": {
@ -734,6 +751,11 @@
"checked_items": "Markierte Artikel", "checked_items": "Markierte Artikel",
"clear_items": "Markierte Elemente löschen", "clear_items": "Markierte Elemente löschen",
"add_item": "Artikel hinzufügen" "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": { "editor": {
@ -765,10 +787,16 @@
"para_sure": "Bist du dir sicher, dass du die Benutzeroberfläche selbst verwalten möchtest?", "para_sure": "Bist du dir sicher, dass du die Benutzeroberfläche selbst verwalten möchtest?",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"save": "Kontrolle übernehmen" "save": "Kontrolle übernehmen"
},
"menu": {
"raw_editor": "Raw-Konfigurationseditor"
} }
}, },
"menu": { "menu": {
"configure_ui": "Benutzeroberfläche konfigurieren" "configure_ui": "Benutzeroberfläche konfigurieren",
"unused_entities": "Ungenutzte Elemente",
"help": "Hilfe",
"refresh": "Aktualisieren"
} }
} }
}, },
@ -945,8 +973,8 @@
}, },
"sun": { "sun": {
"elevation": "Höhe", "elevation": "Höhe",
"rising": "Aufgehend", "rising": "Aufgang",
"setting": "Einstellung" "setting": "Untergang"
}, },
"updater": { "updater": {
"title": "Update-Anweisungen" "title": "Update-Anweisungen"
@ -1001,7 +1029,11 @@
"weblink": "Weblink", "weblink": "Weblink",
"zwave": "Z-Wave", "zwave": "Z-Wave",
"vacuum": "Staubsauger", "vacuum": "Staubsauger",
"zha": "ZHA" "zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "Systemzustand"
}, },
"attribute": { "attribute": {
"weather": { "weather": {

View File

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

View File

@ -423,6 +423,17 @@
"event": "Evento:", "event": "Evento:",
"enter": "Entrar", "enter": "Entrar",
"leave": "Salir" "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", "device_unavailable": "dispositivo no disponible",
"entity_unavailable": "entidad no disponible" "entity_unavailable": "entidad no disponible"
} }
},
"zha": {
"caption": "ZHA"
} }
}, },
"profile": { "profile": {
@ -726,7 +740,8 @@
"edit_card": { "edit_card": {
"header": "Configuración de la tarjeta", "header": "Configuración de la tarjeta",
"save": "Guardar", "save": "Guardar",
"toggle_editor": "Cambiar editor" "toggle_editor": "Cambiar editor",
"edit": "Editar"
}, },
"migrate": { "migrate": {
"header": "Configuración inválida", "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'.", "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" "migrate": "Migrar configuración"
} }
},
"menu": {
"help": "Ayuda"
} }
} }
}, },
@ -951,7 +969,10 @@
"updater": "Actualizador", "updater": "Actualizador",
"weblink": "Enlace web", "weblink": "Enlace web",
"zwave": "", "zwave": "",
"vacuum": "Aspiradora" "vacuum": "Aspiradora",
"zha": "ZHA",
"lovelace": "Lovelace",
"system_health": "Estado del sistema"
}, },
"attribute": { "attribute": {
"weather": { "weather": {

View File

@ -301,7 +301,8 @@
"period": "Periodo" "period": "Periodo"
}, },
"logbook": { "logbook": {
"showing_entries": "Mostrando entradas del" "showing_entries": "Mostrando entradas del",
"period": "Periodo"
}, },
"mailbox": { "mailbox": {
"empty": "No tiene ningún mensaje", "empty": "No tiene ningún mensaje",
@ -433,6 +434,14 @@
"hours": "Horas", "hours": "Horas",
"minutes": "Minutos", "minutes": "Minutos",
"seconds": "Segundos" "seconds": "Segundos"
},
"geo_location": {
"label": "Geolocalización",
"source": "Fuente",
"zone": "Zona",
"event": "Evento:",
"enter": "Entrar",
"leave": "Salir"
} }
} }
}, },
@ -565,7 +574,18 @@
}, },
"zha": { "zha": {
"caption": "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": { "profile": {
@ -734,6 +754,11 @@
"checked_items": "Elementos marcados", "checked_items": "Elementos marcados",
"clear_items": "Borrar elementos marcados", "clear_items": "Borrar elementos marcados",
"add_item": "Añadir artículo" "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": { "editor": {
@ -765,10 +790,16 @@
"para_sure": "¿Está seguro de que desea tomar el control de su interfaz de usuario?", "para_sure": "¿Está seguro de que desea tomar el control de su interfaz de usuario?",
"cancel": "No importa", "cancel": "No importa",
"save": "Tomar el control" "save": "Tomar el control"
},
"menu": {
"raw_editor": "Editor de configuración en bruto"
} }
}, },
"menu": { "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", "weblink": "Enlace web",
"zwave": "Z-Wave", "zwave": "Z-Wave",
"vacuum": "Aspiradora", "vacuum": "Aspiradora",
"zha": "ZHA" "zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "Salud del sistema"
}, },
"attribute": { "attribute": {
"weather": { "weather": {

View File

@ -301,7 +301,8 @@
"period": "Période" "period": "Période"
}, },
"logbook": { "logbook": {
"showing_entries": "Afficher les entrées pour le" "showing_entries": "Afficher les entrées pour le",
"period": "Période"
}, },
"mailbox": { "mailbox": {
"empty": "Vous n'avez aucun message", "empty": "Vous n'avez aucun message",
@ -433,6 +434,14 @@
"hours": "Heures", "hours": "Heures",
"minutes": "Minutes", "minutes": "Minutes",
"seconds": "Secondes" "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", "device_unavailable": "appareil indisponible",
"entity_unavailable": "entité 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": { "profile": {
@ -730,6 +754,11 @@
"checked_items": "Éléments cochés", "checked_items": "Éléments cochés",
"clear_items": "Effacer éléments cochés", "clear_items": "Effacer éléments cochés",
"add_item": "Ajouter un élément" "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": { "editor": {
@ -761,10 +790,16 @@
"para_sure": "Êtes-vous sûr de vouloir prendre le controle de l'interface utilisateur?", "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.", "cancel": "Oublie ce que j'ai dit, c'est pas grave.",
"save": "Prenez le contrôle" "save": "Prenez le contrôle"
},
"menu": {
"raw_editor": "Éditeur de configuration"
} }
}, },
"menu": { "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": { "sun": {
"elevation": "Élévation", "elevation": "Élévation",
"rising": "Lever" "rising": "Lever",
"setting": "Coucher"
}, },
"updater": { "updater": {
"title": "Instructions de mise à jour" "title": "Instructions de mise à jour"
@ -995,7 +1031,12 @@
"updater": "Mise à jour", "updater": "Mise à jour",
"weblink": "Lien", "weblink": "Lien",
"zwave": "Z-Wave", "zwave": "Z-Wave",
"vacuum": "Aspirateur" "vacuum": "Aspirateur",
"zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "Santé du système"
}, },
"attribute": { "attribute": {
"weather": { "weather": {

View File

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

View File

@ -576,10 +576,12 @@
"description": "Zigbee Home Automation hálózat menedzsment" "description": "Zigbee Home Automation hálózat menedzsment"
}, },
"area_registry": { "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": { "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": { "profile": {
@ -750,7 +752,9 @@
"add_item": "Tétel hozzáadása" "add_item": "Tétel hozzáadása"
}, },
"empty_state": { "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": { "editor": {
@ -782,6 +786,9 @@
"para_sure": "Biztosan át szeretnéd venni az irányítást a felhasználói felületed felett?", "para_sure": "Biztosan át szeretnéd venni az irányítást a felhasználói felületed felett?",
"cancel": "Mégsem", "cancel": "Mégsem",
"save": "Irányítás átvétele" "save": "Irányítás átvétele"
},
"menu": {
"raw_editor": "Konfiguráció szerkesztő"
} }
}, },
"menu": { "menu": {
@ -956,7 +963,7 @@
"dialogs": { "dialogs": {
"more_info_settings": { "more_info_settings": {
"save": "Mentés", "save": "Mentés",
"name": "Név", "name": "Név felülbírálása",
"entity_id": "Entitás ID" "entity_id": "Entitás ID"
}, },
"more_info_control": { "more_info_control": {
@ -1024,7 +1031,8 @@
"zha": "ZHA", "zha": "ZHA",
"hassio": "Hass.io", "hassio": "Hass.io",
"homeassistant": "Home Assistant", "homeassistant": "Home Assistant",
"lovelace": "Lovelace" "lovelace": "Lovelace",
"system_health": "Rendszer Állapot"
}, },
"attribute": { "attribute": {
"weather": { "weather": {

View File

@ -301,7 +301,8 @@
"period": "Periodo" "period": "Periodo"
}, },
"logbook": { "logbook": {
"showing_entries": "Mostra registrazioni per" "showing_entries": "Mostra registrazioni per",
"period": "Periodo"
}, },
"mailbox": { "mailbox": {
"empty": "Non hai nessun messaggio", "empty": "Non hai nessun messaggio",
@ -426,13 +427,21 @@
}, },
"webhook": { "webhook": {
"label": "Webhook", "label": "Webhook",
"webhook_id": "ID Webhook" "webhook_id": "Webhook ID"
}, },
"time_pattern": { "time_pattern": {
"label": "Pattern temporale", "label": "Pattern temporale",
"hours": "Ore", "hours": "Ore",
"minutes": "Minuti", "minutes": "Minuti",
"seconds": "Secondi" "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" "description_not_login": "Accesso non effettuato"
}, },
"integrations": { "integrations": {
"caption": "integrazioni", "caption": "Integrazioni",
"description": "Gestisci dispositivi e servizi connessi", "description": "Gestisci dispositivi e servizi connessi",
"discovered": "Scoperto", "discovered": "Scoperto",
"configured": "Configurato", "configured": "Configurato",
@ -565,7 +574,18 @@
}, },
"zha": { "zha": {
"caption": "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": { "profile": {
@ -702,7 +722,7 @@
"data": { "data": {
"user": "Utente" "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": { "abort": {
@ -734,6 +754,11 @@
"checked_items": "Elementi selezionati", "checked_items": "Elementi selezionati",
"clear_items": "Cancella gli elementi selezionati", "clear_items": "Cancella gli elementi selezionati",
"add_item": "Aggiungi elemento" "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": { "editor": {
@ -765,10 +790,16 @@
"para_sure": "Sei sicuro di voler prendere il controllo della tua interfaccia utente?", "para_sure": "Sei sicuro di voler prendere il controllo della tua interfaccia utente?",
"cancel": "Rinuncia", "cancel": "Rinuncia",
"save": "Prendere il controllo" "save": "Prendere il controllo"
},
"menu": {
"raw_editor": "Editor di configurazione grezzo"
} }
}, },
"menu": { "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": { "dialogs": {
"more_info_settings": { "more_info_settings": {
"save": "Salva", "save": "Salva",
"name": "Nome", "name": "Sovrascrittura del nome",
"entity_id": "ID Entità" "entity_id": "ID Entità"
}, },
"more_info_control": { "more_info_control": {
@ -1001,7 +1032,11 @@
"weblink": "Link Web", "weblink": "Link Web",
"zwave": "Z-Wave", "zwave": "Z-Wave",
"vacuum": "Aspirapolvere", "vacuum": "Aspirapolvere",
"zha": "ZHA" "zha": "ZHA",
"hassio": "Hass.io",
"homeassistant": "Home Assistant",
"lovelace": "Lovelace",
"system_health": "Salute del sistema"
}, },
"attribute": { "attribute": {
"weather": { "weather": {

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