mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-31 05:06:38 +00:00
commit
4058a0c8d0
@ -3,7 +3,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
throw Error("latestBuild not defined for babel loader config");
|
||||
}
|
||||
return {
|
||||
test: /\.m?js$|\.ts$/,
|
||||
test: /\.m?js$|\.tsx?$/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
@ -12,7 +12,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
require("@babel/preset-env").default,
|
||||
{ modules: false },
|
||||
],
|
||||
require("@babel/preset-typescript").default,
|
||||
[
|
||||
require("@babel/preset-typescript").default,
|
||||
{
|
||||
jsxPragma: "h",
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
plugins: [
|
||||
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
|
||||
@ -28,6 +33,14 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
pragma: "h",
|
||||
},
|
||||
],
|
||||
[
|
||||
require("@babel/plugin-proposal-decorators").default,
|
||||
{ decoratorsBeforeExport: true },
|
||||
],
|
||||
[
|
||||
require("@babel/plugin-proposal-class-properties").default,
|
||||
{ loose: true },
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -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
60
config/webpack.js
Normal 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
11
demo/script/size_stats
Executable 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
|
@ -3,7 +3,6 @@ import "../custom-cards/ha-demo-card";
|
||||
// tslint:disable-next-line
|
||||
import { HADemoCard } from "../custom-cards/ha-demo-card";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import { HUIView } from "../../../src/panels/lovelace/hui-view";
|
||||
import { selectedDemoConfig } from "../configs/demo-configs";
|
||||
|
||||
export const mockLovelace = (hass: MockHomeAssistant) => {
|
||||
@ -16,13 +15,17 @@ export const mockLovelace = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
};
|
||||
|
||||
// Patch HUI-VIEW to make the lovelace object available to the demo card
|
||||
const oldCreateCard = HUIView.prototype.createCardElement;
|
||||
customElements.whenDefined("hui-view").then(() => {
|
||||
// tslint:disable-next-line
|
||||
const HUIView = customElements.get("hui-view");
|
||||
// Patch HUI-VIEW to make the lovelace object available to the demo card
|
||||
const oldCreateCard = HUIView.prototype.createCardElement;
|
||||
|
||||
HUIView.prototype.createCardElement = function(config) {
|
||||
const el = oldCreateCard.call(this, config);
|
||||
if (el.tagName === "HA-DEMO-CARD") {
|
||||
(el as HADemoCard).lovelace = this.lovelace;
|
||||
}
|
||||
return el;
|
||||
};
|
||||
HUIView.prototype.createCardElement = function(config) {
|
||||
const el = oldCreateCard.call(this, config);
|
||||
if (el.tagName === "HA-DEMO-CARD") {
|
||||
(el as HADemoCard).lovelace = this.lovelace;
|
||||
}
|
||||
return el;
|
||||
};
|
||||
});
|
||||
|
@ -3,10 +3,12 @@ const webpack = require("webpack");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const WorkboxPlugin = require("workbox-webpack-plugin");
|
||||
const { babelLoaderConfig } = require("../config/babel.js");
|
||||
const { minimizer } = require("../config/babel.js");
|
||||
const webpackBase = require("../config/webpack.js");
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||
const isStatsBuild = process.env.STATS === "1";
|
||||
const chunkFilename =
|
||||
isProd && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||
const buildPath = path.resolve(__dirname, "dist");
|
||||
const publicPath = "/";
|
||||
|
||||
@ -14,9 +16,7 @@ const latestBuild = false;
|
||||
|
||||
module.exports = {
|
||||
mode: isProd ? "production" : "development",
|
||||
// Disabled in prod while we make Home Assistant able to serve the right files.
|
||||
// Was source-map
|
||||
devtool: isProd ? "none" : "inline-source-map",
|
||||
devtool: isProd ? "cheap-source-map" : "inline-source-map",
|
||||
entry: {
|
||||
main: "./src/entrypoint.ts",
|
||||
compatibility: "../src/entrypoints/compatibility.js",
|
||||
@ -39,9 +39,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer,
|
||||
},
|
||||
optimization: webpackBase.optimization,
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: false,
|
||||
@ -71,23 +69,14 @@ module.exports = {
|
||||
to: "static/images/leaflet/",
|
||||
},
|
||||
]),
|
||||
...webpackBase.plugins,
|
||||
isProd &&
|
||||
new WorkboxPlugin.GenerateSW({
|
||||
swDest: "service_worker_es5.js",
|
||||
importWorkboxFrom: "local",
|
||||
}),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
alias: {
|
||||
react: "preact-compat",
|
||||
"react-dom": "preact-compat",
|
||||
// Not necessary unless you consume a module using `createClass`
|
||||
"create-react-class": "preact-compat/lib/create-react-class",
|
||||
// Not necessary unless you consume a module requiring `react-dom-factories`
|
||||
"react-dom-factories": "preact-compat/lib/react-dom-factories",
|
||||
},
|
||||
},
|
||||
resolve: webpackBase.resolve,
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
chunkFilename: chunkFilename,
|
||||
|
@ -2,6 +2,19 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/demo-cards";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("sensor", "brightness", "12", {}),
|
||||
getEntity("plant", "bonsai", "ok", {}),
|
||||
getEntity("sensor", "outside_humidity", "54", {
|
||||
unit_of_measurement: "%",
|
||||
}),
|
||||
getEntity("sensor", "outside_temperature", "15.6", {
|
||||
unit_of_measurement: "°C",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
@ -66,7 +79,7 @@ const CONFIGS = [
|
||||
class DemoGaugeEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards configs="[[_configs]]"></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -78,6 +91,12 @@ class DemoGaugeEntity extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-gauge-card", DemoGaugeEntity);
|
||||
|
@ -2,6 +2,17 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/demo-cards";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("light", "kitchen_lights", "on", {
|
||||
friendly_name: "Kitchen Lights",
|
||||
}),
|
||||
getEntity("light", "bed_light", "off", {
|
||||
friendly_name: "Bed Light",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
@ -10,6 +21,8 @@ const CONFIGS = [
|
||||
- type: picture-entity
|
||||
image: /images/kitchen.png
|
||||
entity: light.kitchen_lights
|
||||
tap_action:
|
||||
action: toggle
|
||||
`,
|
||||
},
|
||||
{
|
||||
@ -18,6 +31,8 @@ const CONFIGS = [
|
||||
- type: picture-entity
|
||||
image: /images/bed.png
|
||||
entity: light.bed_light
|
||||
tap_action:
|
||||
action: toggle
|
||||
`,
|
||||
},
|
||||
{
|
||||
@ -68,7 +83,7 @@ const CONFIGS = [
|
||||
class DemoPicEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards configs="[[_configs]]"></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -80,6 +95,12 @@ class DemoPicEntity extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-picture-entity-card", DemoPicEntity);
|
||||
|
@ -2,6 +2,25 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/demo-cards";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("switch", "decorative_lights", "on", {
|
||||
friendly_name: "Decorative Lights",
|
||||
}),
|
||||
getEntity("light", "ceiling_lights", "on", {
|
||||
friendly_name: "Ceiling Lights",
|
||||
}),
|
||||
getEntity("binary_sensor", "movement_backyard", "on", {
|
||||
friendly_name: "Movement Backyard",
|
||||
device_class: "moving",
|
||||
}),
|
||||
getEntity("binary_sensor", "basement_floor_wet", "off", {
|
||||
friendly_name: "Basement Floor Wet",
|
||||
device_class: "moisture",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
@ -105,7 +124,7 @@ const CONFIGS = [
|
||||
class DemoPicGlance extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards configs="[[_configs]]"></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -117,6 +136,12 @@ class DemoPicGlance extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-picture-glance-card", DemoPicGlance);
|
||||
|
@ -1,7 +1,7 @@
|
||||
const path = require("path");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const { babelLoaderConfig } = require("../config/babel.js");
|
||||
const { minimizer } = require("../config/babel.js");
|
||||
const webpackBase = require("../config/webpack.js");
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||
@ -32,9 +32,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer,
|
||||
},
|
||||
optimization: webpackBase.optimization,
|
||||
plugins: [
|
||||
new CopyWebpackPlugin([
|
||||
"public",
|
||||
@ -63,9 +61,7 @@ module.exports = {
|
||||
},
|
||||
}),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
},
|
||||
resolve: webpackBase.resolve,
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
chunkFilename: chunkFilename,
|
||||
|
20
package.json
20
package.json
@ -64,25 +64,24 @@
|
||||
"@polymer/polymer": "^3.0.5",
|
||||
"@vaadin/vaadin-combo-box": "^4.2.0",
|
||||
"@vaadin/vaadin-date-picker": "^3.3.1",
|
||||
"@webcomponents/shadycss": "^1.6.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.0",
|
||||
"@webcomponents/shadycss": "^1.9.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.6",
|
||||
"chart.js": "~2.7.2",
|
||||
"chartjs-chart-timeline": "^0.2.1",
|
||||
"codemirror": "^5.43.0",
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"eslint-import-resolver-webpack": "^0.10.1",
|
||||
"fecha": "^3.0.0",
|
||||
"gulp-hash-filename": "^2.0.1",
|
||||
"home-assistant-js-websocket": "^3.2.4",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"jquery": "^3.3.1",
|
||||
"js-yaml": "^3.12.0",
|
||||
"leaflet": "^1.3.4",
|
||||
"lit-element": "2.0.0-rc.5",
|
||||
"lit-html": "1.0.0-rc.2",
|
||||
"lit-element": "^2.0.0",
|
||||
"lit-html": "^1.0.0",
|
||||
"marked": "^0.6.0",
|
||||
"mdn-polyfills": "^5.12.0",
|
||||
"memoize-one": "^5.0.0",
|
||||
"moment": "^2.22.2",
|
||||
"preact": "^8.3.1",
|
||||
"preact-compat": "^3.18.4",
|
||||
@ -97,6 +96,8 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/plugin-external-helpers": "^7.0.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.3.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.3.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||
"@babel/plugin-transform-react-jsx": "^7.0.0",
|
||||
@ -105,6 +106,7 @@
|
||||
"@gfx/zopfli": "^1.0.9",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/codemirror": "^0.0.71",
|
||||
"@types/memoize-one": "^4.1.0",
|
||||
"@types/mocha": "^5.2.5",
|
||||
"babel-eslint": "^10",
|
||||
"babel-loader": "^8.0.4",
|
||||
@ -116,12 +118,14 @@
|
||||
"eslint": "^5.6.0",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-config-prettier": "^4.0.0",
|
||||
"eslint-import-resolver-webpack": "^0.10.1",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-prettier": "^3.0.0",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-foreach": "^0.1.0",
|
||||
"gulp-hash": "^4.2.2",
|
||||
"gulp-hash-filename": "^2.0.1",
|
||||
"gulp-insert": "^0.5.0",
|
||||
"gulp-json-transform": "^0.4.5",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
@ -156,8 +160,8 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"@webcomponents/webcomponentsjs": "2.2.1",
|
||||
"@webcomponents/shadycss": "^1.6.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.6",
|
||||
"@webcomponents/shadycss": "^1.9.0",
|
||||
"@vaadin/vaadin-overlay": "3.2.2",
|
||||
"@vaadin/vaadin-lumo-styles": "1.3.0"
|
||||
},
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20190203.0",
|
||||
version="20190212.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
20
src/common/file/b64-to-blob.ts
Normal file
20
src/common/file/b64-to-blob.ts
Normal 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 });
|
||||
};
|
120
src/components/entity/ha-entities-picker.ts
Normal file
120
src/components/entity/ha-entities-picker.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
200
src/components/entity/ha-entity-picker.ts
Normal file
200
src/components/entity/ha-entity-picker.ts
Normal 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);
|
@ -9,7 +9,7 @@ import {
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
PropertyDeclarations,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
@ -17,7 +17,7 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
class HaEntityToggle extends LitElement {
|
||||
// hass is not a property so that we only re-render on stateObj changes
|
||||
public hass?: HomeAssistant;
|
||||
public stateObj?: HassEntity;
|
||||
@property() public stateObj?: HassEntity;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.stateObj) {
|
||||
@ -51,12 +51,6 @@ class HaEntityToggle extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
stateObj: {},
|
||||
};
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("click", (ev) => ev.stopPropagation());
|
||||
|
@ -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);
|
127
src/components/entity/state-badge.ts
Normal file
127
src/components/entity/state-badge.ts
Normal 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);
|
@ -9,9 +9,12 @@ class HaCard extends PolymerElement {
|
||||
:host {
|
||||
@apply --paper-material-elevation-1;
|
||||
display: block;
|
||||
border-radius: 2px;
|
||||
border-radius: var(--ha-card-border-radius, 2px);
|
||||
transition: all 0.3s ease-out;
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
background: var(
|
||||
--ha-card-background,
|
||||
var(--paper-card-background-color, white)
|
||||
);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.header {
|
||||
|
@ -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
36
src/components/ha-icon.ts
Normal 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);
|
@ -3,8 +3,8 @@ import {
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
@ -82,13 +82,9 @@ const computePanels = (hass: HomeAssistant) => {
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaSidebar extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public _defaultPage?: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._defaultPage = localStorage.defaultPage || DEFAULT_PANEL;
|
||||
}
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public _defaultPage?: string =
|
||||
localStorage.defaultPage || DEFAULT_PANEL;
|
||||
|
||||
protected render() {
|
||||
const hass = this.hass;
|
||||
@ -217,13 +213,6 @@ class HaSidebar extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_defaultPage: {},
|
||||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (!this.hass || !changedProps.has("hass")) {
|
||||
return false;
|
||||
|
17
src/data/automation.ts
Normal file
17
src/data/automation.ts
Normal 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
12
src/data/camera.ts
Normal 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
46
src/data/person.ts
Normal 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
5
src/data/script.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface EventAction {
|
||||
event: string;
|
||||
event_data?: { [key: string]: any };
|
||||
event_data_template?: { [key: string]: any };
|
||||
}
|
@ -7,8 +7,19 @@ export interface ZHADeviceEntity extends HassEntity {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ZHAEntities {
|
||||
[key: string]: HassEntity[];
|
||||
export interface ZHAEntityReference extends HassEntity {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ZHADevice {
|
||||
name: string;
|
||||
ieee: string;
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
quirk_applied: boolean;
|
||||
quirk_class: string;
|
||||
entities: ZHAEntityReference[];
|
||||
manufacturer_code: number;
|
||||
}
|
||||
|
||||
export interface Attribute {
|
||||
@ -19,6 +30,7 @@ export interface Attribute {
|
||||
export interface Cluster {
|
||||
name: string;
|
||||
id: number;
|
||||
endpoint_id: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
@ -29,7 +41,8 @@ export interface Command {
|
||||
}
|
||||
|
||||
export interface ReadAttributeServiceData {
|
||||
entity_id: string;
|
||||
ieee: string;
|
||||
endpoint_id: number;
|
||||
cluster_id: number;
|
||||
cluster_type: string;
|
||||
attribute: number;
|
||||
@ -41,64 +54,60 @@ export const reconfigureNode = (
|
||||
ieeeAddress: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "zha/nodes/reconfigure",
|
||||
type: "zha/devices/reconfigure",
|
||||
ieee: ieeeAddress,
|
||||
});
|
||||
|
||||
export const fetchAttributesForCluster = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
ieeeAddress: string,
|
||||
endpointId: number,
|
||||
clusterId: number,
|
||||
clusterType: string
|
||||
): Promise<Attribute[]> =>
|
||||
hass.callWS({
|
||||
type: "zha/entities/clusters/attributes",
|
||||
entity_id: entityId,
|
||||
type: "zha/devices/clusters/attributes",
|
||||
ieee: ieeeAddress,
|
||||
endpoint_id: endpointId,
|
||||
cluster_id: clusterId,
|
||||
cluster_type: clusterType,
|
||||
});
|
||||
|
||||
export const fetchDevices = (hass: HomeAssistant): Promise<ZHADevice[]> =>
|
||||
hass.callWS({
|
||||
type: "zha/devices",
|
||||
});
|
||||
|
||||
export const readAttributeValue = (
|
||||
hass: HomeAssistant,
|
||||
data: ReadAttributeServiceData
|
||||
): Promise<string> => {
|
||||
return hass.callWS({
|
||||
...data,
|
||||
type: "zha/entities/clusters/attributes/value",
|
||||
type: "zha/devices/clusters/attributes/value",
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchCommandsForCluster = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
ieeeAddress: string,
|
||||
endpointId: number,
|
||||
clusterId: number,
|
||||
clusterType: string
|
||||
): Promise<Command[]> =>
|
||||
hass.callWS({
|
||||
type: "zha/entities/clusters/commands",
|
||||
entity_id: entityId,
|
||||
type: "zha/devices/clusters/commands",
|
||||
ieee: ieeeAddress,
|
||||
endpoint_id: endpointId,
|
||||
cluster_id: clusterId,
|
||||
cluster_type: clusterType,
|
||||
});
|
||||
|
||||
export const fetchClustersForZhaNode = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
ieeeAddress: string
|
||||
): Promise<Cluster[]> =>
|
||||
hass.callWS({
|
||||
type: "zha/entities/clusters",
|
||||
entity_id: entityId,
|
||||
type: "zha/devices/clusters",
|
||||
ieee: ieeeAddress,
|
||||
});
|
||||
|
||||
export const fetchEntitiesForZhaNode = (
|
||||
hass: HomeAssistant
|
||||
): Promise<ZHAEntities> =>
|
||||
hass.callWS({
|
||||
type: "zha/entities",
|
||||
});
|
||||
|
@ -18,6 +18,10 @@ class HaStoreAuth extends LocalizeMixin(PolymerElement) {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
border-top: 0;
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import {
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
UpdatingElement,
|
||||
} from "lit-element";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import "./more-info-alarm_control_panel";
|
||||
import "./more-info-automation";
|
||||
@ -23,26 +28,30 @@ import "./more-info-weather";
|
||||
|
||||
import stateMoreInfoType from "../../../common/entity/state_more_info_type";
|
||||
import dynamicContentUpdater from "../../../common/dom/dynamic_content_updater";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
class MoreInfoContent extends PolymerElement {
|
||||
static get properties() {
|
||||
class MoreInfoContent extends UpdatingElement {
|
||||
public hass?: HomeAssistant;
|
||||
public stateObj?: HassEntity;
|
||||
private _detachedChild?: ChildNode;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
hass: {},
|
||||
stateObj: {},
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return ["stateObjChanged(stateObj, hass)"];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
protected firstUpdated(): void {
|
||||
this.style.display = "block";
|
||||
}
|
||||
|
||||
stateObjChanged(stateObj, hass) {
|
||||
let moreInfoType;
|
||||
// This is not a lit element, but an updating element, so we implement update
|
||||
protected update(changedProps: PropertyValues): void {
|
||||
super.update(changedProps);
|
||||
const stateObj = this.stateObj;
|
||||
const hass = this.hass;
|
||||
|
||||
if (!stateObj || !hass) {
|
||||
if (this.lastChild) {
|
||||
this._detachedChild = this.lastChild;
|
||||
@ -51,18 +60,20 @@ class MoreInfoContent extends PolymerElement {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._detachedChild) {
|
||||
this.appendChild(this._detachedChild);
|
||||
this._detachedChild = null;
|
||||
}
|
||||
if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
|
||||
moreInfoType = stateObj.attributes.custom_ui_more_info;
|
||||
} else {
|
||||
moreInfoType = "more-info-" + stateMoreInfoType(stateObj);
|
||||
this._detachedChild = undefined;
|
||||
}
|
||||
|
||||
const moreInfoType =
|
||||
stateObj.attributes && "custom_ui_more_info" in stateObj.attributes
|
||||
? stateObj.attributes.custom_ui_more_info
|
||||
: "more-info-" + stateMoreInfoType(stateObj);
|
||||
|
||||
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
|
||||
hass: hass,
|
||||
stateObj: stateObj,
|
||||
hass,
|
||||
stateObj,
|
||||
});
|
||||
}
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import { Constructor, LitElement } from "lit-element";
|
||||
import { HassBaseEl } from "./hass-base-mixin";
|
||||
import { HaToast } from "../../components/ha-toast";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
|
||||
export default (superClass: Constructor<LitElement & HassBaseEl>) =>
|
||||
class extends superClass {
|
||||
private _discToast?: HaToast;
|
||||
|
||||
protected hassConnected() {
|
||||
super.hassConnected();
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
// Need to load in advance because when disconnected, can't dynamically load code.
|
||||
import(/* webpackChunkName: "ha-toast" */ "../../components/ha-toast");
|
||||
}
|
||||
@ -24,10 +25,13 @@ export default (superClass: Constructor<LitElement & HassBaseEl>) =>
|
||||
if (!this._discToast) {
|
||||
const el = document.createElement("ha-toast");
|
||||
el.duration = 0;
|
||||
el.text = this.hass!.localize("ui.notification_toast.connection_lost");
|
||||
this._discToast = el;
|
||||
this.shadowRoot!.appendChild(el as any);
|
||||
}
|
||||
this._discToast.dir = computeRTL(this.hass!);
|
||||
this._discToast.text = this.hass!.localize(
|
||||
"ui.notification_toast.connection_lost"
|
||||
);
|
||||
this._discToast.opened = true;
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { Constructor } from "lit-element";
|
||||
import {
|
||||
Constructor,
|
||||
// @ts-ignore
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
/* tslint:disable */
|
||||
@ -17,14 +21,9 @@ export class HassBaseEl {
|
||||
export default <T>(superClass: Constructor<T>): Constructor<T & HassBaseEl> =>
|
||||
// @ts-ignore
|
||||
class extends superClass {
|
||||
private __provideHass: HTMLElement[];
|
||||
private __provideHass: HTMLElement[] = [];
|
||||
// @ts-ignore
|
||||
protected hass: HomeAssistant;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.__provideHass = [];
|
||||
}
|
||||
@property() protected hass: HomeAssistant;
|
||||
|
||||
// Exists so all methods can safely call super method
|
||||
protected hassConnected() {
|
||||
|
@ -1,11 +1,5 @@
|
||||
import "@polymer/app-route/app-location";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import { html, LitElement, PropertyValues, css, property } from "lit-element";
|
||||
|
||||
import "../home-assistant-main";
|
||||
import "../ha-init-page";
|
||||
@ -43,19 +37,9 @@ export class HomeAssistantAppEl extends ext(HassBaseMixin(LitElement), [
|
||||
NotificationMixin,
|
||||
dialogManagerMixin,
|
||||
]) {
|
||||
private _route?: Route;
|
||||
private _error?: boolean;
|
||||
private _panelUrl?: string;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_route: {},
|
||||
_routeData: {},
|
||||
_panelUrl: {},
|
||||
_error: {},
|
||||
};
|
||||
}
|
||||
@property() private _route?: Route;
|
||||
@property() private _error?: boolean;
|
||||
@property() private _panelUrl?: string;
|
||||
|
||||
protected render() {
|
||||
const hass = this.hass;
|
||||
|
@ -2,10 +2,10 @@ import {
|
||||
LitElement,
|
||||
html,
|
||||
TemplateResult,
|
||||
PropertyDeclarations,
|
||||
CSSResult,
|
||||
css,
|
||||
PropertyValues,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "@polymer/app-layout/app-drawer-layout/app-drawer-layout";
|
||||
import "@polymer/app-layout/app-drawer/app-drawer";
|
||||
@ -33,17 +33,9 @@ declare global {
|
||||
}
|
||||
|
||||
class HomeAssistantMain extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public route?: Route;
|
||||
private _narrow?: boolean;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_narrow: {},
|
||||
route: {},
|
||||
};
|
||||
}
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public route?: Route;
|
||||
@property() private _narrow?: boolean;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const hass = this.hass;
|
||||
|
@ -1,9 +1,4 @@
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { LitElement, html, PropertyValues, property } from "lit-element";
|
||||
|
||||
import "./hass-loading-screen";
|
||||
import { HomeAssistant, Panel, PanelElement, Route } from "../types";
|
||||
@ -112,34 +107,16 @@ function ensureLoaded(panel): Promise<void> | null {
|
||||
}
|
||||
|
||||
class PartialPanelResolver extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public narrow?: boolean;
|
||||
public showMenu?: boolean;
|
||||
public route?: Route | null;
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public narrow?: boolean;
|
||||
@property() public showMenu?: boolean;
|
||||
@property() public route?: Route | null;
|
||||
|
||||
private _routeTail?: Route | null;
|
||||
@property() private _routeTail?: Route | null;
|
||||
@property() private _panelEl?: PanelElement;
|
||||
@property() private _error?: boolean;
|
||||
private _panel?: Panel;
|
||||
private _panelEl?: PanelElement;
|
||||
private _error?: boolean;
|
||||
private _cache: { [name: string]: PanelElement };
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
narrow: {},
|
||||
showMenu: {},
|
||||
route: {},
|
||||
|
||||
_routeTail: {},
|
||||
_error: {},
|
||||
_panelEl: {},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._cache = {};
|
||||
}
|
||||
private _cache: { [name: string]: PanelElement } = {};
|
||||
|
||||
protected render() {
|
||||
if (this._error) {
|
||||
|
@ -51,7 +51,13 @@ class DialogAreaDetail extends LitElement {
|
||||
opened
|
||||
@opened-changed="${this._openedChanged}"
|
||||
>
|
||||
<h2>${this._params.entry ? this._params.entry.name : "New Area"}</h2>
|
||||
<h2>
|
||||
${this._params.entry
|
||||
? this._params.entry.name
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.area_registry.editor.default_name"
|
||||
)}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
${this._error
|
||||
? html`
|
||||
@ -62,7 +68,7 @@ class DialogAreaDetail extends LitElement {
|
||||
<paper-input
|
||||
.value=${this._name}
|
||||
@value-changed=${this._nameChanged}
|
||||
label="Name"
|
||||
.label=${this.hass.localize("ui.dialogs.more_info_settings.name")}
|
||||
error-message="Name is required"
|
||||
.invalid=${nameInvalid}
|
||||
></paper-input>
|
||||
@ -76,7 +82,9 @@ class DialogAreaDetail extends LitElement {
|
||||
@click="${this._deleteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
DELETE
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.area_registry.editor.delete"
|
||||
)}
|
||||
</paper-button>
|
||||
`
|
||||
: html``}
|
||||
@ -84,7 +92,13 @@ class DialogAreaDetail extends LitElement {
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
>
|
||||
${this._params.entry ? "UPDATE" : "CREATE"}
|
||||
${this._params.entry
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.area_registry.editor.update"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.area_registry.editor.create"
|
||||
)}
|
||||
</paper-button>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
|
@ -50,7 +50,9 @@ class HaConfigAreaRegistry extends LitElement {
|
||||
return html`
|
||||
<hass-subpage header="Area Registry">
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">Area Registry</span>
|
||||
<span slot="header">
|
||||
${this.hass.localize("ui.panel.config.area_registry.picker.header")}
|
||||
</span>
|
||||
<span slot="introduction">
|
||||
Areas are used to organize where devices are. This information will
|
||||
be used throughout Home Assistant to help you in organizing your
|
||||
@ -74,10 +76,14 @@ class HaConfigAreaRegistry extends LitElement {
|
||||
${this._items.length === 0
|
||||
? html`
|
||||
<div class="empty">
|
||||
Looks like you have no areas yet!
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.area_registry.picker.no_areas"
|
||||
)}
|
||||
<paper-button @click=${this._createArea}>
|
||||
CREATE AREA</paper-button
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.area_registry.picker.create_area"
|
||||
)}
|
||||
</paper-button>
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
|
@ -14,7 +14,7 @@ export interface AreaRegistryDetailDialogParams {
|
||||
}
|
||||
|
||||
export const loadAreaRegistryDetailDialog = () =>
|
||||
import(/* webpackChunkName: "entity-registry-detail-dialog" */ "./dialog-area-registry-detail");
|
||||
import(/* webpackChunkName: "area-registry-detail-dialog" */ "./dialog-area-registry-detail");
|
||||
|
||||
export const showAreaRegistryDetailDialog = (
|
||||
element: HTMLElement,
|
||||
|
@ -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);
|
288
src/panels/config/automation/ha-automation-editor.ts
Normal file
288
src/panels/config/automation/ha-automation-editor.ts
Normal 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);
|
@ -39,6 +39,7 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
}
|
||||
.content {
|
||||
padding-bottom: 24px;
|
||||
direction: ltr;
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
|
@ -17,6 +17,7 @@ class HaConfigCloudForgotPassword extends EventsMixin(PolymerElement) {
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
padding-bottom: 24px;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
paper-card {
|
||||
|
@ -26,6 +26,7 @@ class HaConfigCloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
padding-bottom: 24px;
|
||||
direction: ltr;
|
||||
}
|
||||
[slot="introduction"] {
|
||||
margin: -1em 0;
|
||||
|
@ -16,6 +16,10 @@ class HaConfigCloudRegister extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
[slot=introduction] {
|
||||
margin: -1em 0;
|
||||
}
|
||||
|
@ -81,7 +81,9 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
slot="dropdown-content"
|
||||
selected="[[_computeSelectedArea(areas, device)]]"
|
||||
>
|
||||
<paper-item>No Area</paper-item>
|
||||
<paper-item>
|
||||
[[localize('ui.panel.config.integrations.config_entry.no_area')]]
|
||||
</paper-item>
|
||||
<template is="dom-repeat" items="[[areas]]">
|
||||
<paper-item area="[[item]]">[[item.name]]</paper-item>
|
||||
</template>
|
||||
|
@ -39,11 +39,11 @@ class HaConfigCustomize extends LocalizeMixin(PolymerElement) {
|
||||
|
||||
<div class$="[[computeClasses(isWide)]]">
|
||||
<ha-config-section is-wide="[[isWide]]">
|
||||
<span slot="header">Customization</span>
|
||||
<span slot="header">
|
||||
[[localize('ui.panel.config.customize.picker.header')]]
|
||||
</span>
|
||||
<span slot="introduction">
|
||||
Tweak per-entity attributes.<br />
|
||||
Added/edited customizations will take effect immediately. Removed
|
||||
customizations will take effect when the entity is updated.
|
||||
[[localize('ui.panel.config.customize.picker.introduction')]]
|
||||
</span>
|
||||
<ha-entity-config
|
||||
hass="[[hass]]"
|
||||
|
@ -52,13 +52,14 @@ class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
|
||||
type: Array,
|
||||
value: [
|
||||
"core",
|
||||
"customize",
|
||||
"person",
|
||||
"entity_registry",
|
||||
"area_registry",
|
||||
"automation",
|
||||
"script",
|
||||
"zha",
|
||||
"zwave",
|
||||
"customize",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -65,7 +65,11 @@ class DialogEntityRegistryDetail extends LitElement {
|
||||
<paper-dialog-scrollable>
|
||||
${!stateObj
|
||||
? html`
|
||||
<div>This entity is not currently available.</div>
|
||||
<div>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entity_registry.editor.unavailable"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._error
|
||||
@ -99,13 +103,17 @@ class DialogEntityRegistryDetail extends LitElement {
|
||||
@click="${this._deleteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
DELETE
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entity_registry.editor.delete"
|
||||
)}
|
||||
</paper-button>
|
||||
<paper-button
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${invalidDomainUpdate || this._submitting}
|
||||
>
|
||||
UPDATE
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entity_registry.editor.update"
|
||||
)}
|
||||
</paper-button>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
|
@ -53,7 +53,11 @@ class HaConfigEntityRegistry extends LitElement {
|
||||
return html`
|
||||
<hass-subpage header="Entity Registry">
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">Entity Registry</span>
|
||||
<span slot="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entity_registry.picker.header"
|
||||
)}
|
||||
</span>
|
||||
<span slot="introduction">
|
||||
Home Assistant keeps a registry of every entity it has ever seen
|
||||
that can be uniquely identified. Each of these entities will have an
|
||||
@ -79,7 +83,9 @@ class HaConfigEntityRegistry extends LitElement {
|
||||
<paper-item-body two-line>
|
||||
<div class="name">
|
||||
${computeEntityRegistryName(this.hass!, entry) ||
|
||||
"(unavailable)"}
|
||||
this.hass!.localize(
|
||||
"ui.panel.config.entity_registry.picker.unavailable"
|
||||
)}
|
||||
</div>
|
||||
<div class="secondary entity-id">
|
||||
${entry.entity_id}
|
||||
@ -150,6 +156,7 @@ Deleting an entry will not remove the entity from Home Assistant. To do this, yo
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
direction: ltr;
|
||||
}
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
|
@ -15,6 +15,7 @@ class HaEntityConfig extends PolymerElement {
|
||||
<style include="iron-flex ha-style">
|
||||
paper-card {
|
||||
display: block;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.device-picker {
|
||||
|
@ -9,19 +9,6 @@ import isComponentLoaded from "../../common/config/is_component_loaded";
|
||||
import EventsMixin from "../../mixins/events-mixin";
|
||||
import NavigateMixin from "../../mixins/navigate-mixin";
|
||||
|
||||
import(/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry");
|
||||
import(/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation");
|
||||
import(/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud");
|
||||
import(/* webpackChunkName: "panel-config-config" */ "./config-entries/ha-config-entries");
|
||||
import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core");
|
||||
import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize");
|
||||
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
|
||||
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
|
||||
import(/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry");
|
||||
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
|
||||
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
|
||||
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin NavigateMixin
|
||||
@ -136,6 +123,15 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||
></ha-config-zwave>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if='[[_equals(_routeData.page, "person")]]' restamp>
|
||||
<ha-config-person
|
||||
page-name="person"
|
||||
route="[[route]]"
|
||||
hass="[[hass]]"
|
||||
is-wide="[[isWide]]"
|
||||
></ha-config-person>
|
||||
</template>
|
||||
|
||||
<template
|
||||
is="dom-if"
|
||||
if='[[_equals(_routeData.page, "customize")]]'
|
||||
@ -207,6 +203,19 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||
this.addEventListener("ha-refresh-cloud-status", () =>
|
||||
this._updateCloudStatus()
|
||||
);
|
||||
import(/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry");
|
||||
import(/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation");
|
||||
import(/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud");
|
||||
import(/* webpackChunkName: "panel-config-config" */ "./config-entries/ha-config-entries");
|
||||
import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core");
|
||||
import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize");
|
||||
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
|
||||
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
|
||||
import(/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry");
|
||||
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
|
||||
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
|
||||
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
|
||||
import(/* webpackChunkName: "panel-config-person" */ "./person/ha-config-person");
|
||||
}
|
||||
|
||||
async _updateCloudStatus() {
|
||||
|
@ -54,6 +54,7 @@ export default class NumericStateCondition extends Component {
|
||||
name="value_template"
|
||||
value={value_template}
|
||||
onvalue-changed={this.onChange}
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -22,6 +22,7 @@ export default class TemplateCondition extends Component {
|
||||
name="value_template"
|
||||
value={value_template}
|
||||
onvalue-changed={this.onChange}
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -53,6 +53,7 @@ export default class JSONTextArea extends Component {
|
||||
value={value}
|
||||
style={style}
|
||||
onvalue-changed={this.onChange}
|
||||
dir="ltr"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
12
src/panels/config/js/preact-types.ts
Normal file
12
src/panels/config/js/preact-types.ts
Normal 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>;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,8 +3,26 @@ import "@polymer/paper-input/paper-input";
|
||||
|
||||
import JSONTextArea from "../json_textarea";
|
||||
import { onChangeEvent } from "../../../../common/preact/event";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { EventAction } from "../../../../data/script";
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
action: EventAction;
|
||||
localize: LocalizeFunc;
|
||||
onChange: (index: number, action: EventAction) => void;
|
||||
}
|
||||
|
||||
export default class EventActionForm extends Component<Props> {
|
||||
private onChange: (event: Event) => void;
|
||||
|
||||
static get defaultConfig(): EventAction {
|
||||
return {
|
||||
event: "",
|
||||
event_data: {},
|
||||
};
|
||||
}
|
||||
|
||||
export default class EventAction extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -12,16 +30,11 @@ export default class EventAction extends Component {
|
||||
this.serviceDataChanged = this.serviceDataChanged.bind(this);
|
||||
}
|
||||
|
||||
serviceDataChanged(data) {
|
||||
this.props.onChange(
|
||||
this.props.index,
|
||||
Object.assign({}, this.props.action, { data })
|
||||
);
|
||||
}
|
||||
|
||||
render({ action, localize }) {
|
||||
/* eslint-disable camelcase */
|
||||
const { event, event_data } = action;
|
||||
public render() {
|
||||
const {
|
||||
action: { event, event_data },
|
||||
localize,
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<paper-input
|
||||
@ -42,9 +55,11 @@ export default class EventAction extends Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EventAction.defaultConfig = {
|
||||
event: "",
|
||||
event_data: {},
|
||||
};
|
||||
private serviceDataChanged(eventData) {
|
||||
this.props.onChange(this.props.index, {
|
||||
...this.props.action,
|
||||
event_data: eventData,
|
||||
});
|
||||
}
|
||||
}
|
@ -36,6 +36,7 @@ export default class WaitAction extends Component {
|
||||
name="wait_template"
|
||||
value={wait_template}
|
||||
onvalue-changed={this.onTemplateChange}
|
||||
dir="ltr"
|
||||
/>
|
||||
<paper-input
|
||||
label={localize(
|
||||
|
@ -67,6 +67,7 @@ export default class NumericStateTrigger extends Component {
|
||||
name="value_template"
|
||||
value={value_template}
|
||||
onvalue-changed={this.onChange}
|
||||
dir="ltr"
|
||||
/>
|
||||
<paper-input
|
||||
label={localize(
|
||||
|
@ -23,6 +23,7 @@ export default class TemplateTrigger extends Component {
|
||||
name="value_template"
|
||||
value={value_template}
|
||||
onvalue-changed={this.onChange}
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
188
src/panels/config/person/dialog-person-detail.ts
Normal file
188
src/panels/config/person/dialog-person-detail.ts
Normal 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);
|
217
src/panels/config/person/ha-config-person.ts
Normal file
217
src/panels/config/person/ha-config-person.ts
Normal 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);
|
23
src/panels/config/person/show-dialog-person-detail.ts
Normal file
23
src/panels/config/person/show-dialog-person-detail.ts
Normal 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,
|
||||
});
|
||||
};
|
@ -30,6 +30,9 @@ class HaUserEditor extends EventsMixin(
|
||||
paper-card:last-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
hass-subpage paper-card:first-of-type {
|
||||
direction: ltr;
|
||||
}
|
||||
</style>
|
||||
|
||||
<hass-subpage header="View user">
|
||||
|
@ -14,11 +14,7 @@ import { Cluster } from "../../../data/zha";
|
||||
import "../../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../../resources/ha-style";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
ZHAClusterSelectedParams,
|
||||
ZHAEntitySelectedParams,
|
||||
ZHANodeSelectedParams,
|
||||
} from "./types";
|
||||
import { ZHAClusterSelectedParams, ZHANodeSelectedParams } from "./types";
|
||||
import "./zha-cluster-attributes";
|
||||
import "./zha-cluster-commands";
|
||||
import "./zha-network";
|
||||
@ -29,14 +25,12 @@ export class HaConfigZha extends LitElement {
|
||||
public isWide?: boolean;
|
||||
private _selectedNode?: HassEntity;
|
||||
private _selectedCluster?: Cluster;
|
||||
private _selectedEntity?: HassEntity;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
isWide: {},
|
||||
_selectedCluster: {},
|
||||
_selectedEntity: {},
|
||||
_selectedNode: {},
|
||||
};
|
||||
}
|
||||
@ -64,7 +58,6 @@ export class HaConfigZha extends LitElement {
|
||||
.hass="${this.hass}"
|
||||
@zha-cluster-selected="${this._onClusterSelected}"
|
||||
@zha-node-selected="${this._onNodeSelected}"
|
||||
@zha-entity-selected="${this._onEntitySelected}"
|
||||
></zha-node>
|
||||
${this._selectedCluster
|
||||
? html`
|
||||
@ -72,7 +65,6 @@ export class HaConfigZha extends LitElement {
|
||||
.isWide="${this.isWide}"
|
||||
.hass="${this.hass}"
|
||||
.selectedNode="${this._selectedNode}"
|
||||
.selectedEntity="${this._selectedEntity}"
|
||||
.selectedCluster="${this._selectedCluster}"
|
||||
></zha-cluster-attributes>
|
||||
|
||||
@ -80,7 +72,6 @@ export class HaConfigZha extends LitElement {
|
||||
.isWide="${this.isWide}"
|
||||
.hass="${this.hass}"
|
||||
.selectedNode="${this._selectedNode}"
|
||||
.selectedEntity="${this._selectedEntity}"
|
||||
.selectedCluster="${this._selectedCluster}"
|
||||
></zha-cluster-commands>
|
||||
`
|
||||
@ -100,13 +91,6 @@ export class HaConfigZha extends LitElement {
|
||||
): void {
|
||||
this._selectedNode = selectedNodeEvent.detail.node;
|
||||
this._selectedCluster = undefined;
|
||||
this._selectedEntity = undefined;
|
||||
}
|
||||
|
||||
private _onEntitySelected(
|
||||
selectedEntityEvent: HASSDomEvent<ZHAEntitySelectedParams>
|
||||
): void {
|
||||
this._selectedEntity = selectedEntityEvent.detail.entity;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { ZHADeviceEntity, Cluster } from "../../../data/zha";
|
||||
|
||||
export interface PickerTarget extends EventTarget {
|
||||
@ -17,7 +16,8 @@ export interface ChangeEvent {
|
||||
}
|
||||
|
||||
export interface SetAttributeServiceData {
|
||||
entity_id: string;
|
||||
ieee: string;
|
||||
endpoint_id: number;
|
||||
cluster_id: number;
|
||||
cluster_type: string;
|
||||
attribute: number;
|
||||
@ -26,17 +26,14 @@ export interface SetAttributeServiceData {
|
||||
}
|
||||
|
||||
export interface IssueCommandServiceData {
|
||||
entity_id: string;
|
||||
ieee: string;
|
||||
endpoint_id: number;
|
||||
cluster_id: number;
|
||||
cluster_type: string;
|
||||
command: number;
|
||||
command_type: string;
|
||||
}
|
||||
|
||||
export interface ZHAEntitySelectedParams {
|
||||
entity: HassEntity;
|
||||
}
|
||||
|
||||
export interface ZHANodeSelectedParams {
|
||||
node: ZHADeviceEntity;
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-service-description";
|
||||
import {
|
||||
@ -19,7 +18,7 @@ import {
|
||||
fetchAttributesForCluster,
|
||||
ReadAttributeServiceData,
|
||||
readAttributeValue,
|
||||
ZHADeviceEntity,
|
||||
ZHADevice,
|
||||
} from "../../../data/zha";
|
||||
import { haStyle } from "../../../resources/ha-style";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@ -34,8 +33,7 @@ export class ZHAClusterAttributes extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public isWide?: boolean;
|
||||
public showHelp: boolean;
|
||||
public selectedNode?: HassEntity;
|
||||
public selectedEntity?: ZHADeviceEntity;
|
||||
public selectedNode?: ZHADevice;
|
||||
public selectedCluster?: Cluster;
|
||||
private _attributes: Attribute[];
|
||||
private _selectedAttributeIndex: number;
|
||||
@ -57,7 +55,6 @@ export class ZHAClusterAttributes extends LitElement {
|
||||
isWide: {},
|
||||
showHelp: {},
|
||||
selectedNode: {},
|
||||
selectedEntity: {},
|
||||
selectedCluster: {},
|
||||
_attributes: {},
|
||||
_selectedAttributeIndex: {},
|
||||
@ -172,49 +169,54 @@ export class ZHAClusterAttributes extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchAttributesForCluster(): Promise<void> {
|
||||
if (this.selectedEntity && this.selectedCluster && this.hass) {
|
||||
if (this.selectedNode && this.selectedCluster && this.hass) {
|
||||
this._attributes = await fetchAttributesForCluster(
|
||||
this.hass,
|
||||
this.selectedEntity!.entity_id,
|
||||
this.selectedEntity!.device_info!.identifiers[0][1],
|
||||
this.selectedNode!.ieee,
|
||||
this.selectedCluster!.endpoint_id,
|
||||
this.selectedCluster!.id,
|
||||
this.selectedCluster!.type
|
||||
);
|
||||
this._attributes.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _computeReadAttributeServiceData():
|
||||
| ReadAttributeServiceData
|
||||
| undefined {
|
||||
if (!this.selectedEntity || !this.selectedCluster || !this.selectedNode) {
|
||||
if (!this.selectedCluster || !this.selectedNode) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
entity_id: this.selectedEntity!.entity_id,
|
||||
ieee: this.selectedNode!.ieee,
|
||||
endpoint_id: this.selectedCluster!.endpoint_id,
|
||||
cluster_id: this.selectedCluster!.id,
|
||||
cluster_type: this.selectedCluster!.type,
|
||||
attribute: this._attributes[this._selectedAttributeIndex].id,
|
||||
manufacturer: this._manufacturerCodeOverride
|
||||
? parseInt(this._manufacturerCodeOverride as string, 10)
|
||||
: this.selectedNode!.attributes.manufacturer_code,
|
||||
: this.selectedNode!.manufacturer_code,
|
||||
};
|
||||
}
|
||||
|
||||
private _computeSetAttributeServiceData():
|
||||
| SetAttributeServiceData
|
||||
| undefined {
|
||||
if (!this.selectedEntity || !this.selectedCluster || !this.selectedNode) {
|
||||
if (!this.selectedCluster || !this.selectedNode) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
entity_id: this.selectedEntity!.entity_id,
|
||||
ieee: this.selectedNode!.ieee,
|
||||
endpoint_id: this.selectedCluster!.endpoint_id,
|
||||
cluster_id: this.selectedCluster!.id,
|
||||
cluster_type: this.selectedCluster!.type,
|
||||
attribute: this._attributes[this._selectedAttributeIndex].id,
|
||||
value: this._attributeValue,
|
||||
manufacturer: this._manufacturerCodeOverride
|
||||
? parseInt(this._manufacturerCodeOverride as string, 10)
|
||||
: this.selectedNode!.attributes.manufacturer_code,
|
||||
: this.selectedNode!.manufacturer_code,
|
||||
};
|
||||
}
|
||||
|
||||
@ -306,8 +308,7 @@ export class ZHAClusterAttributes extends LitElement {
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
`,
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -8,14 +8,13 @@ import {
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-service-description";
|
||||
import {
|
||||
Cluster,
|
||||
Command,
|
||||
fetchCommandsForCluster,
|
||||
ZHADeviceEntity,
|
||||
ZHADevice,
|
||||
} from "../../../data/zha";
|
||||
import { haStyle } from "../../../resources/ha-style";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@ -29,8 +28,7 @@ import {
|
||||
export class ZHAClusterCommands extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public isWide?: boolean;
|
||||
public selectedNode?: HassEntity;
|
||||
public selectedEntity?: ZHADeviceEntity;
|
||||
public selectedNode?: ZHADevice;
|
||||
public selectedCluster?: Cluster;
|
||||
private _showHelp: boolean;
|
||||
private _commands: Command[];
|
||||
@ -50,7 +48,6 @@ export class ZHAClusterCommands extends LitElement {
|
||||
hass: {},
|
||||
isWide: {},
|
||||
selectedNode: {},
|
||||
selectedEntity: {},
|
||||
selectedCluster: {},
|
||||
_showHelp: {},
|
||||
_commands: {},
|
||||
@ -146,25 +143,29 @@ export class ZHAClusterCommands extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchCommandsForCluster(): Promise<void> {
|
||||
if (this.selectedEntity && this.selectedCluster && this.hass) {
|
||||
if (this.selectedNode && this.selectedCluster && this.hass) {
|
||||
this._commands = await fetchCommandsForCluster(
|
||||
this.hass,
|
||||
this.selectedEntity!.entity_id,
|
||||
this.selectedEntity!.device_info!.identifiers[0][1],
|
||||
this.selectedNode!.ieee,
|
||||
this.selectedCluster!.endpoint_id,
|
||||
this.selectedCluster!.id,
|
||||
this.selectedCluster!.type
|
||||
);
|
||||
this._commands.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _computeIssueClusterCommandServiceData():
|
||||
| IssueCommandServiceData
|
||||
| undefined {
|
||||
if (!this.selectedEntity || !this.selectedCluster) {
|
||||
if (!this.selectedNode || !this.selectedCluster) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
entity_id: this.selectedEntity!.entity_id,
|
||||
ieee: this.selectedNode!.ieee,
|
||||
endpoint_id: this.selectedCluster!.endpoint_id,
|
||||
cluster_id: this.selectedCluster!.id,
|
||||
cluster_type: this.selectedCluster!.type,
|
||||
command: this._commands[this._selectedCommandIndex].id,
|
||||
@ -257,8 +258,7 @@ export class ZHAClusterCommands extends LitElement {
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
`,
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -11,11 +11,7 @@ import "@polymer/paper-card/paper-card";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-service-description";
|
||||
import {
|
||||
Cluster,
|
||||
fetchClustersForZhaNode,
|
||||
ZHADeviceEntity,
|
||||
} from "../../../data/zha";
|
||||
import { Cluster, fetchClustersForZhaNode, ZHADevice } from "../../../data/zha";
|
||||
import { haStyle } from "../../../resources/ha-style";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
@ -31,14 +27,16 @@ declare global {
|
||||
}
|
||||
|
||||
const computeClusterKey = (cluster: Cluster): string => {
|
||||
return `${cluster.name} (id: ${cluster.id}, type: ${cluster.type})`;
|
||||
return `${cluster.name} (Endpoint id: ${cluster.endpoint_id}, Id: ${
|
||||
cluster.id
|
||||
}, Type: ${cluster.type})`;
|
||||
};
|
||||
|
||||
export class ZHAClusters extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public isWide?: boolean;
|
||||
public showHelp: boolean;
|
||||
public selectedEntity?: ZHADeviceEntity;
|
||||
public selectedDevice?: ZHADevice;
|
||||
private _selectedClusterIndex: number;
|
||||
private _clusters: Cluster[];
|
||||
|
||||
@ -54,14 +52,14 @@ export class ZHAClusters extends LitElement {
|
||||
hass: {},
|
||||
isWide: {},
|
||||
showHelp: {},
|
||||
selectedEntity: {},
|
||||
selectedDevice: {},
|
||||
_selectedClusterIndex: {},
|
||||
_clusters: {},
|
||||
};
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("selectedEntity")) {
|
||||
if (changedProperties.has("selectedDevice")) {
|
||||
this._clusters = [];
|
||||
this._selectedClusterIndex = -1;
|
||||
fireEvent(this, "zha-cluster-selected", {
|
||||
@ -103,9 +101,11 @@ export class ZHAClusters extends LitElement {
|
||||
if (this.hass) {
|
||||
this._clusters = await fetchClustersForZhaNode(
|
||||
this.hass,
|
||||
this.selectedEntity!.entity_id,
|
||||
this.selectedEntity!.device_info!.identifiers[0][1]
|
||||
this.selectedDevice!.ieee
|
||||
);
|
||||
this._clusters.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
118
src/panels/config/zha/zha-device-card.ts
Normal file
118
src/panels/config/zha/zha-device-card.ts
Normal 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);
|
@ -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);
|
@ -102,8 +102,7 @@ export class ZHANetwork extends LitElement {
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
`,
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
PropertyDeclarations,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
PropertyValues,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
@ -11,29 +12,22 @@ import "@polymer/paper-card/paper-card";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import sortByName from "../../../common/entity/states_sort_by_name";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-service-description";
|
||||
import { haStyle } from "../../../resources/ha-style";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import {
|
||||
ItemSelectedEvent,
|
||||
NodeServiceData,
|
||||
ZHAEntitySelectedParams,
|
||||
} from "./types";
|
||||
import { ItemSelectedEvent, NodeServiceData } from "./types";
|
||||
import "./zha-clusters";
|
||||
import "./zha-entities";
|
||||
import { reconfigureNode } from "../../../data/zha";
|
||||
import "./zha-device-card";
|
||||
import { reconfigureNode, fetchDevices, ZHADevice } from "../../../data/zha";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"zha-node-selected": {
|
||||
node?: HassEntity;
|
||||
node?: ZHADevice;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -43,10 +37,9 @@ export class ZHANode extends LitElement {
|
||||
public isWide?: boolean;
|
||||
private _showHelp: boolean;
|
||||
private _selectedNodeIndex: number;
|
||||
private _selectedNode?: HassEntity;
|
||||
private _selectedEntity?: HassEntity;
|
||||
private _selectedNode?: ZHADevice;
|
||||
private _serviceData?: {};
|
||||
private _nodes: HassEntity[];
|
||||
private _nodes: ZHADevice[];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -62,13 +55,31 @@ export class ZHANode extends LitElement {
|
||||
_showHelp: {},
|
||||
_selectedNodeIndex: {},
|
||||
_selectedNode: {},
|
||||
_entities: {},
|
||||
_serviceData: {},
|
||||
_selectedEntity: {},
|
||||
_nodes: {},
|
||||
};
|
||||
}
|
||||
|
||||
public firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this._nodes.length === 0) {
|
||||
this._fetchDevices();
|
||||
}
|
||||
this.addEventListener("hass-service-called", (ev) =>
|
||||
this.serviceCalled(ev)
|
||||
);
|
||||
}
|
||||
|
||||
protected serviceCalled(ev): void {
|
||||
// Check if this is for us
|
||||
if (ev.detail.success && ev.detail.service === "remove") {
|
||||
this._selectedNodeIndex = -1;
|
||||
this._fetchDevices();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
this._nodes = this._computeNodes(this.hass);
|
||||
return html`
|
||||
<ha-config-section .isWide="${this.isWide}">
|
||||
<div class="sectionHeader" slot="header">
|
||||
@ -94,12 +105,11 @@ export class ZHANode extends LitElement {
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
@iron-select="${this._selectedNodeChanged}"
|
||||
.selected="${this._selectedNodeIndex}"
|
||||
>
|
||||
${this._nodes.map(
|
||||
(entry) => html`
|
||||
<paper-item
|
||||
>${this._computeSelectCaption(entry)}</paper-item
|
||||
>
|
||||
<paper-item>${entry.name}</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
@ -112,9 +122,18 @@ export class ZHANode extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._selectedNodeIndex !== -1
|
||||
? html`
|
||||
<zha-device-card
|
||||
class="card"
|
||||
.hass="${this.hass}"
|
||||
.device="${this._selectedNode}"
|
||||
.narrow="${!this.isWide}"
|
||||
></zha-device-card>
|
||||
`
|
||||
: ""}
|
||||
${this._selectedNodeIndex !== -1 ? this._renderNodeActions() : ""}
|
||||
${this._selectedNodeIndex !== -1 ? this._renderEntities() : ""}
|
||||
${this._selectedEntity ? this._renderClusters() : ""}
|
||||
${this._selectedNode ? this._renderClusters() : ""}
|
||||
</paper-card>
|
||||
</ha-config-section>
|
||||
`;
|
||||
@ -123,9 +142,6 @@ export class ZHANode extends LitElement {
|
||||
private _renderNodeActions(): TemplateResult {
|
||||
return html`
|
||||
<div class="card-actions">
|
||||
<paper-button @click="${this._showNodeInformation}"
|
||||
>Node Information</paper-button
|
||||
>
|
||||
<paper-button @click="${this._onReconfigureNodeClick}"
|
||||
>Reconfigure Node</paper-button
|
||||
>
|
||||
@ -158,22 +174,11 @@ export class ZHANode extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEntities(): TemplateResult {
|
||||
return html`
|
||||
<zha-entities
|
||||
.hass="${this.hass}"
|
||||
.selectedNode="${this._selectedNode}"
|
||||
.showHelp="${this._showHelp}"
|
||||
@zha-entity-selected="${this._onEntitySelected}"
|
||||
></zha-entities>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderClusters(): TemplateResult {
|
||||
return html`
|
||||
<zha-clusters
|
||||
.hass="${this.hass}"
|
||||
.selectedEntity="${this._selectedEntity}"
|
||||
.selectedDevice="${this._selectedNode}"
|
||||
.showHelp="${this._showHelp}"
|
||||
></zha-clusters>
|
||||
`;
|
||||
@ -186,50 +191,26 @@ export class ZHANode extends LitElement {
|
||||
private _selectedNodeChanged(event: ItemSelectedEvent): void {
|
||||
this._selectedNodeIndex = event!.target!.selected;
|
||||
this._selectedNode = this._nodes[this._selectedNodeIndex];
|
||||
this._selectedEntity = undefined;
|
||||
fireEvent(this, "zha-node-selected", { node: this._selectedNode });
|
||||
this._serviceData = this._computeNodeServiceData();
|
||||
}
|
||||
|
||||
private async _onReconfigureNodeClick(): Promise<void> {
|
||||
if (this.hass) {
|
||||
await reconfigureNode(this.hass, this._selectedNode!.attributes.ieee);
|
||||
await reconfigureNode(this.hass, this._selectedNode!.ieee);
|
||||
}
|
||||
}
|
||||
|
||||
private _showNodeInformation(): void {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: this._selectedNode!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _computeNodeServiceData(): NodeServiceData {
|
||||
return {
|
||||
ieee_address: this._selectedNode!.attributes.ieee,
|
||||
ieee_address: this._selectedNode!.ieee,
|
||||
};
|
||||
}
|
||||
|
||||
private _computeSelectCaption(stateObj: HassEntity): string {
|
||||
return (
|
||||
computeStateName(stateObj) + " (Node:" + stateObj.attributes.ieee + ")"
|
||||
);
|
||||
}
|
||||
|
||||
private _computeNodes(hass?: HomeAssistant): HassEntity[] {
|
||||
if (hass) {
|
||||
return Object.keys(hass.states)
|
||||
.map((key) => hass.states[key])
|
||||
.filter((ent) => ent.entity_id.match("zha[.]"))
|
||||
.sort(sortByName);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private _onEntitySelected(
|
||||
entitySelectedEvent: HASSDomEvent<ZHAEntitySelectedParams>
|
||||
): void {
|
||||
this._selectedEntity = entitySelectedEvent.detail.entity;
|
||||
private async _fetchDevices() {
|
||||
this._nodes = (await fetchDevices(this.hass!)).sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
@ -287,6 +268,17 @@ export class ZHANode extends LitElement {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1 0 300px;
|
||||
min-width: 0;
|
||||
max-width: 600px;
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
ha-service-description {
|
||||
display: block;
|
||||
color: grey;
|
||||
|
@ -2,8 +2,10 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
PropertyDeclarations,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
css,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
@ -50,20 +52,22 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
return { states: ["arm_home", "arm_away"] };
|
||||
}
|
||||
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
private _code?: string;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
_code: {},
|
||||
};
|
||||
}
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() private _config?: Config;
|
||||
@property() private _code?: string;
|
||||
|
||||
public getCardSize(): number {
|
||||
return 4;
|
||||
if (!this._config || !this.hass) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
|
||||
if (!stateObj) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return stateObj.attributes.code_format !== FORMAT_NUMBER ? 3 : 8;
|
||||
}
|
||||
|
||||
public setConfig(config: Config): void {
|
||||
@ -114,7 +118,6 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card .header="${this._config.name || this._label(stateObj.state)}">
|
||||
<ha-label-badge
|
||||
class="${classMap({ [stateObj.state]: true })}"
|
||||
@ -204,9 +207,9 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
this._code = "";
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
ha-card {
|
||||
padding-bottom: 16px;
|
||||
position: relative;
|
||||
@ -293,8 +296,8 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
paper-button#disarm {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,8 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
PropertyDeclarations,
|
||||
TemplateResult,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
|
||||
@ -53,20 +53,11 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
return {};
|
||||
}
|
||||
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() private _config?: Config;
|
||||
@property() private _roundSliderStyle?: TemplateResult;
|
||||
@property() private _jQuery?: any;
|
||||
private _brightnessTimout?: number;
|
||||
private _roundSliderStyle?: TemplateResult;
|
||||
private _jQuery?: any;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
roundSliderStyle: {},
|
||||
_jQuery: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 2;
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { html, LitElement, TemplateResult } from "lit-element";
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
CSSResult,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { repeat } from "lit-html/directives/repeat";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
@ -26,25 +33,17 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
|
||||
await import(/* webpackChunkName: "hui-shopping-list-editor" */ "../editor/config-elements/hui-shopping-list-editor");
|
||||
return document.createElement("hui-shopping-list-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): object {
|
||||
return {};
|
||||
}
|
||||
|
||||
public hass?: HomeAssistant;
|
||||
private _config?: Config;
|
||||
private _uncheckedItems?: ShoppingListItem[];
|
||||
private _checkedItems?: ShoppingListItem[];
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() private _config?: Config;
|
||||
@property() private _uncheckedItems?: ShoppingListItem[];
|
||||
@property() private _checkedItems?: ShoppingListItem[];
|
||||
private _unsubEvents?: Promise<() => Promise<void>>;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
_uncheckedItems: {},
|
||||
_checkedItems: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return (this._config ? (this._config.title ? 1 : 0) : 0) + 3;
|
||||
}
|
||||
@ -82,7 +81,6 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card .header="${this._config.title}">
|
||||
<div class="addRow">
|
||||
<ha-icon
|
||||
@ -180,9 +178,9 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStyle(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
.editRow,
|
||||
.addRow {
|
||||
display: flex;
|
||||
@ -192,6 +190,9 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
|
||||
padding: 9px 15px 11px 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
paper-item-body {
|
||||
width: 75%;
|
||||
}
|
||||
paper-checkbox {
|
||||
padding: 11px 11px 11px 18px;
|
||||
}
|
||||
@ -230,8 +231,8 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
|
||||
.addRow > ha-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private async _fetchData(): Promise<void> {
|
||||
|
@ -46,8 +46,9 @@ const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
input_select: "input-select",
|
||||
input_text: "input-text",
|
||||
light: "toggle",
|
||||
media_player: "media-player",
|
||||
lock: "lock",
|
||||
media_player: "media-player",
|
||||
remote: "toggle",
|
||||
scene: "scene",
|
||||
script: "script",
|
||||
sensor: "sensor",
|
||||
|
@ -98,7 +98,11 @@ export class HuiCardOptions extends LitElement {
|
||||
slot="dropdown-trigger"
|
||||
></paper-icon-button>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
<paper-item @click="${this._moveCard}">Move Card</paper-item>
|
||||
<paper-item @click="${this._moveCard}"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.move"
|
||||
)}</paper-item
|
||||
>
|
||||
<paper-item @click="${this._deleteCard}"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.delete"
|
||||
|
@ -5,25 +5,20 @@ import "../../../components/ha-icon";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import {
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
PropertyValues,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EntitiesCardEntityConfig } from "../cards/hui-entities-card";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
|
||||
class HuiGenericEntityRow extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public config?: EntitiesCardEntityConfig;
|
||||
public showSecondary: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.showSecondary = true;
|
||||
}
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public config?: EntitiesCardEntityConfig;
|
||||
@property() public showSecondary: boolean = true;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.config) {
|
||||
@ -70,14 +65,6 @@ class HuiGenericEntityRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
config: {},
|
||||
showSecondary: {},
|
||||
};
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("hass")) {
|
||||
|
@ -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);
|
226
src/panels/lovelace/components/hui-image.ts
Normal file
226
src/panels/lovelace/components/hui-image.ts
Normal 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);
|
@ -3,44 +3,67 @@ import CodeMirror from "codemirror";
|
||||
import "codemirror/mode/yaml/yaml";
|
||||
// @ts-ignore
|
||||
import codeMirrorCSS from "codemirror/lib/codemirror.css";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"yaml-changed": {
|
||||
value: string;
|
||||
};
|
||||
"yaml-save": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class HuiYamlEditor extends HTMLElement {
|
||||
public _hass?: HomeAssistant;
|
||||
public codemirror: CodeMirror;
|
||||
private _value: string;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
CodeMirror.commands.save = (cm: CodeMirror) => {
|
||||
fireEvent(cm.getWrapperElement(), "yaml-save");
|
||||
};
|
||||
this._value = "";
|
||||
const shadowRoot = this.attachShadow({ mode: "open" });
|
||||
shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${codeMirrorCSS}
|
||||
.CodeMirror {
|
||||
height: var(--code-mirror-height, 300px);
|
||||
direction: var(--code-mirror-direction, ltr);
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
|
||||
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
|
||||
transition: 0.2s ease border-right;
|
||||
}
|
||||
.CodeMirror-focused .CodeMirror-gutters {
|
||||
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));;
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
color: var(--paper-dialog-color, var(--primary-text-color));
|
||||
}
|
||||
${codeMirrorCSS}
|
||||
.CodeMirror {
|
||||
height: var(--code-mirror-height, 300px);
|
||||
direction: var(--code-mirror-direction, ltr);
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
|
||||
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
|
||||
transition: 0.2s ease border-right;
|
||||
}
|
||||
.CodeMirror-focused .CodeMirror-gutters {
|
||||
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));;
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
color: var(--paper-dialog-color, var(--primary-text-color));
|
||||
}
|
||||
|
||||
.rtl .CodeMirror-vscrollbar {
|
||||
right: auto;
|
||||
left: 0px;
|
||||
}
|
||||
.rtl-gutter {
|
||||
width: 20px;
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
if (this._hass) {
|
||||
this.setScrollBarDirection();
|
||||
}
|
||||
}
|
||||
|
||||
set value(value: string) {
|
||||
if (this.codemirror) {
|
||||
if (value !== this.codemirror.getValue()) {
|
||||
@ -72,7 +95,12 @@ export class HuiYamlEditor extends HTMLElement {
|
||||
cm.replaceSelection(spaces);
|
||||
},
|
||||
},
|
||||
gutters:
|
||||
this._hass && computeRTL(this._hass!)
|
||||
? ["rtl-gutter", "CodeMirror-linenumbers"]
|
||||
: [],
|
||||
});
|
||||
this.setScrollBarDirection();
|
||||
this.codemirror.on("changes", () => this._onChange());
|
||||
} else {
|
||||
this.codemirror.refresh();
|
||||
@ -82,6 +110,16 @@ export class HuiYamlEditor extends HTMLElement {
|
||||
private _onChange(): void {
|
||||
fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() });
|
||||
}
|
||||
|
||||
private setScrollBarDirection() {
|
||||
if (!this.codemirror) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.codemirror
|
||||
.getWrapperElement()
|
||||
.classList.toggle("rtl", computeRTL(this._hass!));
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -119,8 +119,10 @@ export class HuiEditCard extends LitElement {
|
||||
? this._configElement
|
||||
: html`
|
||||
<hui-yaml-editor
|
||||
.hass="${this.hass}"
|
||||
.value="${this._configValue!.value}"
|
||||
@yaml-changed="${this._handleYamlChanged}"
|
||||
@yaml-save="${this._save}"
|
||||
></hui-yaml-editor>
|
||||
`}
|
||||
</div>
|
||||
|
@ -57,7 +57,7 @@ export class HuiAlarmPanelCardEditor extends LitElement
|
||||
return html``;
|
||||
}
|
||||
|
||||
const states = ["arm_home", "arm_away", "arm_night", "arm_custom_bypass"];
|
||||
const states = ["arm_home", "arm_away", "arm_night", "armed_custom_bypass"];
|
||||
|
||||
return html`
|
||||
${configElementStyle} ${this.renderStyle()}
|
||||
|
@ -95,6 +95,7 @@ class LovelacePanel extends LitElement {
|
||||
if (state === "yaml-editor") {
|
||||
return html`
|
||||
<hui-editor
|
||||
.hass="${this.hass}"
|
||||
.lovelace="${this.lovelace}"
|
||||
.closeEditor="${this._closeEditor}"
|
||||
></hui-editor>
|
||||
|
@ -18,6 +18,7 @@ import "./components/hui-yaml-editor";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { HuiYamlEditor } from "./components/hui-yaml-editor";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
const lovelaceStruct = struct.interface({
|
||||
title: "string?",
|
||||
@ -26,6 +27,7 @@ const lovelaceStruct = struct.interface({
|
||||
});
|
||||
|
||||
class LovelaceFullConfigEditor extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public lovelace?: Lovelace;
|
||||
public closeEditor?: () => void;
|
||||
private _saving?: boolean;
|
||||
@ -34,6 +36,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {},
|
||||
lovelace: {},
|
||||
_saving: {},
|
||||
_changed: {},
|
||||
@ -61,7 +64,11 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="content">
|
||||
<hui-yaml-editor @yaml-changed="${this._yamlChanged}">
|
||||
<hui-yaml-editor
|
||||
.hass="${this.hass}"
|
||||
@yaml-changed="${this._yamlChanged}"
|
||||
@yaml-save="${this._handleSave}"
|
||||
>
|
||||
</hui-yaml-editor>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
@ -99,7 +106,6 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
|
||||
.content {
|
||||
height: calc(100vh - 68px);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
hui-code-editor {
|
||||
|
@ -49,6 +49,7 @@ import { showEditLovelaceDialog } from "./editor/lovelace-editor/show-edit-lovel
|
||||
import { Lovelace } from "./types";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { haStyle } from "../../resources/ha-style";
|
||||
import { computeRTL, computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
|
||||
// CSS and JS should only be imported once. Modules and HTML are safe.
|
||||
const CSS_CACHE = {};
|
||||
@ -243,6 +244,7 @@ class HUIRoot extends LitElement {
|
||||
scrollable
|
||||
.selected="${this._curView}"
|
||||
@iron-activate="${this._handleViewSelected}"
|
||||
dir="${computeRTLDirection(this.hass!)}"
|
||||
>
|
||||
${this.lovelace!.config.views.map(
|
||||
(view) => html`
|
||||
@ -252,7 +254,9 @@ class HUIRoot extends LitElement {
|
||||
<paper-icon-button
|
||||
title="Move view left"
|
||||
class="edit-icon view"
|
||||
icon="hass:arrow-left"
|
||||
icon="${computeRTL(this.hass!)
|
||||
? "hass:arrow-right"
|
||||
: "hass:arrow-left"}"
|
||||
@click="${this._moveViewLeft}"
|
||||
?disabled="${this._curView === 0}"
|
||||
></paper-icon-button>
|
||||
@ -277,7 +281,9 @@ class HUIRoot extends LitElement {
|
||||
<paper-icon-button
|
||||
title="Move view right"
|
||||
class="edit-icon view"
|
||||
icon="hass:arrow-right"
|
||||
icon="${computeRTL(this.hass!)
|
||||
? "hass:arrow-left"
|
||||
: "hass:arrow-right"}"
|
||||
@click="${this._moveViewRight}"
|
||||
?disabled="${(this._curView! as number) +
|
||||
1 ===
|
||||
|
@ -12,11 +12,12 @@ import { createCardElement } from "./common/create-card-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { LovelaceCard } from "./types";
|
||||
import { LovelaceConfig } from "../../data/lovelace";
|
||||
import computeDomain from "../../common/entity/compute_domain";
|
||||
|
||||
export class HuiUnusedEntities extends LitElement {
|
||||
private _hass?: HomeAssistant;
|
||||
private _config?: LovelaceConfig;
|
||||
private _element?: LovelaceCard;
|
||||
private _elements?: LovelaceCard[];
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
@ -27,16 +28,18 @@ export class HuiUnusedEntities extends LitElement {
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
if (!this._element) {
|
||||
this._createElement();
|
||||
if (!this._elements) {
|
||||
this._createElements();
|
||||
return;
|
||||
}
|
||||
this._element.hass = this._hass;
|
||||
for (const element of this._elements) {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceConfig): void {
|
||||
this._config = config;
|
||||
this._createElement();
|
||||
this._createElements();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
@ -46,7 +49,7 @@ export class HuiUnusedEntities extends LitElement {
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<div id="root">${this._element}</div>
|
||||
<div id="root">${this._elements}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -54,30 +57,47 @@ export class HuiUnusedEntities extends LitElement {
|
||||
return html`
|
||||
<style>
|
||||
#root {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 8px 0;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
hui-entities-card {
|
||||
max-width: 400px;
|
||||
padding: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
private _createElement(): void {
|
||||
if (this._hass) {
|
||||
const entities = computeUnusedEntities(this._hass, this._config!).map(
|
||||
(entity) => ({
|
||||
entity,
|
||||
secondary_info: "entity-id",
|
||||
})
|
||||
);
|
||||
this._element = createCardElement({
|
||||
type: "entities",
|
||||
title: "Unused entities",
|
||||
entities,
|
||||
show_header_toggle: false,
|
||||
});
|
||||
this._element!.hass = this._hass;
|
||||
private _createElements(): void {
|
||||
if (!this._hass) {
|
||||
return;
|
||||
}
|
||||
const domains: { [domain: string]: string[] } = {};
|
||||
computeUnusedEntities(this._hass, this._config!).forEach((entity) => {
|
||||
const domain = computeDomain(entity);
|
||||
|
||||
if (!(domain in domains)) {
|
||||
domains[domain] = [];
|
||||
}
|
||||
domains[domain].push(entity);
|
||||
});
|
||||
this._elements = Object.keys(domains)
|
||||
.sort()
|
||||
.map((domain) => {
|
||||
const el = createCardElement({
|
||||
type: "entities",
|
||||
title: this._hass!.localize(`domain.${domain}`) || domain,
|
||||
entities: domains[domain].map((entity) => ({
|
||||
entity,
|
||||
secondary_info: "entity-id",
|
||||
})),
|
||||
show_header_toggle: false,
|
||||
});
|
||||
el.hass = this._hass;
|
||||
return el;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -530,7 +530,18 @@
|
||||
"introduction": "Here it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.",
|
||||
"area_registry": {
|
||||
"caption": "Area Registry",
|
||||
"description": "Overview of all areas in your home."
|
||||
"description": "Overview of all areas in your home.",
|
||||
"picker": {
|
||||
"header": "Area Registry"
|
||||
},
|
||||
"no_areas": "Looks like you have no areas yet!",
|
||||
"create_area": "CREATE AREA",
|
||||
"editor": {
|
||||
"default_name": "New Area",
|
||||
"delete": "DELETE",
|
||||
"update": "UPDATE",
|
||||
"create": "CREATE"
|
||||
}
|
||||
},
|
||||
"core": {
|
||||
"caption": "General",
|
||||
@ -565,7 +576,11 @@
|
||||
},
|
||||
"customize": {
|
||||
"caption": "Customization",
|
||||
"description": "Customize your entities"
|
||||
"description": "Customize your entities",
|
||||
"picker": {
|
||||
"header": "Customization",
|
||||
"introduction": "Tweak per-entity attributes. Added/edited customizations will take effect immediately. Removed customizations will take effect when the entity is updated."
|
||||
}
|
||||
},
|
||||
"automation": {
|
||||
"caption": "Automation",
|
||||
@ -755,7 +770,27 @@
|
||||
},
|
||||
"entity_registry": {
|
||||
"caption": "Entity Registry",
|
||||
"description": "Overview of all known entities."
|
||||
"description": "Overview of all known entities.",
|
||||
"picker": {
|
||||
"header": "Entity Registry",
|
||||
"unavailable": "(unavailable)"
|
||||
},
|
||||
"editor": {
|
||||
"unavailable": "This entity is not currently available.",
|
||||
"default_name": "New Area",
|
||||
"delete": "DELETE",
|
||||
"update": "UPDATE"
|
||||
}
|
||||
},
|
||||
"person": {
|
||||
"caption": "Persons",
|
||||
"description": "Manage the persons that Home Assistant tracks.",
|
||||
"detail": {
|
||||
"name": "Name",
|
||||
"device_tracker_intro": "Select the devices that belong to this person.",
|
||||
"device_tracker_picked": "Track Device",
|
||||
"device_tracker_pick": "Pick device to track"
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"caption": "Integrations",
|
||||
@ -774,7 +809,8 @@
|
||||
"hub": "Connected via",
|
||||
"firmware": "Firmware: {version}",
|
||||
"device_unavailable": "device unavailable",
|
||||
"entity_unavailable": "entity unavailable"
|
||||
"entity_unavailable": "entity unavailable",
|
||||
"no_area": "No Area"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
@ -847,7 +883,8 @@
|
||||
"toggle_editor": "Toggle Editor",
|
||||
"add": "Add Card",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete"
|
||||
"delete": "Delete",
|
||||
"move": "Move"
|
||||
},
|
||||
"save_config": {
|
||||
"header": "Take control of your Lovelace UI",
|
||||
@ -956,6 +993,29 @@
|
||||
"working": "Please wait",
|
||||
"unknown_error": "Something went wrong",
|
||||
"providers": {
|
||||
"command_line": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"username": "[%key:ui::panel::page-authorize::form::providers::homeassistant::step::init::data::username%]",
|
||||
"password": "[%key:ui::panel::page-authorize::form::providers::homeassistant::step::init::data::password%]"
|
||||
}
|
||||
},
|
||||
"mfa": {
|
||||
"data": {
|
||||
"code": "[%key:ui::panel::page-authorize::form::providers::homeassistant::step::mfa::data::code%]"
|
||||
},
|
||||
"description": "[%key:ui::panel::page-authorize::form::providers::homeassistant::step::mfa::description%]"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:ui::panel::page-authorize::form::providers::homeassistant::error::invalid_auth%]",
|
||||
"invalid_code": "[%key:ui::panel::page-authorize::form::providers::homeassistant::error::invalid_code%]"
|
||||
},
|
||||
"abort": {
|
||||
"login_expired": "[%key:ui::panel::page-authorize::form::providers::homeassistant::abort::login_expired%]"
|
||||
}
|
||||
},
|
||||
"homeassistant": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
@ -29,6 +29,13 @@ declare global {
|
||||
getComputedStyleValue(element, propertyName);
|
||||
};
|
||||
}
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"value-changed": {
|
||||
value: unknown;
|
||||
};
|
||||
change: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface WebhookError {
|
||||
|
@ -28,7 +28,7 @@
|
||||
"disarmed": "Desactivada",
|
||||
"armed_home": "Activada, mode a casa",
|
||||
"armed_away": "Activada, mode fora",
|
||||
"armed_night": "Activada, mode nit",
|
||||
"armed_night": "Activada, mode nocturn",
|
||||
"pending": "Pendent",
|
||||
"arming": "Activant",
|
||||
"disarming": "Desactivant",
|
||||
@ -301,7 +301,8 @@
|
||||
"period": "Període"
|
||||
},
|
||||
"logbook": {
|
||||
"showing_entries": "Mostrant entrades de"
|
||||
"showing_entries": "Mostrant entrades de",
|
||||
"period": "Període"
|
||||
},
|
||||
"mailbox": {
|
||||
"empty": "No tens missatges",
|
||||
@ -789,10 +790,16 @@
|
||||
"para_sure": "Estàs segur que vols prendre el control de la interfície d'usuari?",
|
||||
"cancel": "M'ho he repensat",
|
||||
"save": "Prendre el control"
|
||||
},
|
||||
"menu": {
|
||||
"raw_editor": "Editor de codi"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"configure_ui": "Configurar la interfície d'usuari"
|
||||
"configure_ui": "Configurar la interfície d'usuari",
|
||||
"unused_entities": "Entitats sense utilitzar",
|
||||
"help": "Ajuda",
|
||||
"refresh": "Actualitzar"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -864,7 +871,7 @@
|
||||
"disarm": "Desactivar",
|
||||
"arm_home": "Activar, a casa",
|
||||
"arm_away": "Activar, fora",
|
||||
"arm_night": "Activar, nit",
|
||||
"arm_night": "Activar, nocturn",
|
||||
"armed_custom_bypass": "Bypass personalitzat"
|
||||
},
|
||||
"automation": {
|
||||
@ -1028,7 +1035,8 @@
|
||||
"zha": "ZHA",
|
||||
"hassio": "Hass.io",
|
||||
"homeassistant": "Home Assistant",
|
||||
"lovelace": "Lovelace"
|
||||
"lovelace": "Lovelace",
|
||||
"system_health": "Estat del sistema"
|
||||
},
|
||||
"attribute": {
|
||||
"weather": {
|
||||
|
@ -427,6 +427,10 @@
|
||||
"webhook": {
|
||||
"label": "Webhook",
|
||||
"webhook_id": "Webhook ID"
|
||||
},
|
||||
"geo_location": {
|
||||
"label": "Geolokace",
|
||||
"zone": "Zóna"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -556,6 +560,12 @@
|
||||
"device_unavailable": "zařízení není k dispozici",
|
||||
"entity_unavailable": "entita není k dispozici"
|
||||
}
|
||||
},
|
||||
"zha": {
|
||||
"caption": "ZHA"
|
||||
},
|
||||
"area_registry": {
|
||||
"description": "Přehled všech oblastí ve vaší domácnosti."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@ -758,7 +768,9 @@
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"configure_ui": "Konfigurovat UI"
|
||||
"configure_ui": "Konfigurovat UI",
|
||||
"help": "Pomoc",
|
||||
"refresh": "Obnovit"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -928,6 +940,18 @@
|
||||
"save": "Uložit",
|
||||
"name": "Název",
|
||||
"entity_id": "Entity ID"
|
||||
},
|
||||
"more_info_control": {
|
||||
"script": {
|
||||
"last_action": "Poslední akce"
|
||||
},
|
||||
"sun": {
|
||||
"rising": "Vychází",
|
||||
"setting": "Zapadá"
|
||||
},
|
||||
"updater": {
|
||||
"title": "Pokyny pro aktualizaci"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth_store": {
|
||||
@ -978,7 +1002,10 @@
|
||||
"weblink": "Webový odkaz",
|
||||
"zwave": "Z-Wave",
|
||||
"vacuum": "Vysavač",
|
||||
"zha": "ZHA"
|
||||
"zha": "ZHA",
|
||||
"hassio": "Hass.io",
|
||||
"homeassistant": "Home Assistant",
|
||||
"lovelace": "Lovelace"
|
||||
},
|
||||
"attribute": {
|
||||
"weather": {
|
||||
|
@ -301,7 +301,8 @@
|
||||
"period": "Periode"
|
||||
},
|
||||
"logbook": {
|
||||
"showing_entries": "Viser emner for"
|
||||
"showing_entries": "Viser emner for",
|
||||
"period": "Periode"
|
||||
},
|
||||
"mailbox": {
|
||||
"empty": "Du har ingen beskeder",
|
||||
@ -420,13 +421,27 @@
|
||||
"label": "Zone",
|
||||
"entity": "Enhed med placering",
|
||||
"zone": "Zone",
|
||||
"event": "Begivenhed",
|
||||
"event": "Begivenhed:",
|
||||
"enter": "Ankom",
|
||||
"leave": "Forlade"
|
||||
},
|
||||
"webhook": {
|
||||
"label": "Webhook",
|
||||
"webhook_id": "Webhook-ID"
|
||||
},
|
||||
"time_pattern": {
|
||||
"label": "Tidsmønster",
|
||||
"hours": "Timer",
|
||||
"minutes": "Minutter",
|
||||
"seconds": "Sekunder"
|
||||
},
|
||||
"geo_location": {
|
||||
"label": "Geolokation",
|
||||
"source": "Kilde",
|
||||
"zone": "Zone",
|
||||
"event": "Begivenhed",
|
||||
"enter": "Ankommer",
|
||||
"leave": "Forlad"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -437,7 +452,7 @@
|
||||
"duplicate": "Kopier",
|
||||
"delete": "Slet",
|
||||
"delete_confirm": "Er du sikker på du vil slette?",
|
||||
"unsupported_condition": "Ikke-understøttet betingelse: {betingelse}",
|
||||
"unsupported_condition": "Ikke-understøttet betingelse: {condition}",
|
||||
"type_select": "Betingelsestype",
|
||||
"type": {
|
||||
"state": {
|
||||
@ -448,7 +463,7 @@
|
||||
"label": "Numerisk stadie",
|
||||
"above": "Over",
|
||||
"below": "Under",
|
||||
"value_template": "Værdi skabelon (ikke krævet)"
|
||||
"value_template": "Værdi skabelon (valgfri)"
|
||||
},
|
||||
"sun": {
|
||||
"label": "Sol",
|
||||
@ -556,12 +571,27 @@
|
||||
"device_unavailable": "enhed utilgængelig",
|
||||
"entity_unavailable": "entitet utilgængelig"
|
||||
}
|
||||
},
|
||||
"zha": {
|
||||
"caption": "ZHA",
|
||||
"description": "Zigbee Home Automation opsætning",
|
||||
"services": {
|
||||
"reconfigure": "Genkonfigurer ZHA-enhed (helbred enhed). Brug dette hvis du har problemer med enheden. Hvis den pågældende enhed er en batteridrevet enhed skal du sørge for at den er vågen og accepterer kommandoer når du bruger denne service."
|
||||
}
|
||||
},
|
||||
"area_registry": {
|
||||
"caption": "Område registrering",
|
||||
"description": "Oversigt over alle områder i dit hjem."
|
||||
},
|
||||
"entity_registry": {
|
||||
"caption": "Enheds registrering",
|
||||
"description": "Oversigt over alle kendte enheder."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"push_notifications": {
|
||||
"header": "Push notifikationer",
|
||||
"description": "Send meddelelser til denne enhed",
|
||||
"description": "Send meddelelser til denne enhed.",
|
||||
"error_load_platform": "Konfigurer notify.html5",
|
||||
"error_use_https": "Kræver SSL aktiveret til frontend.",
|
||||
"push_notifications": "Push-meddelelser",
|
||||
@ -724,6 +754,11 @@
|
||||
"checked_items": "Markerede elementer",
|
||||
"clear_items": "Ryd markerede elementer",
|
||||
"add_item": "Tilføj element"
|
||||
},
|
||||
"empty_state": {
|
||||
"title": "Velkommen hjem",
|
||||
"no_devices": "Denne side giver dig mulighed for at styre dine enheder, men det ser ud til, at du endnu ikke har konfigureret enheder. Gå til integrationssiden for at komme i gang.",
|
||||
"go_to_integrations_page": "Gå til integrationssiden."
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -755,10 +790,16 @@
|
||||
"para_sure": "Er du sikker på du ønsker at tage kontrol over din brugergrænseflade?",
|
||||
"cancel": "Glem det",
|
||||
"save": "tag kontrol"
|
||||
},
|
||||
"menu": {
|
||||
"raw_editor": "Tekstbaseret redigering"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"configure_ui": "Konfigurer UI"
|
||||
"configure_ui": "Konfigurer UI",
|
||||
"unused_entities": "Ubrugte enheder",
|
||||
"help": "Hjælp",
|
||||
"refresh": "Opdater"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -928,6 +969,19 @@
|
||||
"save": "Gem",
|
||||
"name": "Navn",
|
||||
"entity_id": "Enheds ID"
|
||||
},
|
||||
"more_info_control": {
|
||||
"script": {
|
||||
"last_action": "Senest udløst"
|
||||
},
|
||||
"sun": {
|
||||
"elevation": "Elevation",
|
||||
"rising": "Solopgang",
|
||||
"setting": "Indstilling"
|
||||
},
|
||||
"updater": {
|
||||
"title": "Opdateringsvejledning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth_store": {
|
||||
@ -977,7 +1031,12 @@
|
||||
"updater": "Opdater",
|
||||
"weblink": "Link",
|
||||
"zwave": "Z-Wave",
|
||||
"vacuum": "Støvsuger"
|
||||
"vacuum": "Støvsuger",
|
||||
"zha": "ZHA",
|
||||
"hassio": "Hass.io",
|
||||
"homeassistant": "Home Assistant",
|
||||
"lovelace": "Lovelace",
|
||||
"system_health": "System sundhed"
|
||||
},
|
||||
"attribute": {
|
||||
"weather": {
|
||||
|
@ -301,7 +301,8 @@
|
||||
"period": "Zeitraum"
|
||||
},
|
||||
"logbook": {
|
||||
"showing_entries": "Zeige Einträge für"
|
||||
"showing_entries": "Zeige Einträge für",
|
||||
"period": "Zeitraum"
|
||||
},
|
||||
"mailbox": {
|
||||
"empty": "Du hast keine Nachrichten",
|
||||
@ -433,6 +434,14 @@
|
||||
"hours": "Stunden",
|
||||
"minutes": "Minuten",
|
||||
"seconds": "Sekunden"
|
||||
},
|
||||
"geo_location": {
|
||||
"label": "Geolokalisierung",
|
||||
"source": "Quelle",
|
||||
"zone": "Zone",
|
||||
"event": "Ereignis:",
|
||||
"enter": "Betreten",
|
||||
"leave": "Verlassen"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -566,6 +575,14 @@
|
||||
"zha": {
|
||||
"caption": "ZHA",
|
||||
"description": "Zigbee Home Automation Netzwerkmanagement"
|
||||
},
|
||||
"area_registry": {
|
||||
"caption": "Gebietsregister",
|
||||
"description": "Überblick über alle Bereiche in Deinem Haus."
|
||||
},
|
||||
"entity_registry": {
|
||||
"caption": "Entitätsregister",
|
||||
"description": "Überblick aller bekannten Elemente."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@ -734,6 +751,11 @@
|
||||
"checked_items": "Markierte Artikel",
|
||||
"clear_items": "Markierte Elemente löschen",
|
||||
"add_item": "Artikel hinzufügen"
|
||||
},
|
||||
"empty_state": {
|
||||
"title": "Willkommen zu Hause",
|
||||
"no_devices": "Auf dieser Seite kannst du deine Geräte steuern, es sieht jedoch so aus, als hättest du noch keine eingerichtet. Gehe zur Integrationsseite, um damit zu beginnen.",
|
||||
"go_to_integrations_page": "Gehe zur Integrationsseite."
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -765,10 +787,16 @@
|
||||
"para_sure": "Bist du dir sicher, dass du die Benutzeroberfläche selbst verwalten möchtest?",
|
||||
"cancel": "Abbrechen",
|
||||
"save": "Kontrolle übernehmen"
|
||||
},
|
||||
"menu": {
|
||||
"raw_editor": "Raw-Konfigurationseditor"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"configure_ui": "Benutzeroberfläche konfigurieren"
|
||||
"configure_ui": "Benutzeroberfläche konfigurieren",
|
||||
"unused_entities": "Ungenutzte Elemente",
|
||||
"help": "Hilfe",
|
||||
"refresh": "Aktualisieren"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -945,8 +973,8 @@
|
||||
},
|
||||
"sun": {
|
||||
"elevation": "Höhe",
|
||||
"rising": "Aufgehend",
|
||||
"setting": "Einstellung"
|
||||
"rising": "Aufgang",
|
||||
"setting": "Untergang"
|
||||
},
|
||||
"updater": {
|
||||
"title": "Update-Anweisungen"
|
||||
@ -1001,7 +1029,11 @@
|
||||
"weblink": "Weblink",
|
||||
"zwave": "Z-Wave",
|
||||
"vacuum": "Staubsauger",
|
||||
"zha": "ZHA"
|
||||
"zha": "ZHA",
|
||||
"hassio": "Hass.io",
|
||||
"homeassistant": "Home Assistant",
|
||||
"lovelace": "Lovelace",
|
||||
"system_health": "Systemzustand"
|
||||
},
|
||||
"attribute": {
|
||||
"weather": {
|
||||
|
@ -301,7 +301,8 @@
|
||||
"period": "Περίοδος"
|
||||
},
|
||||
"logbook": {
|
||||
"showing_entries": "Εμφανίζοντα καταχωρήσεις για"
|
||||
"showing_entries": "Εμφανίζοντα καταχωρήσεις για",
|
||||
"period": "Περίοδος"
|
||||
},
|
||||
"mailbox": {
|
||||
"empty": "Δεν έχετε μηνύματα",
|
||||
@ -433,6 +434,13 @@
|
||||
"hours": "Ώρες",
|
||||
"minutes": "Λεπτά",
|
||||
"seconds": "Δευτερόλεπτα"
|
||||
},
|
||||
"geo_location": {
|
||||
"label": "Γεωγραφική θέση",
|
||||
"source": "Πηγή",
|
||||
"zone": "Ζώνη",
|
||||
"enter": "Είσοδος",
|
||||
"leave": "Αποχώρηση"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -556,7 +564,7 @@
|
||||
"no_device": "Οντότητες χωρίς συσκευές",
|
||||
"delete_confirm": "Είστε σίγουρος ότι θέλετε να διαγραφεί αυτή η ενοποίηση;",
|
||||
"restart_confirm": "Επανεκκινήστε το Home Assistant για να ολοκληρώσετε την κατάργηση αυτής της ενοποίησης",
|
||||
"manuf": "από {κατασκευαστής}",
|
||||
"manuf": "από {manufacturer}",
|
||||
"hub": "Συνδεδεμένο μέσω",
|
||||
"firmware": "Υλικολογισμικό: {έκδοση}",
|
||||
"device_unavailable": "συσκευή μη διαθέσιμη",
|
||||
@ -566,6 +574,14 @@
|
||||
"zha": {
|
||||
"caption": "ZHA",
|
||||
"description": "Διαχείριση του δικτύου ZigBee Home Automation"
|
||||
},
|
||||
"area_registry": {
|
||||
"caption": "Περιοχή Μητρώου",
|
||||
"description": "Επισκόπηση όλων των περιοχών στο σπίτι σας."
|
||||
},
|
||||
"entity_registry": {
|
||||
"caption": "Μητρώο οντοτήτων",
|
||||
"description": "Επισκόπηση όλων των γνωστών οντοτήτων."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@ -734,6 +750,11 @@
|
||||
"checked_items": "Επιλεγμένα στοιχεία",
|
||||
"clear_items": "Εκκαθάριση επιλεγμένων στοιχείων",
|
||||
"add_item": "Προσθήκη στοιχείου"
|
||||
},
|
||||
"empty_state": {
|
||||
"title": "Καλωσορίσατε στην αρχική σελίδα",
|
||||
"no_devices": "Αυτή η σελίδα σάς επιτρέπει να ελέγχετε τις συσκευές σας, ωστόσο φαίνεται ότι δεν έχετε ακόμα ρυθμίσει συσκευές. Μεταβείτε στη σελίδα ενοποίησης για να ξεκινήσετε.",
|
||||
"go_to_integrations_page": "Μεταβείτε στη σελίδα ενοποίησης."
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -768,7 +789,10 @@
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"configure_ui": "Διαμορφώστε το περιβάλλον χρήστη"
|
||||
"configure_ui": "Διαμορφώστε το περιβάλλον χρήστη",
|
||||
"unused_entities": "Αχρησιμοποίητες οντότητες",
|
||||
"help": "Βοήθεια",
|
||||
"refresh": "Ανανέωση"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1001,7 +1025,11 @@
|
||||
"weblink": "Σύνδεσμος",
|
||||
"zwave": "Z-Wave",
|
||||
"vacuum": "Εκκένωση ",
|
||||
"zha": "ΖΗΑ"
|
||||
"zha": "ΖΗΑ",
|
||||
"hassio": "Hass.io",
|
||||
"homeassistant": "Home Assistant",
|
||||
"lovelace": "Lovelace",
|
||||
"system_health": "Υγεία Συστήματος"
|
||||
},
|
||||
"attribute": {
|
||||
"weather": {
|
||||
|
@ -423,6 +423,17 @@
|
||||
"event": "Evento:",
|
||||
"enter": "Entrar",
|
||||
"leave": "Salir"
|
||||
},
|
||||
"webhook": {
|
||||
"label": "Webhook"
|
||||
},
|
||||
"time_pattern": {
|
||||
"hours": "Horas",
|
||||
"minutes": "Minutos",
|
||||
"seconds": "Hass.io"
|
||||
},
|
||||
"geo_location": {
|
||||
"event": "Evento:"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -552,6 +563,9 @@
|
||||
"device_unavailable": "dispositivo no disponible",
|
||||
"entity_unavailable": "entidad no disponible"
|
||||
}
|
||||
},
|
||||
"zha": {
|
||||
"caption": "ZHA"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@ -726,7 +740,8 @@
|
||||
"edit_card": {
|
||||
"header": "Configuración de la tarjeta",
|
||||
"save": "Guardar",
|
||||
"toggle_editor": "Cambiar editor"
|
||||
"toggle_editor": "Cambiar editor",
|
||||
"edit": "Editar"
|
||||
},
|
||||
"migrate": {
|
||||
"header": "Configuración inválida",
|
||||
@ -734,6 +749,9 @@
|
||||
"para_migrate": "Home Assistant puede agregar ID a todas sus tarjetas y vistas automáticamente por usted presionando el botón 'Migrar configuración'.",
|
||||
"migrate": "Migrar configuración"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"help": "Ayuda"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -951,7 +969,10 @@
|
||||
"updater": "Actualizador",
|
||||
"weblink": "Enlace web",
|
||||
"zwave": "",
|
||||
"vacuum": "Aspiradora"
|
||||
"vacuum": "Aspiradora",
|
||||
"zha": "ZHA",
|
||||
"lovelace": "Lovelace",
|
||||
"system_health": "Estado del sistema"
|
||||
},
|
||||
"attribute": {
|
||||
"weather": {
|
||||
|
@ -301,7 +301,8 @@
|
||||
"period": "Periodo"
|
||||
},
|
||||
"logbook": {
|
||||
"showing_entries": "Mostrando entradas del"
|
||||
"showing_entries": "Mostrando entradas del",
|
||||
"period": "Periodo"
|
||||
},
|
||||
"mailbox": {
|
||||
"empty": "No tiene ningún mensaje",
|
||||
@ -433,6 +434,14 @@
|
||||
"hours": "Horas",
|
||||
"minutes": "Minutos",
|
||||
"seconds": "Segundos"
|
||||
},
|
||||
"geo_location": {
|
||||
"label": "Geolocalización",
|
||||
"source": "Fuente",
|
||||
"zone": "Zona",
|
||||
"event": "Evento:",
|
||||
"enter": "Entrar",
|
||||
"leave": "Salir"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -565,7 +574,18 @@
|
||||
},
|
||||
"zha": {
|
||||
"caption": "ZHA",
|
||||
"description": "Gestión de red de Zigbee Home Automation"
|
||||
"description": "Gestión de red de Zigbee Home Automation",
|
||||
"services": {
|
||||
"reconfigure": "Reconfigura el dispositivo ZHA (curar dispositivo). Usa esto si tienes problemas con el dispositivo. Si el dispositivo en cuestión es un dispositivo alimentado por batería, asegurate de que está activo y aceptando comandos cuando uses este servicio."
|
||||
}
|
||||
},
|
||||
"area_registry": {
|
||||
"caption": "Registro de área",
|
||||
"description": "Visión general de todas las áreas de tu casa."
|
||||
},
|
||||
"entity_registry": {
|
||||
"caption": "Registro de Entidades",
|
||||
"description": "Resumen de todas las entidades conocidas."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@ -734,6 +754,11 @@
|
||||
"checked_items": "Elementos marcados",
|
||||
"clear_items": "Borrar elementos marcados",
|
||||
"add_item": "Añadir artículo"
|
||||
},
|
||||
"empty_state": {
|
||||
"title": "Bienvenido a casa",
|
||||
"no_devices": "Esta página te permite controlar tus dispositivos, aunque parece que aún no has configurado ninguno. Dirígete a la página de integraciones para empezar.",
|
||||
"go_to_integrations_page": "Ir a la página de integraciones."
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -765,10 +790,16 @@
|
||||
"para_sure": "¿Está seguro de que desea tomar el control de su interfaz de usuario?",
|
||||
"cancel": "No importa",
|
||||
"save": "Tomar el control"
|
||||
},
|
||||
"menu": {
|
||||
"raw_editor": "Editor de configuración en bruto"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"configure_ui": "Configurar la interfaz de usuario"
|
||||
"configure_ui": "Configurar la interfaz de usuario",
|
||||
"unused_entities": "Entidades no utilizadas",
|
||||
"help": "Ayuda",
|
||||
"refresh": "Refrescar"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1001,7 +1032,11 @@
|
||||
"weblink": "Enlace web",
|
||||
"zwave": "Z-Wave",
|
||||
"vacuum": "Aspiradora",
|
||||
"zha": "ZHA"
|
||||
"zha": "ZHA",
|
||||
"hassio": "Hass.io",
|
||||
"homeassistant": "Home Assistant",
|
||||
"lovelace": "Lovelace",
|
||||
"system_health": "Salud del sistema"
|
||||
},
|
||||
"attribute": {
|
||||
"weather": {
|
||||
|
@ -301,7 +301,8 @@
|
||||
"period": "Période"
|
||||
},
|
||||
"logbook": {
|
||||
"showing_entries": "Afficher les entrées pour le"
|
||||
"showing_entries": "Afficher les entrées pour le",
|
||||
"period": "Période"
|
||||
},
|
||||
"mailbox": {
|
||||
"empty": "Vous n'avez aucun message",
|
||||
@ -433,6 +434,14 @@
|
||||
"hours": "Heures",
|
||||
"minutes": "Minutes",
|
||||
"seconds": "Secondes"
|
||||
},
|
||||
"geo_location": {
|
||||
"label": "Géolocalisation",
|
||||
"source": "Source",
|
||||
"zone": "Zone",
|
||||
"event": "Événement :",
|
||||
"enter": "Entre",
|
||||
"leave": "Quitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -562,6 +571,21 @@
|
||||
"device_unavailable": "appareil indisponible",
|
||||
"entity_unavailable": "entité indisponible"
|
||||
}
|
||||
},
|
||||
"zha": {
|
||||
"caption": "ZHA",
|
||||
"description": "Gestion de réseau domotique ZigBee",
|
||||
"services": {
|
||||
"reconfigure": "Reconfigurer le périphérique ZHA. Utilisez cette option si vous rencontrez des problèmes avec le périphérique. Si l'appareil en question est un appareil alimenté par batterie, assurez-vous qu'il soit allumé et qu'il accepte les commandes lorsque vous utilisez ce service."
|
||||
}
|
||||
},
|
||||
"area_registry": {
|
||||
"caption": "Registre des pièces",
|
||||
"description": "Vue d'ensemble de toutes les pièces de votre maison."
|
||||
},
|
||||
"entity_registry": {
|
||||
"caption": "Registre des entités",
|
||||
"description": "Vue d'ensemble de toutes les entités connues."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@ -730,6 +754,11 @@
|
||||
"checked_items": "Éléments cochés",
|
||||
"clear_items": "Effacer éléments cochés",
|
||||
"add_item": "Ajouter un élément"
|
||||
},
|
||||
"empty_state": {
|
||||
"title": "Bienvenue à la maison",
|
||||
"no_devices": "Cette page vous permet de contrôler vos périphériques. Toutefois, il semble que vous n’ayez pas encore configuré de périphériques. Rendez-vous sur la page des intégrations pour commencer.",
|
||||
"go_to_integrations_page": "Aller à la page des intégrations."
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -761,10 +790,16 @@
|
||||
"para_sure": "Êtes-vous sûr de vouloir prendre le controle de l'interface utilisateur?",
|
||||
"cancel": "Oublie ce que j'ai dit, c'est pas grave.",
|
||||
"save": "Prenez le contrôle"
|
||||
},
|
||||
"menu": {
|
||||
"raw_editor": "Éditeur de configuration"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"configure_ui": "Configurer l'interface utilisateur"
|
||||
"configure_ui": "Configurer l'interface utilisateur",
|
||||
"unused_entities": "Entités inutilisées",
|
||||
"help": "Aide",
|
||||
"refresh": "Actualiser"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -941,7 +976,8 @@
|
||||
},
|
||||
"sun": {
|
||||
"elevation": "Élévation",
|
||||
"rising": "Lever"
|
||||
"rising": "Lever",
|
||||
"setting": "Coucher"
|
||||
},
|
||||
"updater": {
|
||||
"title": "Instructions de mise à jour"
|
||||
@ -995,7 +1031,12 @@
|
||||
"updater": "Mise à jour",
|
||||
"weblink": "Lien",
|
||||
"zwave": "Z-Wave",
|
||||
"vacuum": "Aspirateur"
|
||||
"vacuum": "Aspirateur",
|
||||
"zha": "ZHA",
|
||||
"hassio": "Hass.io",
|
||||
"homeassistant": "Home Assistant",
|
||||
"lovelace": "Lovelace",
|
||||
"system_health": "Santé du système"
|
||||
},
|
||||
"attribute": {
|
||||
"weather": {
|
||||
|
@ -353,20 +353,20 @@
|
||||
"description": "צור וערוך אוטומציות",
|
||||
"picker": {
|
||||
"header": "עורך אוטומציה",
|
||||
"introduction": "עורך אוטומציה מאפשר לך ליצור ולערוך אוטומציה. אנא קרא את [ההוראות] (https:\/\/home-assistant.io\/docs\/automation\/editor\/) כדי לוודא שהגדרת את ה - Home Assistant כהלכה.",
|
||||
"introduction": "עורך אוטומציה מאפשר לך ליצור ולערוך אוטומציות. אנא קרא את [ההוראות](https:\/\/home-assistant.io\/docs\/automation\/editor\/) כדי לוודא שהגדרת את ה - Home Assistant כהלכה.",
|
||||
"pick_automation": "בחר אוטומציה לעריכה",
|
||||
"no_automations": "לא הצלחנו למצוא שום אוטומציה הניתנת לעריכה",
|
||||
"add_automation": "הוסף אוטומציה"
|
||||
},
|
||||
"editor": {
|
||||
"introduction": "השתמש אוטומציות להביא את החיים לבית שלך",
|
||||
"introduction": "השתמש באוטומציות להביא את הבית שלך לחיים",
|
||||
"default_name": "אוטומציה חדשה",
|
||||
"save": "שמור",
|
||||
"unsaved_confirm": "יש לך שינויים שלא נשמרו. אתה בטוח שאתה רוצה לעזוב?",
|
||||
"alias": "שם",
|
||||
"triggers": {
|
||||
"header": "טריגרים",
|
||||
"introduction": "טריגרים הם מה שמתחיל כל אוטומציה. ניתן לציין מספר טריגרים עבור אותו כלל. לאחר הפעלת טריגר, ה - Assistant Assistant יאמת את התנאים, אם קיימים, ויקרא לפעולה. \n\n [למידע נוסף על גורמים טריגרים.) (Https:\/\/home-assistant.io\/docs\/automation\/trigger\/)",
|
||||
"introduction": "טריגרים הם מה שמתחיל כל אוטומציה. ניתן לציין מספר טריגרים עבור אותו כלל. לאחר הפעלת טריגר, ה - Home Assistant יאמת את התנאים, אם קיימים, ויקרא לפעולה. \n\n[למד עוד על טריגרים](https:\/\/home-assistant.io\/docs\/automation\/trigger\/)",
|
||||
"add": "הוספת טריגר",
|
||||
"duplicate": "שכפל",
|
||||
"delete": "מחק",
|
||||
@ -447,7 +447,7 @@
|
||||
},
|
||||
"conditions": {
|
||||
"header": "תנאים",
|
||||
"introduction": "התנאים הם חלק אופציונלי של כלל אוטומציה, וניתן להשתמש בהם כדי למנוע פעולה כלשהי בעת הפעלתה. התנאים נראים דומים מאוד לטריגרים אך הם שונים מאוד. הטריגר יסתכל על האירועים המתרחשים במערכת בעוד תנאי רק מסתכל על איך המערכת נראית עכשיו. הטריגר יכול שמתג נדלק. תנאי יכול לראות רק אם מתג מופעל או כבוי. \n\n [למידע נוסף על תנאים.] (Https:\/\/home-assistant.io\/docs\/scripts\/conditions\/)",
|
||||
"introduction": "התנאים הם חלק אופציונלי של כלל אוטומציה, וניתן להשתמש בהם כדי למנוע פעולה כלשהי בעת הפעלתה. התנאים נראים דומים מאוד לטריגרים אך הם שונים מאוד. הטריגר יסתכל על האירועים המתרחשים במערכת בעוד תנאי רק מסתכל על איך המערכת נראית עכשיו. הטריגר יכול שמתג נדלק. תנאי יכול לראות רק אם מתג מופעל או כבוי. \n\n[למידע נוסף על תנאים](Https:\/\/home-assistant.io\/docs\/scripts\/conditions\/)",
|
||||
"add": "הוסף תנאי",
|
||||
"duplicate": "שכפל",
|
||||
"delete": "מחק",
|
||||
@ -492,7 +492,7 @@
|
||||
},
|
||||
"actions": {
|
||||
"header": "פעולות",
|
||||
"introduction": "הפעולות הן מה שHome Assistant יעשה כאשר אוטומציה מופעלת. \n\n [למידע נוסף על פעולות.] (https:\/\/home-assistant.io\/docs\/automation\/action\/)",
|
||||
"introduction": "הפעולות הן מה שHome Assistant יעשה כאשר אוטומציה מופעלת. \n\n[למידע נוסף על פעולות](https:\/\/home-assistant.io\/docs\/automation\/action\/)",
|
||||
"add": "הוסף פעולה",
|
||||
"duplicate": "שכפל",
|
||||
"delete": "מחק",
|
||||
@ -506,7 +506,7 @@
|
||||
},
|
||||
"delay": {
|
||||
"label": "עיכוב",
|
||||
"delay": "עיקוב"
|
||||
"delay": "עיכוב"
|
||||
},
|
||||
"wait_template": {
|
||||
"label": "לחכות",
|
||||
@ -517,7 +517,7 @@
|
||||
"label": "תנאי"
|
||||
},
|
||||
"event": {
|
||||
"label": "אירוע אש",
|
||||
"label": "ירה אירוע",
|
||||
"event": "ארוע",
|
||||
"service_data": "נתוני שירות"
|
||||
}
|
||||
@ -557,7 +557,7 @@
|
||||
"description": "ניהול התקנים ושירותים מחוברים",
|
||||
"discovered": "זוהו",
|
||||
"configured": "הוגדר",
|
||||
"new": "הגדר אינטגריצה",
|
||||
"new": "הגדר אינטגרציה",
|
||||
"configure": "הגדר",
|
||||
"none": "כלום אינו הוגדר עדיין",
|
||||
"config_entry": {
|
||||
|
@ -576,10 +576,12 @@
|
||||
"description": "Zigbee Home Automation hálózat menedzsment"
|
||||
},
|
||||
"area_registry": {
|
||||
"description": "Az összes otthoni terület áttekintése."
|
||||
"caption": "Terület Nyilvántartás",
|
||||
"description": "Az összes otthoni terület áttekintése"
|
||||
},
|
||||
"entity_registry": {
|
||||
"description": "Az összes ismert entitás áttekintése."
|
||||
"caption": "Entitás Nyilvántartás",
|
||||
"description": "Az összes ismert entitás áttekintése"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@ -750,7 +752,9 @@
|
||||
"add_item": "Tétel hozzáadása"
|
||||
},
|
||||
"empty_state": {
|
||||
"title": "Üdv Itthon"
|
||||
"title": "Üdv Itthon",
|
||||
"no_devices": "Ez az oldal lehetővé teszi az eszközeid vezérlését, de úgy tűnik, hogy még nincs beállítva egy sem. A kezdéshez lépj át az integrációs oldalra.",
|
||||
"go_to_integrations_page": "Ugrás az 'integrációk' oldalra."
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -782,6 +786,9 @@
|
||||
"para_sure": "Biztosan át szeretnéd venni az irányítást a felhasználói felületed felett?",
|
||||
"cancel": "Mégsem",
|
||||
"save": "Irányítás átvétele"
|
||||
},
|
||||
"menu": {
|
||||
"raw_editor": "Konfiguráció szerkesztő"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@ -956,7 +963,7 @@
|
||||
"dialogs": {
|
||||
"more_info_settings": {
|
||||
"save": "Mentés",
|
||||
"name": "Név",
|
||||
"name": "Név felülbírálása",
|
||||
"entity_id": "Entitás ID"
|
||||
},
|
||||
"more_info_control": {
|
||||
@ -1024,7 +1031,8 @@
|
||||
"zha": "ZHA",
|
||||
"hassio": "Hass.io",
|
||||
"homeassistant": "Home Assistant",
|
||||
"lovelace": "Lovelace"
|
||||
"lovelace": "Lovelace",
|
||||
"system_health": "Rendszer Állapot"
|
||||
},
|
||||
"attribute": {
|
||||
"weather": {
|
||||
|
@ -301,7 +301,8 @@
|
||||
"period": "Periodo"
|
||||
},
|
||||
"logbook": {
|
||||
"showing_entries": "Mostra registrazioni per"
|
||||
"showing_entries": "Mostra registrazioni per",
|
||||
"period": "Periodo"
|
||||
},
|
||||
"mailbox": {
|
||||
"empty": "Non hai nessun messaggio",
|
||||
@ -426,13 +427,21 @@
|
||||
},
|
||||
"webhook": {
|
||||
"label": "Webhook",
|
||||
"webhook_id": "ID Webhook"
|
||||
"webhook_id": "Webhook ID"
|
||||
},
|
||||
"time_pattern": {
|
||||
"label": "Pattern temporale",
|
||||
"hours": "Ore",
|
||||
"minutes": "Minuti",
|
||||
"seconds": "Secondi"
|
||||
},
|
||||
"geo_location": {
|
||||
"label": "Geolocalizzazione",
|
||||
"source": "Fonte",
|
||||
"zone": "Zona",
|
||||
"event": "Evento:",
|
||||
"enter": "Ingresso",
|
||||
"leave": "Uscita"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -544,7 +553,7 @@
|
||||
"description_not_login": "Accesso non effettuato"
|
||||
},
|
||||
"integrations": {
|
||||
"caption": "integrazioni",
|
||||
"caption": "Integrazioni",
|
||||
"description": "Gestisci dispositivi e servizi connessi",
|
||||
"discovered": "Scoperto",
|
||||
"configured": "Configurato",
|
||||
@ -565,7 +574,18 @@
|
||||
},
|
||||
"zha": {
|
||||
"caption": "ZHA",
|
||||
"description": "Gestione rete Zigbee Home Automation"
|
||||
"description": "Gestione rete Zigbee Home Automation",
|
||||
"services": {
|
||||
"reconfigure": "Riconfigurare il dispositivo ZHA (dispositivo di guarigione). Utilizzare questa opzione se si verificano problemi con il dispositivo. Se il dispositivo in questione è un dispositivo alimentato a batteria, assicurarsi che sia sveglio e che accetti i comandi quando si utilizza questo servizio."
|
||||
}
|
||||
},
|
||||
"area_registry": {
|
||||
"caption": "Registro di area",
|
||||
"description": "Panoramica di tutte le aree della tua casa."
|
||||
},
|
||||
"entity_registry": {
|
||||
"caption": "Registro delle entità",
|
||||
"description": "Panoramica di tutte le entità conosciute."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@ -702,7 +722,7 @@
|
||||
"data": {
|
||||
"user": "Utente"
|
||||
},
|
||||
"description": "Perfavore, scegli l'utente con cui vuoi effettuare l'accesso:"
|
||||
"description": "Per favore, scegli l'utente con cui vuoi effettuare l'accesso:"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
@ -734,6 +754,11 @@
|
||||
"checked_items": "Elementi selezionati",
|
||||
"clear_items": "Cancella gli elementi selezionati",
|
||||
"add_item": "Aggiungi elemento"
|
||||
},
|
||||
"empty_state": {
|
||||
"title": "Benvenuto a casa",
|
||||
"no_devices": "Questa pagina ti consente di controllare i tuoi dispositivi, tuttavia sembra che tu non abbia ancora configurato uno. Vai alla pagina delle integrazioni per iniziare.",
|
||||
"go_to_integrations_page": "Vai alla pagina delle integrazioni."
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -765,10 +790,16 @@
|
||||
"para_sure": "Sei sicuro di voler prendere il controllo della tua interfaccia utente?",
|
||||
"cancel": "Rinuncia",
|
||||
"save": "Prendere il controllo"
|
||||
},
|
||||
"menu": {
|
||||
"raw_editor": "Editor di configurazione grezzo"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"configure_ui": "Configurare l'interfaccia utente"
|
||||
"configure_ui": "Configurare l'interfaccia utente",
|
||||
"unused_entities": "Entità non utilizzate",
|
||||
"help": "Aiuto",
|
||||
"refresh": "Aggiorna"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -936,7 +967,7 @@
|
||||
"dialogs": {
|
||||
"more_info_settings": {
|
||||
"save": "Salva",
|
||||
"name": "Nome",
|
||||
"name": "Sovrascrittura del nome",
|
||||
"entity_id": "ID Entità"
|
||||
},
|
||||
"more_info_control": {
|
||||
@ -1001,7 +1032,11 @@
|
||||
"weblink": "Link Web",
|
||||
"zwave": "Z-Wave",
|
||||
"vacuum": "Aspirapolvere",
|
||||
"zha": "ZHA"
|
||||
"zha": "ZHA",
|
||||
"hassio": "Hass.io",
|
||||
"homeassistant": "Home Assistant",
|
||||
"lovelace": "Lovelace",
|
||||
"system_health": "Salute del sistema"
|
||||
},
|
||||
"attribute": {
|
||||
"weather": {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user