Merge pull request #6156 from home-assistant/dev

This commit is contained in:
Bram Kragten 2020-06-13 13:47:09 +02:00 committed by GitHub
commit ca8586789a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
205 changed files with 5101 additions and 4896 deletions

View File

@ -1,4 +0,0 @@
node_modules
hass_frontend
hass_frontend_es5
.git

View File

@ -1,31 +0,0 @@
FROM node:8.11.1-alpine
# install yarn
ENV PATH /root/.yarn/bin:$PATH
## Install/force base tools
RUN apk update \
&& apk add make g++ curl bash binutils tar git python2 python3 \
&& rm -rf /var/cache/apk/* \
&& /bin/bash \
&& touch ~/.bashrc
## Install yarn
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
## Setup the project
RUN mkdir -p /frontend
WORKDIR /frontend
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
COPY script/docker_entrypoint.sh /usr/bin/docker_entrypoint.sh
RUN chmod +x /usr/bin/docker_entrypoint.sh
CMD [ "docker_entrypoint.sh" ]

View File

@ -22,15 +22,6 @@ This is the repository for the official [Home Assistant](https://home-assistant.
A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project. A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project.
### Docker environment
It is possible to compile the project and/or run commands in the development environment having only the [Docker](https://www.docker.com) pre-installed in the system. On the root of project you can do:
- `sh ./script/docker_run.sh build` Build all the project with one command
- `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the _classic environment_) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
**Note**: if you have installed `npm` in addition to the `docker`, you can use the commands `npm run docker_build` and `npm run bash` to get a full build or bash as explained above
## License ## License
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects. Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.

View File

@ -85,8 +85,8 @@ module.exports.babelExclude = () => [
const outputPath = (outputRoot, latestBuild) => const outputPath = (outputRoot, latestBuild) =>
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5"); path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
const publicPath = (latestBuild) => const publicPath = (latestBuild, root = "") =>
latestBuild ? "/frontend_latest/" : "/frontend_es5/"; latestBuild ? `${root}/frontend_latest/` : `${root}/frontend_es5/`;
/* /*
BundleConfig { BundleConfig {
@ -170,15 +170,12 @@ module.exports.config = {
}, },
hassio({ isProdBuild, latestBuild }) { hassio({ isProdBuild, latestBuild }) {
if (latestBuild) {
throw new Error("Hass.io does not support latest build!");
}
return { return {
entry: { entry: {
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"), entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
}, },
outputPath: paths.hassio_output_root, outputPath: outputPath(paths.hassio_output_root, latestBuild),
publicPath: paths.hassio_publicPath, publicPath: publicPath(latestBuild, paths.hassio_publicPath),
isProdBuild, isProdBuild,
latestBuild, latestBuild,
dontHash: new Set(["entrypoint"]), dontHash: new Set(["entrypoint"]),

View File

@ -20,6 +20,7 @@ gulp.task(
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations"), gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-cast", "copy-static-cast",
"gen-index-cast-dev",
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast" env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
) )
); );

View File

@ -6,30 +6,32 @@ const merge = require("merge-stream");
const path = require("path"); const path = require("path");
const paths = require("../paths"); const paths = require("../paths");
const zopfliOptions = { threshold: 150 };
gulp.task("compress-app", function compressApp() { gulp.task("compress-app", function compressApp() {
const jsLatest = gulp const jsLatest = gulp
.src(path.resolve(paths.app_output_latest, "**/*.js")) .src(path.resolve(paths.app_output_latest, "**/*.js"))
.pipe(zopfli({ threshold: 150 })) .pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.app_output_latest)); .pipe(gulp.dest(paths.app_output_latest));
const jsEs5 = gulp const jsEs5 = gulp
.src(path.resolve(paths.app_output_es5, "**/*.js")) .src(path.resolve(paths.app_output_es5, "**/*.js"))
.pipe(zopfli({ threshold: 150 })) .pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.app_output_es5)); .pipe(gulp.dest(paths.app_output_es5));
const polyfills = gulp const polyfills = gulp
.src(path.resolve(paths.app_output_static, "polyfills/*.js")) .src(path.resolve(paths.app_output_static, "polyfills/*.js"))
.pipe(zopfli({ threshold: 150 })) .pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "polyfills"))); .pipe(gulp.dest(path.resolve(paths.app_output_static, "polyfills")));
const translations = gulp const translations = gulp
.src(path.resolve(paths.app_output_static, "translations/**/*.json")) .src(path.resolve(paths.app_output_static, "translations/**/*.json"))
.pipe(zopfli({ threshold: 150 })) .pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "translations"))); .pipe(gulp.dest(path.resolve(paths.app_output_static, "translations")));
const icons = gulp const icons = gulp
.src(path.resolve(paths.app_output_static, "mdi/*.json")) .src(path.resolve(paths.app_output_static, "mdi/*.json"))
.pipe(zopfli({ threshold: 150 })) .pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "mdi"))); .pipe(gulp.dest(path.resolve(paths.app_output_static, "mdi")));
return merge(jsLatest, jsEs5, polyfills, translations, icons); return merge(jsLatest, jsEs5, polyfills, translations, icons);
@ -38,6 +40,6 @@ gulp.task("compress-app", function compressApp() {
gulp.task("compress-hassio", function compressApp() { gulp.task("compress-hassio", function compressApp() {
return gulp return gulp
.src(path.resolve(paths.hassio_output_root, "**/*.js")) .src(path.resolve(paths.hassio_output_root, "**/*.js"))
.pipe(zopfli()) .pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.hassio_output_root)); .pipe(gulp.dest(paths.hassio_output_root));
}); });

View File

@ -36,11 +36,13 @@ function copyMdiIcons(staticDir) {
function copyPolyfills(staticDir) { function copyPolyfills(staticDir) {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
// Web Component polyfills and adapters // For custom panels using ES5 builds that don't use Babel 7+
copyFileDir( copyFileDir(
npmPath("@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"), npmPath("@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"),
staticPath("polyfills/") staticPath("polyfills/")
); );
// Web Component polyfills and adapters
copyFileDir( copyFileDir(
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js"), npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js"),
staticPath("polyfills/") staticPath("polyfills/")

View File

@ -1,6 +1,9 @@
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs");
const path = require("path");
const env = require("../env"); const env = require("../env");
const paths = require("../paths");
require("./clean.js"); require("./clean.js");
require("./gen-icons-json.js"); require("./gen-icons-json.js");
@ -8,6 +11,24 @@ require("./webpack.js");
require("./compress.js"); require("./compress.js");
require("./rollup.js"); require("./rollup.js");
async function writeEntrypointJS() {
// We ship two builds and we need to do feature detection on what version to load.
fs.mkdirSync(paths.hassio_output_root, { recursive: true });
fs.writeFileSync(
path.resolve(paths.hassio_output_root, "entrypoint.js"),
`
try {
new Function("import('${paths.hassio_publicPath}/frontend_latest/entrypoint.js')")();
} catch (err) {
var el = document.createElement('script');
el.src = '${paths.hassio_publicPath}/frontend_es5/entrypoint.js';
document.body.appendChild(el);
}
`,
{ encoding: "utf-8" }
);
}
gulp.task( gulp.task(
"develop-hassio", "develop-hassio",
gulp.series( gulp.series(
@ -16,6 +37,7 @@ gulp.task(
}, },
"clean-hassio", "clean-hassio",
"gen-icons-json", "gen-icons-json",
writeEntrypointJS,
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio" env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
) )
); );
@ -29,6 +51,7 @@ gulp.task(
"clean-hassio", "clean-hassio",
"gen-icons-json", "gen-icons-json",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio", env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
writeEntrypointJS,
...// Don't compress running tests ...// Don't compress running tests
(env.isTest() ? [] : ["compress-hassio"]) (env.isTest() ? [] : ["compress-hassio"])
) )

View File

@ -92,11 +92,7 @@ gulp.task("rollup-watch-app", () => {
}); });
gulp.task("rollup-watch-hassio", () => { gulp.task("rollup-watch-hassio", () => {
watchRollup( watchRollup(rollupConfig.createHassioConfig, ["hassio/src/**"]);
// Force latestBuild = false for hassio config.
(conf) => rollupConfig.createHassioConfig({ ...conf, latestBuild: false }),
["hassio/src/**"]
);
}); });
gulp.task("rollup-dev-server-demo", () => { gulp.task("rollup-dev-server-demo", () => {
@ -137,12 +133,7 @@ gulp.task(
); );
gulp.task("rollup-prod-hassio", () => gulp.task("rollup-prod-hassio", () =>
buildRollup( bothBuilds(rollupConfig.createHassioConfig, { isProdBuild: true })
rollupConfig.createHassioConfig({
isProdBuild: true,
latestBuild: false,
})
)
); );
gulp.task("rollup-prod-gallery", () => gulp.task("rollup-prod-gallery", () =>

View File

@ -129,7 +129,7 @@ gulp.task("webpack-watch-hassio", () => {
webpack( webpack(
createHassioConfig({ createHassioConfig({
isProdBuild: false, isProdBuild: false,
latestBuild: false, latestBuild: true,
}) })
).watch({}, handler()); ).watch({}, handler());
}); });
@ -139,9 +139,8 @@ gulp.task(
() => () =>
new Promise((resolve) => new Promise((resolve) =>
webpack( webpack(
createHassioConfig({ bothBuilds(createHassioConfig, {
isProdBuild: true, isProdBuild: true,
latestBuild: false,
}), }),
handler(resolve) handler(resolve)
) )

View File

@ -34,7 +34,7 @@ module.exports = {
hassio_dir: path.resolve(__dirname, "../hassio"), hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"), hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_publicPath: "/api/hassio/app/", hassio_publicPath: "/api/hassio/app",
translations_src: path.resolve(__dirname, "../src/translations"), translations_src: path.resolve(__dirname, "../src/translations"),
}; };

View File

@ -70,7 +70,9 @@ const createWebpackConfig = ({
if ( if (
!context.includes("/node_modules/") || !context.includes("/node_modules/") ||
// calling define.amd will call require("!!webpack amd options") // calling define.amd will call require("!!webpack amd options")
resource.startsWith("!!webpack") resource.startsWith("!!webpack") ||
// loaded by webpack dev server but doesn't exist.
resource === "webpack/hot"
) { ) {
return false; return false;
} }
@ -80,7 +82,11 @@ const createWebpackConfig = ({
? path.resolve(context, resource) ? path.resolve(context, resource)
: require.resolve(resource); : require.resolve(resource);
} catch (err) { } catch (err) {
console.error("Error in ignore plugin", resource, context); console.error(
"Error in Home Assistant ignore plugin",
resource,
context
);
throw err; throw err;
} }

View File

@ -45,7 +45,6 @@
(function() { (function() {
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check. // // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) { if (!isS101) {
_ls("/static/polyfills/custom-elements-es5-adapter.js");
<% if (useRollup) { %> <% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() { _ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>"); System.import("<%= es5LauncherJS %>");

View File

@ -36,7 +36,6 @@
(function() { (function() {
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check. // // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) { if (!isS101) {
_ls("/static/polyfills/custom-elements-es5-adapter.js");
<% if (useRollup) { %> <% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() { _ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>"); System.import("<%= es5LauncherJS %>");

View File

@ -1,11 +1,8 @@
const { createCastConfig } = require("../build-scripts/webpack.js"); const { createCastConfig } = require("../build-scripts/webpack.js");
const { isProdBuild } = require("../build-scripts/env.js"); const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
// File just used for stats builds
const latestBuild = true;
module.exports = createCastConfig({ module.exports = createCastConfig({
isProdBuild: isProdBuild(), isProdBuild: isProdBuild(),
latestBuild, isStatsBuild: isStatsBuild(),
latestBuild: true,
}); });

View File

@ -92,7 +92,6 @@
(function() { (function() {
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check. // // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) { if (!isS101) {
_ls("/static/polyfills/custom-elements-es5-adapter.js");
<% if (useRollup) { %> <% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() { _ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5DemoJS %>"); System.import("<%= es5DemoJS %>");

View File

@ -3,6 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-switch"; import "../../../src/components/ha-switch";
import "../../../src/components/ha-formfield";
import "./demo-card"; import "./demo-card";
class DemoCards extends PolymerElement { class DemoCards extends PolymerElement {
@ -26,9 +27,10 @@ class DemoCards extends PolymerElement {
</style> </style>
<app-toolbar> <app-toolbar>
<div class="filters"> <div class="filters">
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled"> <ha-formfield label="Show config">
Show config <ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
</ha-switch> </ha-switch>
</ha-formfield>
</div> </div>
</app-toolbar> </app-toolbar>
<div class="cards"> <div class="cards">

View File

@ -1,5 +1,8 @@
const { createGalleryConfig } = require("../build-scripts/webpack.js"); const { createGalleryConfig } = require("../build-scripts/webpack.js");
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
module.exports = createGalleryConfig({ module.exports = createGalleryConfig({
isProdBuild: isProdBuild(),
isStatsBuild: isStatsBuild(),
latestBuild: true, latestBuild: true,
}); });

View File

@ -1,9 +1,6 @@
window.loadES5Adapter().then(() => { import "../../src/resources/compatibility";
// eslint-disable-next-line import "../../src/resources/roboto";
import(/* webpackChunkName: "roboto" */ "../../src/resources/roboto"); import "./hassio-main";
// eslint-disable-next-line
import(/* webpackChunkName: "hassio-main" */ "./hassio-main");
});
const styleEl = document.createElement("style"); const styleEl = document.createElement("style");
styleEl.innerHTML = ` styleEl.innerHTML = `

View File

@ -1,4 +1,3 @@
import "@polymer/paper-menu-button/paper-menu-button";
import { import {
css, css,
CSSResult, CSSResult,

View File

@ -1,11 +1,8 @@
const { createHassioConfig } = require("../build-scripts/webpack.js"); const { createHassioConfig } = require("../build-scripts/webpack.js");
const { isProdBuild } = require("../build-scripts/env.js"); const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
// File just used for stats builds
const latestBuild = false;
module.exports = createHassioConfig({ module.exports = createHassioConfig({
isProdBuild: isProdBuild(), isProdBuild: isProdBuild(),
latestBuild, isStatsBuild: isStatsBuild(),
latestBuild: true,
}); });

View File

@ -17,9 +17,7 @@
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types", "lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
"format": "npm run format:eslint && npm run format:prettier", "format": "npm run format:eslint && npm run format:prettier",
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts", "mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
"test": "npm run lint && npm run mocha", "test": "npm run lint && npm run mocha"
"docker_build": "sh ./script/docker_run.sh build $npm_package_version",
"bash": "sh ./script/docker_run.sh bash $npm_package_version"
}, },
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)", "author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -89,7 +87,7 @@
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^0.12.4", "hls.js": "^0.12.4",
"home-assistant-js-websocket": "^5.2.1", "home-assistant-js-websocket": "^5.3.0",
"idb-keyval": "^3.2.0", "idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9", "intl-messageformat": "^8.3.9",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",

View File

@ -1,14 +0,0 @@
#!/bin/bash
# Docker entry point inspired by travis build and script/build_frontend
# Stop on errors
set -e
# Build the frontend but not used the npm run build
/bin/bash script/build_frontend
# TEST
npm run test
#
#xvfb-run wct

View File

@ -1,103 +0,0 @@
#!/bin/bash
# Basic Docker Management scripts
# With this script you can build software, or enter an agnostic development environment and run commands interactively.
check_mandatory_tools(){
if [ "x$(which docker)" == "x" ]; then
echo "UNKNOWN - Missing docker binary! Are you sure it is installed and reachable?"
exit 3
fi
docker info > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "UNKNOWN - Unable to talk to the docker daemon! Maybe the docker daemon is not running"
exit 3
fi
}
check_dev_image(){
if [[ "$(docker images -q ${IMAGE_NAME}:$IMAGE_TAG 2> /dev/null)" == "" ]]; then
echo "UNKNOWN - Can't find the development docker image ${IMAGE_NAME}:$IMAGE_TAG"
while true; do
read -p "Do you want to create it now?" yn
case $yn in
[Yy]* ) create_image; break;;
[Nn]* ) exit 3;;
* ) echo "Please answer y or n";;
esac
done
fi
}
# Building the basic image for compiling the production frontend
create_image(){
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
}
#
# Execute interactive bash on basic image
#
run_bash_on_docker(){
check_dev_image
docker run -it \
-v $PWD/:/frontend/ \
-v /frontend/node_modules \
-v /frontend/bower_components \
${IMAGE_NAME}:${IMAGE_TAG} /bin/bash
}
#
# Execute the basic image for compiling the production frontend
#
build_all(){
check_dev_image
docker run -it \
-v $PWD/:/frontend/ \
-v /frontend/node_modules \
-v /frontend/bower_components \
${IMAGE_NAME}:${IMAGE_TAG} /bin/bash script/build_frontend
}
# Init Global Variable
IMAGE_NAME=home_assistant_fe_image
IMAGE_TAG=${2:-latest}
check_mandatory_tools
case "$1" in
setup_env)
create_image
;;
bash)
run_bash_on_docker
;;
build)
build_all
;;
*)
echo "NAME"
echo " Docker Management."
echo ""
echo "SYNOPSIS"
echo " ${0} command [version]"
echo ""
echo "DESCRIPTION"
echo " With this script you can build software, or enter an agnostic development environment and run commands interactively."
echo ""
echo " The command are:"
echo " setup_env Create develop images"
echo " bash Run bash on develop enviroments"
echo " build Run silent build"
echo ""
echo " The version is optional, if not inserted it assumes \"latest\". "
exit 1
;;
esac
exit 0

View File

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

View File

@ -6,7 +6,7 @@ export const isValidEntityId = (entityId: string) =>
export const createValidEntityId = (input: string) => export const createValidEntityId = (input: string) =>
input input
.toLowerCase() .toLowerCase()
.replace(/\s|'/g, "_") // replace spaces and quotes with underscore .replace(/\s|'|\./g, "_") // replace spaces, points and quotes with underscore
.replace(/\W/g, "") // remove not allowed chars .replace(/\W/g, "") // remove not allowed chars
.replace(/_{2,}/g, "_") // replace multiple underscores with 1 .replace(/_{2,}/g, "_") // replace multiple underscores with 1
.replace(/_$/, ""); // remove underscores at the end .replace(/_$/, ""); // remove underscores at the end

View File

@ -57,9 +57,12 @@ export const iconColorCSS = css`
0% { 0% {
opacity: 1; opacity: 1;
} }
100% { 50% {
opacity: 0; opacity: 0;
} }
100% {
opacity: 1;
}
} }
ha-icon[data-domain="plant"][data-state="problem"], ha-icon[data-domain="plant"][data-state="problem"],

View File

@ -619,6 +619,11 @@ export class HaDataTable extends LitElement {
text-transform: inherit; text-transform: inherit;
} }
.mdc-data-table__cell a {
color: inherit;
text-decoration: none;
}
.mdc-data-table__cell--numeric { .mdc-data-table__cell--numeric {
text-align: right; text-align: right;
} }

View File

@ -40,7 +40,7 @@ export class HaButtonMenu extends LitElement {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
:host { :host {
display: block; display: inline-block;
position: relative; position: relative;
} }
`; `;

View File

@ -176,7 +176,9 @@ class HaCameraStream extends LitElement {
Hls: HLSModule, Hls: HLSModule,
url: string url: string
) { ) {
const hls = new Hls(); const hls = new Hls({
liveBackBufferLength: 60,
});
this._hlsPolyfillInstance = hls; this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl); hls.attachMedia(videoEl);
hls.on(Hls.Events.MEDIA_ATTACHED, () => { hls.on(Hls.Events.MEDIA_ATTACHED, () => {

View File

@ -12,6 +12,8 @@ import {
class HaCard extends LitElement { class HaCard extends LitElement {
@property() public header?: string; @property() public header?: string;
@property({ type: Boolean, reflect: true }) public outlined = false;
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
:host { :host {
@ -19,12 +21,12 @@ class HaCard extends LitElement {
--ha-card-background, --ha-card-background,
var(--paper-card-background-color, white) var(--paper-card-background-color, white)
); );
border-radius: var(--ha-card-border-radius, 2px); border-radius: var(--ha-card-border-radius, 4px);
box-shadow: var( box-shadow: var(
--ha-card-box-shadow, --ha-card-box-shadow,
0 2px 2px 0 rgba(0, 0, 0, 0.14), 0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0 1px 5px 0 rgba(0, 0, 0, 0.12), 0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.2) 0px 1px 3px 0px rgba(0, 0, 0, 0.12)
); );
color: var(--primary-text-color); color: var(--primary-text-color);
display: block; display: block;
@ -32,6 +34,16 @@ class HaCard extends LitElement {
position: relative; position: relative;
} }
:host([outlined]) {
box-shadow: 0 0 0 0;
border-width: 1px;
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
}
.card-header, .card-header,
:host ::slotted(.card-header) { :host ::slotted(.card-header) {
color: var(--ha-card-header-color, --primary-text-color); color: var(--ha-card-header-color, --primary-text-color);

View File

@ -13,7 +13,7 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
<mwc-icon-button <mwc-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")} aria-label=${hass.localize("ui.dialogs.generic.close")}
dialogAction="close" dialogAction="close"
class="close_button" class="header_button"
> >
<ha-svg-icon path=${mdiClose}></ha-svg-icon> <ha-svg-icon path=${mdiClose}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
@ -25,6 +25,9 @@ export class HaDialog extends MwcDialog {
return [ return [
style, style,
css` css`
.mdc-dialog {
z-index: var(--dialog-z-index, 7);
}
.mdc-dialog__actions { .mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end); justify-content: var(--justify-action-buttons, flex-end);
} }
@ -35,10 +38,15 @@ export class HaDialog extends MwcDialog {
display: block; display: block;
height: 20px; height: 20px;
} }
.close_button { .mdc-dialog__content {
padding: var(--dialog-content-padding, 20px 24px);
}
.header_button {
position: absolute; position: absolute;
right: 16px; right: 16px;
top: 12px; top: 12px;
text-decoration: none;
color: inherit;
} }
`, `,
]; ];

View File

@ -0,0 +1,33 @@
import "@material/mwc-formfield";
import type { Formfield } from "@material/mwc-formfield";
import { style } from "@material/mwc-formfield/mwc-formfield-css";
import { css, CSSResult, customElement } from "lit-element";
import { Constructor } from "../types";
const MwcFormfield = customElements.get("mwc-formfield") as Constructor<
Formfield
>;
@customElement("ha-formfield")
export class HaFormfield extends MwcFormfield {
protected static get styles(): CSSResult[] {
return [
style,
css`
::slotted(ha-switch) {
margin-right: 10px;
}
[dir="rtl"] ::slotted(ha-switch),
::slotted(ha-switch)[dir="rtl"] {
margin-left: 10px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-formfield": HaFormfield;
}
}

View File

@ -1,17 +1,37 @@
import { HaIconButton } from "./ha-icon-button"; import {
LitElement,
property,
TemplateResult,
html,
customElement,
} from "lit-element";
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-arrow-next")
export class HaIconButtonArrowNext extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property() private _icon = mdiArrowRight;
export class HaIconButtonArrowNext extends HaIconButton {
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL // wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => { setTimeout(() => {
this.icon = this._icon =
window.getComputedStyle(this).direction === "ltr" window.getComputedStyle(this).direction === "ltr"
? "hass:arrow-right" ? mdiArrowRight
: "hass:arrow-left"; : mdiArrowLeft;
}, 100); }, 100);
} }
protected render(): TemplateResult {
return html`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}
} }
declare global { declare global {
@ -19,5 +39,3 @@ declare global {
"ha-icon-button-arrow-next": HaIconButtonArrowNext; "ha-icon-button-arrow-next": HaIconButtonArrowNext;
} }
} }
customElements.define("ha-icon-button-arrow-next", HaIconButtonArrowNext);

View File

@ -1,8 +1,15 @@
import { LitElement, property, TemplateResult, html } from "lit-element"; import {
LitElement,
property,
TemplateResult,
html,
customElement,
} from "lit-element";
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js"; import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
@customElement("ha-icon-button-arrow-prev")
export class HaIconButtonArrowPrev extends LitElement { export class HaIconButtonArrowPrev extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@ -32,5 +39,3 @@ declare global {
"ha-icon-button-arrow-prev": HaIconButtonArrowPrev; "ha-icon-button-arrow-prev": HaIconButtonArrowPrev;
} }
} }
customElements.define("ha-icon-button-arrow-prev", HaIconButtonArrowPrev);

View File

@ -1,17 +1,37 @@
import { HaIconButton } from "./ha-icon-button"; import {
LitElement,
property,
TemplateResult,
html,
customElement,
} from "lit-element";
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
import "@material/mwc-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-next")
export class HaIconButtonNext extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property() private _icon = mdiChevronRight;
export class HaIconButtonNext extends HaIconButton {
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL // wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => { setTimeout(() => {
this.icon = this._icon =
window.getComputedStyle(this).direction === "ltr" window.getComputedStyle(this).direction === "ltr"
? "hass:chevron-right" ? mdiChevronRight
: "hass:chevron-left"; : mdiChevronLeft;
}, 100); }, 100);
} }
protected render(): TemplateResult {
return html`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}
} }
declare global { declare global {
@ -19,5 +39,3 @@ declare global {
"ha-icon-button-next": HaIconButtonNext; "ha-icon-button-next": HaIconButtonNext;
} }
} }
customElements.define("ha-icon-button-next", HaIconButtonNext);

View File

@ -1,17 +1,37 @@
import { HaIconButton } from "./ha-icon-button"; import {
LitElement,
property,
TemplateResult,
html,
customElement,
} from "lit-element";
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-prev")
export class HaIconButtonPrev extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property() private _icon = mdiChevronLeft;
export class HaIconButtonPrev extends HaIconButton {
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL // wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => { setTimeout(() => {
this.icon = this._icon =
window.getComputedStyle(this).direction === "ltr" window.getComputedStyle(this).direction === "ltr"
? "hass:chevron-left" ? mdiChevronLeft
: "hass:chevron-right"; : mdiChevronRight;
}, 100); }, 100);
} }
protected render(): TemplateResult {
return html`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}
} }
declare global { declare global {
@ -19,5 +39,3 @@ declare global {
"ha-icon-button-prev": HaIconButtonPrev; "ha-icon-button-prev": HaIconButtonPrev;
} }
} }
customElements.define("ha-icon-button-prev", HaIconButtonPrev);

View File

@ -13,7 +13,7 @@ class HaMarkdownElement extends UpdatingElement {
protected update(changedProps) { protected update(changedProps) {
super.update(changedProps); super.update(changedProps);
if (this.content !== undefined) { if (this.content !== undefined) {
this._render(); this._render();
} }
} }

View File

@ -1,3 +1,5 @@
import "@material/mwc-icon-button";
import { mdiMenu } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
@ -12,8 +14,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { subscribeNotifications } from "../data/persistent_notification"; import { subscribeNotifications } from "../data/persistent_notification";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-icon-button"; import "./ha-svg-icon";
import { mdiMenu } from "@mdi/js";
@customElement("ha-menu-button") @customElement("ha-menu-button")
class HaMenuButton extends LitElement { class HaMenuButton extends LitElement {

View File

@ -1,6 +1,12 @@
import { mdiBell, mdiCellphoneSettingsVariant } from "@mdi/js"; import "@material/mwc-icon-button";
import {
mdiBell,
mdiCellphoneSettingsVariant,
mdiMenuOpen,
mdiMenu,
mdiViewDashboard,
} from "@mdi/js";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "./ha-icon-button";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item"; import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
@ -29,9 +35,9 @@ import {
getExternalConfig, getExternalConfig,
} from "../external_app/external_config"; } from "../external_app/external_config";
import type { HomeAssistant, PanelInfo } from "../types"; import type { HomeAssistant, PanelInfo } from "../types";
import "./ha-svg-icon";
import "./ha-icon"; import "./ha-icon";
import "./ha-menu-button"; import "./ha-menu-button";
import "./ha-svg-icon";
import "./user/ha-user-badge"; import "./user/ha-user-badge";
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"]; const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
@ -153,13 +159,16 @@ class HaSidebar extends LitElement {
<div class="menu"> <div class="menu">
${!this.narrow ${!this.narrow
? html` ? html`
<ha-icon-button <mwc-icon-button
aria-label=${hass.localize("ui.sidebar.sidebar_toggle")} .label=${hass.localize("ui.sidebar.sidebar_toggle")}
.icon=${hass.dockedSidebar === "docked"
? "hass:menu-open"
: "hass:menu"}
@click=${this._toggleSidebar} @click=${this._toggleSidebar}
></ha-icon-button> >
<ha-svg-icon
.path=${hass.dockedSidebar === "docked"
? mdiMenuOpen
: mdiMenu}
></ha-svg-icon>
</mwc-icon-button>
` `
: ""} : ""}
<span class="title">Home Assistant</span> <span class="title">Home Assistant</span>
@ -174,14 +183,16 @@ class HaSidebar extends LitElement {
> >
${this._renderPanel( ${this._renderPanel(
defaultPanel.url_path, defaultPanel.url_path,
defaultPanel.icon || "hass:view-dashboard", defaultPanel.title || hass.localize("panel.states"),
defaultPanel.title || hass.localize("panel.states") defaultPanel.icon,
!defaultPanel.icon ? mdiViewDashboard : undefined
)} )}
${beforeSpacer.map((panel) => ${beforeSpacer.map((panel) =>
this._renderPanel( this._renderPanel(
panel.url_path, panel.url_path,
hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon, panel.icon,
hass.localize(`panel.${panel.title}`) || panel.title undefined
) )
)} )}
<div class="spacer" disabled></div> <div class="spacer" disabled></div>
@ -189,8 +200,9 @@ class HaSidebar extends LitElement {
${afterSpacer.map((panel) => ${afterSpacer.map((panel) =>
this._renderPanel( this._renderPanel(
panel.url_path, panel.url_path,
hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon, panel.icon,
hass.localize(`panel.${panel.title}`) || panel.title undefined
) )
)} )}
${this._externalConfig && this._externalConfig.hasSettingsScreen ${this._externalConfig && this._externalConfig.hasSettingsScreen
@ -443,7 +455,12 @@ class HaSidebar extends LitElement {
fireEvent(this, "hass-toggle-menu"); fireEvent(this, "hass-toggle-menu");
} }
private _renderPanel(urlPath, icon, title) { private _renderPanel(
urlPath: string,
title: string | null,
icon?: string | null,
iconPath?: string | null
) {
return html` return html`
<a <a
aria-role="option" aria-role="option"
@ -454,7 +471,12 @@ class HaSidebar extends LitElement {
@mouseleave=${this._itemMouseLeave} @mouseleave=${this._itemMouseLeave}
> >
<paper-icon-item> <paper-icon-item>
<ha-icon slot="item-icon" .icon="${icon}"></ha-icon> ${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span> <span class="item-text">${title}</span>
</paper-icon-item> </paper-icon-item>
</a> </a>
@ -496,13 +518,13 @@ class HaSidebar extends LitElement {
width: 256px; width: 256px;
} }
.menu ha-icon-button { .menu mwc-icon-button {
color: var(--sidebar-icon-color); color: var(--sidebar-icon-color);
} }
:host([expanded]) .menu ha-icon-button { :host([expanded]) .menu mwc-icon-button {
margin-right: 23px; margin-right: 23px;
} }
:host([expanded][_rtl]) .menu ha-icon-button { :host([expanded][_rtl]) .menu mwc-icon-button {
margin-right: 0px; margin-right: 0px;
margin-left: 23px; margin-left: 23px;
} }
@ -714,7 +736,7 @@ class HaSidebar extends LitElement {
font-weight: 500; font-weight: 500;
} }
:host([_rtl]) .menu ha-icon-button { :host([_rtl]) .menu mwc-icon-button {
-webkit-transform: scaleX(-1); -webkit-transform: scaleX(-1);
transform: scaleX(-1); transform: scaleX(-1);
} }

View File

@ -1,15 +1,7 @@
import { ripple } from "@material/mwc-ripple/ripple-directive";
import "@material/mwc-switch"; import "@material/mwc-switch";
import type { Switch } from "@material/mwc-switch"; import type { Switch } from "@material/mwc-switch";
import { style } from "@material/mwc-switch/mwc-switch-css"; import { style } from "@material/mwc-switch/mwc-switch-css";
import { import { css, CSSResult, customElement, property } from "lit-element";
css,
CSSResult,
customElement,
html,
property,
query,
} from "lit-element";
import { forwardHaptic } from "../data/haptics"; import { forwardHaptic } from "../data/haptics";
import { Constructor } from "../types"; import { Constructor } from "../types";
@ -22,18 +14,12 @@ export class HaSwitch extends MwcSwitch {
// Do not add haptic when a user is required to press save. // Do not add haptic when a user is required to press save.
@property({ type: Boolean }) public haptic = false; @property({ type: Boolean }) public haptic = false;
@query("slot") private _slot!: HTMLSlotElement;
protected firstUpdated() { protected firstUpdated() {
super.firstUpdated(); super.firstUpdated();
this.style.setProperty( this.style.setProperty(
"--mdc-theme-secondary", "--mdc-theme-secondary",
"var(--switch-checked-color)" "var(--switch-checked-color)"
); );
this.classList.toggle(
"slotted",
Boolean(this._slot.assignedNodes().length)
);
this.addEventListener("change", () => { this.addEventListener("change", () => {
if (this.haptic) { if (this.haptic) {
forwardHaptic("light"); forwardHaptic("light");
@ -41,40 +27,10 @@ export class HaSwitch extends MwcSwitch {
}); });
} }
protected render() {
return html`
<div class="mdc-switch">
<div class="mdc-switch__track"></div>
<div
class="mdc-switch__thumb-underlay"
.ripple="${ripple({
interactionNode: this,
})}"
>
<div class="mdc-switch__thumb">
<input
type="checkbox"
id="basic-switch"
class="mdc-switch__native-control"
role="switch"
@change="${this._haChangeHandler}"
/>
</div>
</div>
</div>
<label for="basic-switch"><slot></slot></label>
`;
}
protected static get styles(): CSSResult[] { protected static get styles(): CSSResult[] {
return [ return [
style, style,
css` css`
:host {
display: flex;
flex-direction: row;
align-items: center;
}
.mdc-switch.mdc-switch--checked .mdc-switch__thumb { .mdc-switch.mdc-switch--checked .mdc-switch__thumb {
background-color: var(--switch-checked-button-color); background-color: var(--switch-checked-button-color);
border-color: var(--switch-checked-button-color); border-color: var(--switch-checked-button-color);
@ -91,18 +47,9 @@ export class HaSwitch extends MwcSwitch {
background-color: var(--switch-unchecked-track-color); background-color: var(--switch-unchecked-track-color);
border-color: var(--switch-unchecked-track-color); border-color: var(--switch-unchecked-track-color);
} }
:host(.slotted) .mdc-switch {
margin-right: 24px;
}
`, `,
]; ];
} }
private _haChangeHandler(e: Event) {
this.mdcFoundation.handleChange(e);
// catch "click" event and sync properties
this.checked = this.formElement.checked;
}
} }
declare global { declare global {

View File

@ -1,46 +0,0 @@
/*
Wrapper for paper-textarea.
paper-textarea crashes on iOS when created programmatically. This only impacts
our automation and script editors as they are using Preact. Polymer is using
template elements and does not have this issue.
paper-textarea issue: https://github.com/PolymerElements/paper-input/issues/556
WebKit issue: https://bugs.webkit.org/show_bug.cgi?id=174629
*/
import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
class HaTextarea extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
</style>
<paper-textarea
label="[[label]]"
placeholder="[[placeholder]]"
value="{{value}}"
></paper-textarea>
`;
}
static get properties() {
return {
name: String,
label: String,
placeholder: String,
value: {
type: String,
notify: true,
},
};
}
}
customElements.define("ha-textarea", HaTextarea);

View File

@ -12,6 +12,11 @@ export interface ConfigUpdateValues {
internal_url?: string | null; internal_url?: string | null;
} }
export interface CheckConfigResult {
result: "valid" | "invalid";
errors: string | null;
}
export const saveCoreConfig = ( export const saveCoreConfig = (
hass: HomeAssistant, hass: HomeAssistant,
values: Partial<ConfigUpdateValues> values: Partial<ConfigUpdateValues>
@ -25,3 +30,6 @@ export const detectCoreConfig = (hass: HomeAssistant) =>
hass.callWS<Partial<ConfigUpdateValues>>({ hass.callWS<Partial<ConfigUpdateValues>>({
type: "config/core/detect", type: "config/core/detect",
}); });
export const checkCoreConfig = (hass: HomeAssistant) =>
hass.callApi<CheckConfigResult>("POST", "config/core/check_config");

View File

@ -11,6 +11,7 @@ import {
} from "lit-element"; } from "lit-element";
import "../../components/dialog/ha-paper-dialog"; import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-switch"; import "../../components/ha-switch";
import "../../components/ha-formfield";
import type { HaSwitch } from "../../components/ha-switch"; import type { HaSwitch } from "../../components/ha-switch";
import { import {
getConfigEntrySystemOptions, getConfigEntrySystemOptions,
@ -82,13 +83,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
? html` <div class="error">${this._error}</div> ` ? html` <div class="error">${this._error}</div> `
: ""} : ""}
<div class="form"> <div class="form">
<ha-switch <ha-formfield
.checked=${!this._disableNewEntities} .label=${html`<p>
@change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting}
>
<div>
<p>
${this.hass.localize( ${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_label" "ui.dialogs.config_entry_system_options.enable_new_entities_label"
)} )}
@ -101,9 +97,15 @@ class DialogConfigEntrySystemOptions extends LitElement {
`component.${this._params.entry.domain}.title` `component.${this._params.entry.domain}.title`
) || this._params.entry.domain ) || this._params.entry.domain
)} )}
</p> </p>`}
</div> >
</ha-switch> <ha-switch
.checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting}
>
</ha-switch>
</ha-formfield>
</div> </div>
`} `}
</paper-dialog-scrollable> </paper-dialog-scrollable>
@ -172,9 +174,6 @@ class DialogConfigEntrySystemOptions extends LitElement {
padding-bottom: 24px; padding-bottom: 24px;
color: var(--primary-text-color); color: var(--primary-text-color);
} }
p {
margin: 0;
}
.secondary { .secondary {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }

View File

@ -14,8 +14,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../components/dialog/ha-paper-dialog"; import "../../components/ha-dialog";
import type { HaPaperDialog } from "../../components/dialog/ha-paper-dialog";
import "../../components/ha-form/ha-form"; import "../../components/ha-form/ha-form";
import "../../components/ha-markdown"; import "../../components/ha-markdown";
import { import {
@ -27,7 +26,6 @@ import {
DeviceRegistryEntry, DeviceRegistryEntry,
subscribeDeviceRegistry, subscribeDeviceRegistry,
} from "../../data/device_registry"; } from "../../data/device_registry";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow"; import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
@ -90,7 +88,6 @@ class DataEntryFlowDialog extends LitElement {
// We only load the handlers once // We only load the handlers once
if (this._handlers === undefined) { if (this._handlers === undefined) {
this._loading = true; this._loading = true;
this.updateComplete.then(() => this._scheduleCenterDialog());
try { try {
this._handlers = await params.flowConfig.getFlowHandlers(this.hass); this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
} finally { } finally {
@ -98,7 +95,6 @@ class DataEntryFlowDialog extends LitElement {
} }
} }
await this.updateComplete; await this.updateComplete;
this._scheduleCenterDialog();
return; return;
} }
@ -115,9 +111,6 @@ class DataEntryFlowDialog extends LitElement {
this._processStep(step); this._processStep(step);
this._loading = false; this._loading = false;
// When the flow changes, center the dialog.
// Don't do it on each step or else the dialog keeps bouncing.
this._scheduleCenterDialog();
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@ -126,80 +119,84 @@ class DataEntryFlowDialog extends LitElement {
} }
return html` return html`
<ha-paper-dialog <ha-dialog
with-backdrop open
opened @closing=${this._close}
modal scrimClickAction
@opened-changed=${this._openedChanged} escapeKeyAction
hideActions
> >
${this._loading || (this._step === null && this._handlers === undefined) <div>
? html` ${this._loading ||
<step-flow-loading (this._step === null && this._handlers === undefined)
.label=${this.hass.localize( ? html`
"ui.panel.config.integrations.config_flow.loading_first_time" <step-flow-loading
)} .label=${this.hass.localize(
></step-flow-loading> "ui.panel.config.integrations.config_flow.loading_first_time"
` )}
: this._step === undefined ></step-flow-loading>
? // When we are going to next step, we render 1 round of empty `
// to reset the element. : this._step === undefined
"" ? // When we are going to next step, we render 1 round of empty
: html` // to reset the element.
<ha-icon-button ""
aria-label=${this.hass.localize( : html`
"ui.panel.config.integrations.config_flow.dismiss" <ha-icon-button
)} aria-label=${this.hass.localize(
icon="hass:close" "ui.panel.config.integrations.config_flow.dismiss"
dialog-dismiss )}
></ha-icon-button> icon="hass:close"
${this._step === null dialogAction="close"
? // Show handler picker ></ha-icon-button>
html` ${this._step === null
<step-flow-pick-handler ? // Show handler picker
.flowConfig=${this._params.flowConfig} html`
.hass=${this.hass} <step-flow-pick-handler
.handlers=${this._handlers} .flowConfig=${this._params.flowConfig}
.showAdvanced=${this._params.showAdvanced} .hass=${this.hass}
></step-flow-pick-handler> .handlers=${this._handlers}
` .showAdvanced=${this._params.showAdvanced}
: this._step.type === "form" ></step-flow-pick-handler>
? html` `
<step-flow-form : this._step.type === "form"
.flowConfig=${this._params.flowConfig} ? html`
.step=${this._step} <step-flow-form
.hass=${this.hass} .flowConfig=${this._params.flowConfig}
></step-flow-form> .step=${this._step}
` .hass=${this.hass}
: this._step.type === "external" ></step-flow-form>
? html` `
<step-flow-external : this._step.type === "external"
.flowConfig=${this._params.flowConfig} ? html`
.step=${this._step} <step-flow-external
.hass=${this.hass} .flowConfig=${this._params.flowConfig}
></step-flow-external> .step=${this._step}
` .hass=${this.hass}
: this._step.type === "abort" ></step-flow-external>
? html` `
<step-flow-abort : this._step.type === "abort"
.flowConfig=${this._params.flowConfig} ? html`
.step=${this._step} <step-flow-abort
.hass=${this.hass} .flowConfig=${this._params.flowConfig}
></step-flow-abort> .step=${this._step}
` .hass=${this.hass}
: this._devices === undefined || this._areas === undefined ></step-flow-abort>
? // When it's a create entry result, we will fetch device & area registry `
html` <step-flow-loading></step-flow-loading> ` : this._devices === undefined || this._areas === undefined
: html` ? // When it's a create entry result, we will fetch device & area registry
<step-flow-create-entry html` <step-flow-loading></step-flow-loading> `
.flowConfig=${this._params.flowConfig} : html`
.step=${this._step} <step-flow-create-entry
.hass=${this.hass} .flowConfig=${this._params.flowConfig}
.devices=${this._devices} .step=${this._step}
.areas=${this._areas} .hass=${this.hass}
></step-flow-create-entry> .devices=${this._devices}
`} .areas=${this._areas}
`} ></step-flow-create-entry>
</ha-paper-dialog> `}
`}
</div>
</ha-dialog>
`; `;
} }
@ -225,18 +222,6 @@ class DataEntryFlowDialog extends LitElement {
this._areas = []; this._areas = [];
} }
} }
if (changedProps.has("_devices") && this._dialog) {
this._scheduleCenterDialog();
}
}
private _scheduleCenterDialog() {
setTimeout(() => this._dialog.center(), 0);
}
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
} }
private async _fetchDevices(configEntryId) { private async _fetchDevices(configEntryId) {
@ -310,16 +295,13 @@ class DataEntryFlowDialog extends LitElement {
} }
} }
private _openedChanged(ev: PolymerChangedEvent<boolean>): void { private _close(): void {
// Closed dialog by clicking on the overlay if (this._step) {
if (!ev.detail.value) { this._flowDone();
if (this._step) { } else if (this._step === null) {
this._flowDone(); // Flow aborted during picking flow
} else if (this._step === null) { this._step = undefined;
// Flow aborted during picking flow this._params = undefined;
this._step = undefined;
this._params = undefined;
}
} }
} }
@ -327,18 +309,14 @@ class DataEntryFlowDialog extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-paper-dialog { ha-dialog {
max-width: 600px; --dialog-content-padding: 0;
}
ha-paper-dialog > * {
margin: 0;
display: block;
padding: 0;
} }
ha-icon-button { ha-icon-button {
display: inline-block; padding: 16px;
padding: 8px; position: absolute;
float: right; top: 0;
right: 0;
} }
`, `,
]; ];

View File

@ -2,8 +2,21 @@ import { css } from "lit-element";
export const configFlowContentStyles = css` export const configFlowContentStyles = css`
h2 { h2 {
margin-top: 24px; margin: 24px 0 0;
padding: 0 24px; padding: 0 24px;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-family: var(
--mdc-typography-headline6-font-family,
var(--mdc-typography-font-family, Roboto, sans-serif)
);
font-size: var(--mdc-typography-headline6-font-size, 1.25rem);
line-height: var(--mdc-typography-headline6-line-height, 2rem);
font-weight: var(--mdc-typography-headline6-font-weight, 500);
letter-spacing: var(--mdc-typography-headline6-letter-spacing, 0.0125em);
text-decoration: var(--mdc-typography-headline6-text-decoration, inherit);
text-transform: var(--mdc-typography-headline6-text-transform, inherit);
box-sizing: border-box;
} }
.content { .content {

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { import {
css, css,
@ -11,7 +10,7 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import "../../components/dialog/ha-paper-dialog"; import "../../components/ha-dialog";
import "../../components/ha-switch"; import "../../components/ha-switch";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
@ -41,21 +40,17 @@ class DialogBox extends LitElement {
const confirmPrompt = this._params.confirmation || this._params.prompt; const confirmPrompt = this._params.confirmation || this._params.prompt;
return html` return html`
<ha-paper-dialog <ha-dialog
with-backdrop open
opened scrimClickAction
modal escapeKeyAction
@opened-changed="${this._openedChanged}" @close=${this._close}
.heading=${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize("ui.dialogs.generic.default_confirmation_title")}
> >
<h2> <div>
${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize(
"ui.dialogs.generic.default_confirmation_title"
)}
</h2>
<paper-dialog-scrollable>
${this._params.text ${this._params.text
? html` ? html`
<p <p
@ -83,23 +78,21 @@ class DialogBox extends LitElement {
></paper-input> ></paper-input>
` `
: ""} : ""}
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${confirmPrompt &&
html`
<mwc-button @click="${this._dismiss}">
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button>
`}
<mwc-button @click="${this._confirm}">
${this._params.confirmText
? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")}
</mwc-button>
</div> </div>
</ha-paper-dialog> ${confirmPrompt &&
html`
<mwc-button @click=${this._dismiss} slot="secondaryAction">
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button>
`}
<mwc-button @click=${this._confirm} slot="primaryAction">
${this._params.confirmText
? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")}
</mwc-button>
</ha-dialog>
`; `;
} }
@ -127,10 +120,8 @@ class DialogBox extends LitElement {
this._dismiss(); this._dismiss();
} }
private _openedChanged(ev: PolymerChangedEvent<boolean>): void { private _close(): void {
if (!(ev.detail as any).value) { this._params = undefined;
this._params = undefined;
}
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -141,15 +132,6 @@ class DialogBox extends LitElement {
pointer-events: initial !important; pointer-events: initial !important;
cursor: initial !important; cursor: initial !important;
} }
ha-paper-dialog {
min-width: 400px;
max-width: 500px;
}
@media (max-width: 400px) {
ha-paper-dialog {
min-width: initial;
}
}
a { a {
color: var(--primary-color); color: var(--primary-color);
} }
@ -165,6 +147,10 @@ class DialogBox extends LitElement {
.secondary { .secondary {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-dialog {
/* Place above other dialogs */
--dialog-z-index: 104;
}
`, `,
]; ];
} }

View File

@ -11,6 +11,7 @@ import {
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
import "../../components/ha-switch"; import "../../components/ha-switch";
import "../../components/ha-formfield";
import type { HaSwitch } from "../../components/ha-switch"; import type { HaSwitch } from "../../components/ha-switch";
import { computeDeviceName } from "../../data/device_registry"; import { computeDeviceName } from "../../data/device_registry";
import { fetchMQTTDebugInfo, MQTTDeviceDebugInfo } from "../../data/mqtt"; import { fetchMQTTDebugInfo, MQTTDeviceDebugInfo } from "../../data/mqtt";
@ -61,22 +62,28 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
"ui.dialogs.mqtt_device_debug_info.payload_display" "ui.dialogs.mqtt_device_debug_info.payload_display"
)} )}
</h4> </h4>
<ha-switch <ha-formfield
.checked=${this._showDeserialized} .label=${this.hass!.localize(
@change=${this._showDeserializedChanged}
>
${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.deserialize" "ui.dialogs.mqtt_device_debug_info.deserialize"
)} )}
</ha-switch>
<ha-switch
.checked=${this._showAsYaml}
@change=${this._showAsYamlChanged}
> >
${this.hass!.localize( <ha-switch
.checked=${this._showDeserialized}
@change=${this._showDeserializedChanged}
>
</ha-switch>
</ha-formfield>
<ha-formfield
.label=${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.show_as_yaml" "ui.dialogs.mqtt_device_debug_info.show_as_yaml"
)} )}
</ha-switch> >
<ha-switch
.checked=${this._showAsYaml}
@change=${this._showAsYamlChanged}
>
</ha-switch>
</ha-formfield>
<h4> <h4>
${this.hass!.localize("ui.dialogs.mqtt_device_debug_info.entities")} ${this.hass!.localize("ui.dialogs.mqtt_device_debug_info.entities")}
</h4> </h4>

View File

@ -1,113 +0,0 @@
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../components/dialog/ha-paper-dialog";
import type { HaPaperDialog } from "../../components/dialog/ha-paper-dialog";
import { fetchZHADevice, ZHADevice } from "../../data/zha";
import "../../panels/config/zha/zha-device-card";
import type { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { ZHADeviceInfoDialogParams } from "./show-dialog-zha-device-info";
@customElement("dialog-zha-device-info")
class DialogZHADeviceInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _params?: ZHADeviceInfoDialogParams;
@property() private _error?: string;
@property() private _device?: ZHADevice;
public async showDialog(params: ZHADeviceInfoDialogParams): Promise<void> {
this._params = params;
this._device = await fetchZHADevice(this.hass, params.ieee);
await this.updateComplete;
this._dialog.open();
}
protected render(): TemplateResult {
if (!this._params || !this._device) {
return html``;
}
return html`
<ha-paper-dialog
with-backdrop
opened
@opened-changed=${this._openedChanged}
>
${this._error
? html` <div class="error">${this._error}</div> `
: html`
<zha-device-card
class="card"
.hass=${this.hass}
.device=${this._device}
@zha-device-removed=${this._onDeviceRemoved}
.showEntityDetail=${false}
.showActions="${this._device.device_type !== "Coordinator"}"
></zha-device-card>
`}
</ha-paper-dialog>
`;
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!ev.detail.value) {
this._params = undefined;
this._error = undefined;
this._device = undefined;
}
}
private _onDeviceRemoved(): void {
this._closeDialog();
}
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
private _closeDialog() {
this._dialog.close();
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-paper-dialog > * {
margin: 0;
display: block;
padding: 0;
}
.card {
box-sizing: border-box;
display: flex;
flex: 1 0 300px;
min-width: 0;
max-width: 600px;
word-wrap: break-word;
}
.error {
color: var(--google-red-500);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-zha-device-info": DialogZHADeviceInfo;
}
}

View File

@ -1,21 +0,0 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface ZHADeviceInfoDialogParams {
ieee: string;
}
export const loadZHADeviceInfoDialog = () =>
import(
/* webpackChunkName: "dialog-zha-device-info" */ "./dialog-zha-device-info"
);
export const showZHADeviceInfoDialog = (
element: HTMLElement,
zhaDeviceInfoParams: ZHADeviceInfoDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-zha-device-info",
dialogImport: loadZHADeviceInfoDialog,
dialogParams: zhaDeviceInfoParams,
});
};

View File

@ -47,7 +47,6 @@
(function() { (function() {
// Safari 10.1 supports type=module but ignores nomodule, so we add this check. // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) { if (!isS101) {
_ls("/static/polyfills/custom-elements-es5-adapter.js");
<% if (useRollup) { %> <% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() { _ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5PageJS %>"); System.import("<%= es5PageJS %>");

View File

@ -66,7 +66,6 @@
(function() { (function() {
if (!window.latestJS) { if (!window.latestJS) {
window.customPanelJS = "<%= es5CustomPanelJS %>"; window.customPanelJS = "<%= es5CustomPanelJS %>";
_ls("/static/polyfills/custom-elements-es5-adapter.js");
<% if (useRollup) { %> <% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() { _ls("/static/js/s.min.js").onload = function() {

View File

@ -49,7 +49,6 @@
(function() { (function() {
// Safari 10.1 supports type=module but ignores nomodule, so we add this check. // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) { if (!isS101) {
_ls("/static/polyfills/custom-elements-es5-adapter.js");
<% if (useRollup) { %> <% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() { _ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5PageJS %>"); System.import("<%= es5PageJS %>");

View File

@ -91,6 +91,20 @@ class PartialPanelResolver extends HassRouterPage {
private _waitForStart = false; private _waitForStart = false;
private _disconnectedPanel?: ChildNode;
private _hiddenTimeout?: number;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
document.addEventListener(
"visibilitychange",
() => this._handleVisibilityChange(),
false
);
}
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
@ -141,6 +155,27 @@ class PartialPanelResolver extends HassRouterPage {
} }
} }
private _handleVisibilityChange() {
if (document.hidden) {
this._hiddenTimeout = window.setTimeout(() => {
this._hiddenTimeout = undefined;
if (this.lastChild) {
this._disconnectedPanel = this.lastChild;
this.removeChild(this.lastChild);
}
}, 300000);
} else {
if (this._hiddenTimeout) {
clearTimeout(this._hiddenTimeout);
this._hiddenTimeout = undefined;
}
if (this._disconnectedPanel) {
this.appendChild(this._disconnectedPanel);
this._disconnectedPanel = undefined;
}
}
}
private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) { private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) {
this.routerOptions = getRoutes(this.hass.panels); this.routerOptions = getRoutes(this.hass.panels);

View File

@ -1,9 +1,12 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "../../../../components/ha-icon-button"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-icon-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-svg-icon";
import { mdiDotsVertical, mdiArrowUp, mdiArrowDown } from "@mdi/js";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox"; import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
import { import {
css, css,
CSSResult, CSSResult,
@ -96,56 +99,64 @@ export default class HaAutomationActionRow extends LitElement {
<div class="card-menu"> <div class="card-menu">
${this.index !== 0 ${this.index !== 0
? html` ? html`
<ha-icon-button <mwc-icon-button
icon="hass:arrow-up" .title=${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
@click=${this._moveUp} @click=${this._moveUp}
></ha-icon-button> >
<ha-svg-icon path=${mdiArrowUp}></ha-svg-icon>
</mwc-icon-button>
` `
: ""} : ""}
${this.index !== this.totalActions - 1 ${this.index !== this.totalActions - 1
? html` ? html`
<ha-icon-button <mwc-icon-button
icon="hass:arrow-down" .title=${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
@click=${this._moveDown} @click=${this._moveDown}
></ha-icon-button> >
<ha-svg-icon path=${mdiArrowDown}></ha-svg-icon>
</mwc-icon-button>
` `
: ""} : ""}
<paper-menu-button <ha-button-menu corner="BOTTOM_START">
no-animations <mwc-icon-button
horizontal-align="right" slot="trigger"
horizontal-offset="-5" .title=${this.hass.localize("ui.common.menu")}
vertical-offset="-5" .label=${this.hass.localize("ui.common.overflow_menu")}
close-on-activate ><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
> </mwc-icon-button>
<ha-icon-button <mwc-list-item
icon="hass:dots-vertical" @tap=${this._switchYamlMode}
slot="dropdown-trigger" .disabled=${selected === -1}
></ha-icon-button> >
<paper-listbox slot="dropdown-content"> ${yamlMode
<paper-item ? this.hass.localize(
@tap=${this._switchYamlMode} "ui.panel.config.automation.editor.edit_ui"
.disabled=${selected === -1} )
> : this.hass.localize(
${yamlMode "ui.panel.config.automation.editor.edit_yaml"
? this.hass.localize( )}
"ui.panel.config.automation.editor.edit_ui" </mwc-list-item>
) <mwc-list-item disabled>
: this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml" "ui.panel.config.automation.editor.actions.duplicate"
)} )}
</paper-item> </mwc-list-item>
<paper-item disabled> <mwc-list-item @tap=${this._onDelete}>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate" "ui.panel.config.automation.editor.actions.delete"
)} )}
</paper-item> </mwc-list-item>
<paper-item @tap=${this._onDelete}> </ha-button-menu>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
</div> </div>
${yamlMode ${yamlMode
? html` ? html`
@ -259,14 +270,17 @@ export default class HaAutomationActionRow extends LitElement {
top: 0; top: 0;
right: 0; right: 0;
z-index: 3; z-index: 3;
color: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
} }
.rtl .card-menu { .rtl .card-menu {
right: auto; right: auto;
left: 0; left: 0;
} }
.card-menu paper-item { ha-button-menu {
cursor: pointer; margin: 8px;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }
`; `;
} }

View File

@ -1,4 +1,5 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { customElement, LitElement, property } from "lit-element"; import { customElement, LitElement, property } from "lit-element";
import { html } from "lit-html"; import { html } from "lit-html";
import { WaitAction } from "../../../../../data/script"; import { WaitAction } from "../../../../../data/script";
@ -19,7 +20,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
const { wait_template, timeout } = this.action; const { wait_template, timeout } = this.action;
return html` return html`
<ha-textarea <paper-textarea
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.wait_template" "ui.panel.config.automation.editor.actions.type.wait_template.wait_template"
)} )}
@ -27,7 +28,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
.value=${wait_template} .value=${wait_template}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
dir="ltr" dir="ltr"
></ha-textarea> ></paper-textarea>
<paper-input <paper-input
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.timeout" "ui.panel.config.automation.editor.actions.type.wait_template.timeout"

View File

@ -1,6 +1,8 @@
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-menu-button/paper-menu-button"; import "@material/mwc-list/mwc-list-item";
import "../../../../components/ha-button-menu";
import { mdiDotsVertical } from "@mdi/js";
import { import {
css, css,
CSSResult, CSSResult,
@ -61,39 +63,33 @@ export default class HaAutomationConditionRow extends LitElement {
<ha-card> <ha-card>
<div class="card-content"> <div class="card-content">
<div class="card-menu"> <div class="card-menu">
<paper-menu-button <ha-button-menu corner="BOTTOM_START">
no-animations <mwc-icon-button
horizontal-align="right" .title=${this.hass.localize("ui.common.menu")}
horizontal-offset="-5" .label=${this.hass.localize("ui.common.overflow_menu")}
vertical-offset="-5" slot="trigger"
close-on-activate ><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon
> ></mwc-icon-button>
<ha-icon-button <mwc-list-item @tap=${this._switchYamlMode}>
icon="hass:dots-vertical" ${this._yamlMode
slot="dropdown-trigger" ? this.hass.localize(
></ha-icon-button> "ui.panel.config.automation.editor.edit_ui"
<paper-listbox slot="dropdown-content"> )
<paper-item @tap=${this._switchYamlMode}> : this.hass.localize(
${this._yamlMode "ui.panel.config.automation.editor.edit_yaml"
? this.hass.localize( )}
"ui.panel.config.automation.editor.edit_ui" </mwc-list-item>
) <mwc-list-item disabled>
: this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml" "ui.panel.config.automation.editor.actions.duplicate"
)} )}
</paper-item> </mwc-list-item>
<paper-item disabled> <mwc-list-item @tap=${this._onDelete}>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.duplicate" "ui.panel.config.automation.editor.actions.delete"
)} )}
</paper-item> </mwc-list-item>
<paper-item @tap=${this._onDelete}> </ha-button-menu>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.delete"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
</div> </div>
<ha-automation-condition-editor <ha-automation-condition-editor
.yamlMode=${this._yamlMode} .yamlMode=${this._yamlMode}
@ -129,14 +125,17 @@ export default class HaAutomationConditionRow extends LitElement {
top: 0; top: 0;
right: 0; right: 0;
z-index: 3; z-index: 3;
color: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
} }
.rtl .card-menu { .rtl .card-menu {
right: auto; right: auto;
left: 0; left: 0;
} }
.card-menu paper-item { ha-button-menu {
cursor: pointer; margin: 8px;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }
`; `;
} }

View File

@ -2,7 +2,7 @@ import "@polymer/paper-input/paper-input";
import { customElement, html, LitElement, property } from "lit-element"; import { customElement, html, LitElement, property } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker"; import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-textarea"; import "@polymer/paper-input/paper-textarea";
import { NumericStateCondition } from "../../../../../data/automation"; import { NumericStateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-condition-row"; import { handleChangeEvent } from "../ha-automation-condition-row";
@ -45,7 +45,7 @@ export default class HaNumericStateCondition extends LitElement {
.value=${below} .value=${below}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></paper-input>
<ha-textarea <paper-textarea
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.numeric_state.value_template" "ui.panel.config.automation.editor.conditions.type.numeric_state.value_template"
)} )}
@ -53,7 +53,7 @@ export default class HaNumericStateCondition extends LitElement {
.value=${value_template} .value=${value_template}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
dir="ltr" dir="ltr"
></ha-textarea> ></paper-textarea>
`; `;
} }

View File

@ -1,5 +1,5 @@
import { customElement, html, LitElement, property } from "lit-element"; import { customElement, html, LitElement, property } from "lit-element";
import "../../../../../components/ha-textarea"; import "@polymer/paper-input/paper-textarea";
import { TemplateCondition } from "../../../../../data/automation"; import { TemplateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-condition-row"; import { handleChangeEvent } from "../ha-automation-condition-row";
@ -17,7 +17,7 @@ export class HaTemplateCondition extends LitElement {
protected render() { protected render() {
const { value_template } = this.condition; const { value_template } = this.condition;
return html` return html`
<ha-textarea <paper-textarea
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.template.value_template" "ui.panel.config.automation.editor.conditions.type.template.value_template"
)} )}
@ -25,7 +25,7 @@ export class HaTemplateCondition extends LitElement {
.value=${value_template} .value=${value_template}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
dir="ltr" dir="ltr"
></ha-textarea> ></paper-textarea>
`; `;
} }

View File

@ -1,5 +1,6 @@
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-input/paper-textarea";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import { import {
css, css,
@ -117,7 +118,7 @@ export class HaAutomationEditor extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
</paper-input> </paper-input>
<ha-textarea <paper-textarea
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label" "ui.panel.config.automation.editor.description.label"
)} )}
@ -127,7 +128,7 @@ export class HaAutomationEditor extends LitElement {
name="description" name="description"
.value=${this._config.description} .value=${this._config.description}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-textarea> ></paper-textarea>
</div> </div>
${stateObj ${stateObj
? html` ? html`

View File

@ -2,8 +2,10 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import "@material/mwc-list/mwc-list-item";
import "../../../../components/ha-button-menu";
import { mdiDotsVertical } from "@mdi/js";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox"; import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
import { import {
css, css,
CSSResult, CSSResult,
@ -90,42 +92,36 @@ export default class HaAutomationTriggerRow extends LitElement {
<ha-card> <ha-card>
<div class="card-content"> <div class="card-content">
<div class="card-menu"> <div class="card-menu">
<paper-menu-button <ha-button-menu corner="BOTTOM_START">
no-animations <mwc-icon-button
horizontal-align="right" slot="trigger"
horizontal-offset="-5" .title=${this.hass.localize("ui.common.menu")}
vertical-offset="-5" .label=${this.hass.localize("ui.common.overflow_menu")}
close-on-activate ><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon
> ></mwc-icon-button>
<ha-icon-button <mwc-list-item
icon="hass:dots-vertical" @tap=${this._switchYamlMode}
slot="dropdown-trigger" .disabled=${selected === -1}
></ha-icon-button> >
<paper-listbox slot="dropdown-content"> ${yamlMode
<paper-item ? this.hass.localize(
@tap=${this._switchYamlMode} "ui.panel.config.automation.editor.edit_ui"
.disabled=${selected === -1} )
> : this.hass.localize(
${yamlMode "ui.panel.config.automation.editor.edit_yaml"
? this.hass.localize( )}
"ui.panel.config.automation.editor.edit_ui" </mwc-list-item>
) <mwc-list-item disabled>
: this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml" "ui.panel.config.automation.editor.actions.duplicate"
)} )}
</paper-item> </mwc-list-item>
<paper-item disabled> <mwc-list-item @tap=${this._onDelete}>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate" "ui.panel.config.automation.editor.actions.delete"
)} )}
</paper-item> </mwc-list-item>
<paper-item @tap=${this._onDelete}> </ha-button-menu>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.delete"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
</div> </div>
${yamlMode ${yamlMode
? html` ? html`
@ -232,14 +228,17 @@ export default class HaAutomationTriggerRow extends LitElement {
top: 0; top: 0;
right: 0; right: 0;
z-index: 3; z-index: 3;
color: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
} }
.rtl .card-menu { .rtl .card-menu {
right: auto; right: auto;
left: 0; left: 0;
} }
.card-menu paper-item { ha-button-menu {
cursor: pointer; margin: 8px;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }
`; `;
} }

View File

@ -2,7 +2,7 @@ import "@polymer/paper-input/paper-input";
import { customElement, html, LitElement, property } from "lit-element"; import { customElement, html, LitElement, property } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker"; import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-textarea"; import "@polymer/paper-input/paper-textarea";
import { ForDict, NumericStateTrigger } from "../../../../../data/automation"; import { ForDict, NumericStateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row"; import { handleChangeEvent } from "../ha-automation-trigger-row";
@ -61,7 +61,7 @@ export default class HaNumericStateTrigger extends LitElement {
.value=${below} .value=${below}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></paper-input>
<ha-textarea <paper-textarea
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.value_template" "ui.panel.config.automation.editor.triggers.type.numeric_state.value_template"
)} )}
@ -69,7 +69,7 @@ export default class HaNumericStateTrigger extends LitElement {
.value=${value_template} .value=${value_template}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
dir="ltr" dir="ltr"
></ha-textarea> ></paper-textarea>
<paper-input <paper-input
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for" "ui.panel.config.automation.editor.triggers.type.state.for"

View File

@ -1,5 +1,5 @@
import { customElement, html, LitElement, property } from "lit-element"; import { customElement, html, LitElement, property } from "lit-element";
import "../../../../../components/ha-textarea"; import "@polymer/paper-input/paper-textarea";
import { TemplateTrigger } from "../../../../../data/automation"; import { TemplateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row"; import { handleChangeEvent } from "../ha-automation-trigger-row";
@ -17,7 +17,7 @@ export class HaTemplateTrigger extends LitElement {
protected render() { protected render() {
const { value_template } = this.trigger; const { value_template } = this.trigger;
return html` return html`
<ha-textarea <paper-textarea
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.template.value_template" "ui.panel.config.automation.editor.triggers.type.template.value_template"
)} )}
@ -25,7 +25,7 @@ export class HaTemplateTrigger extends LitElement {
.value=${value_template} .value=${value_template}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
dir="ltr" dir="ltr"
></ha-textarea> ></paper-textarea>
`; `;
} }

View File

@ -33,6 +33,7 @@ import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show
import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage"; import "../../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "../../../../components/ha-formfield";
const DEFAULT_CONFIG_EXPOSE = true; const DEFAULT_CONFIG_EXPOSE = true;
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"]; const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
@ -127,14 +128,19 @@ class CloudAlexa extends LitElement {
) )
.join(", ")} .join(", ")}
</state-info> </state-info>
<ha-switch <ha-formfield
.entityId=${entity.entity_id} .label=${this.hass!.localize(
.disabled=${!emptyFilter} "ui.panel.config.cloud.alexa.expose"
.checked=${isExposed} )}
@change=${this._exposeChanged}
> >
${this.hass!.localize("ui.panel.config.cloud.alexa.expose")} <ha-switch
</ha-switch> .entityId=${entity.entity_id}
.disabled=${!emptyFilter}
.checked=${isExposed}
@change=${this._exposeChanged}
>
</ha-switch>
</ha-formfield>
</div> </div>
</ha-card> </ha-card>
`); `);

View File

@ -38,6 +38,7 @@ import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage"; import "../../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast"; import { showToast } from "../../../../util/toast";
import "../../../../components/ha-formfield";
const DEFAULT_CONFIG_EXPOSE = true; const DEFAULT_CONFIG_EXPOSE = true;
@ -127,14 +128,19 @@ class CloudGoogleAssistant extends LitElement {
.map((trait) => trait.substr(trait.lastIndexOf(".") + 1)) .map((trait) => trait.substr(trait.lastIndexOf(".") + 1))
.join(", ")} .join(", ")}
</state-info> </state-info>
<ha-switch <ha-formfield
.entityId=${entity.entity_id} .label=${this.hass!.localize(
.disabled=${!emptyFilter} "ui.panel.config.cloud.google.expose"
.checked=${isExposed} )}
@change=${this._exposeChanged}
> >
${this.hass!.localize("ui.panel.config.cloud.google.expose")} <ha-switch
</ha-switch> .entityId=${entity.entity_id}
.disabled=${!emptyFilter}
.checked=${isExposed}
@change=${this._exposeChanged}
>
</ha-switch>
</ha-formfield>
${entity.might_2fa ${entity.might_2fa
? html` ? html`
<ha-switch <ha-switch

View File

@ -20,6 +20,7 @@ import { HomeAssistant } from "../../../types";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "./ha-config-navigation"; import "./ha-config-navigation";
import { mdiCloudLock } from "@mdi/js";
@customElement("ha-config-dashboard") @customElement("ha-config-dashboard")
class HaConfigDashboard extends LitElement { class HaConfigDashboard extends LitElement {
@ -66,7 +67,7 @@ class HaConfigDashboard extends LitElement {
path: "/config/cloud", path: "/config/cloud",
translationKey: "ui.panel.config.cloud.caption", translationKey: "ui.panel.config.cloud.caption",
info: this.cloudStatus, info: this.cloudStatus,
icon: "hass:cloud-lock", iconPath: mdiCloudLock,
}, },
]} ]}
></ha-config-navigation> ></ha-config-navigation>

View File

@ -38,7 +38,10 @@ class HaConfigNavigation extends LitElement {
tabindex="-1" tabindex="-1"
> >
<paper-icon-item> <paper-icon-item>
<ha-icon .icon=${page.icon} slot="item-icon"></ha-icon> <ha-svg-icon
.path=${page.iconPath}
slot="item-icon"
></ha-svg-icon>
<paper-item-body two-line> <paper-item-body two-line>
${this.hass.localize( ${this.hass.localize(
page.translationKey || page.translationKey ||
@ -88,7 +91,7 @@ class HaConfigNavigation extends LitElement {
display: block; display: block;
outline: 0; outline: 0;
} }
ha-icon, ha-svg-icon,
ha-icon-next { ha-icon-next {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }

View File

@ -75,6 +75,7 @@ export class HaDeviceCard extends LitElement {
: ""} : ""}
<slot></slot> <slot></slot>
</div> </div>
<slot name="actions"></slot>
</ha-card> </ha-card>
`; `;
} }
@ -100,7 +101,6 @@ export class HaDeviceCard extends LitElement {
} }
ha-card { ha-card {
flex: 1 0 100%; flex: 1 0 100%;
padding-bottom: 10px;
min-width: 0; min-width: 0;
} }
.device { .device {

View File

@ -5,16 +5,17 @@ import {
LitElement, LitElement,
property, property,
TemplateResult, TemplateResult,
css,
} from "lit-element"; } from "lit-element";
import { DeviceRegistryEntry } from "../../../../data/device_registry"; import { DeviceRegistryEntry } from "../../../../../data/device_registry";
import { removeMQTTDeviceEntry } from "../../../../data/mqtt"; import { removeMQTTDeviceEntry } from "../../../../../data/mqtt";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { showMQTTDeviceDebugInfoDialog } from "../../../../dialogs/mqtt-device-debug-info-dialog/show-dialog-mqtt-device-debug-info"; import { showMQTTDeviceDebugInfoDialog } from "../../../../../dialogs/mqtt-device-debug-info-dialog/show-dialog-mqtt-device-debug-info";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../../types";
@customElement("ha-device-card-mqtt") @customElement("ha-device-actions-mqtt")
export class HaDeviceCardMqtt extends LitElement { export class HaDeviceActionsMqtt extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry; @property() public device!: DeviceRegistryEntry;
@ -47,7 +48,15 @@ export class HaDeviceCardMqtt extends LitElement {
await showMQTTDeviceDebugInfoDialog(this, { device }); await showMQTTDeviceDebugInfoDialog(this, { device });
} }
static get styles(): CSSResult { static get styles(): CSSResult[] {
return haStyle; return [
haStyle,
css`
:host {
display: flex;
justify-content: space-between;
}
`,
];
} }
} }

View File

@ -0,0 +1,128 @@
import {
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
css,
PropertyValues,
} from "lit-element";
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import {
ZHADevice,
fetchZHADevice,
reconfigureNode,
} from "../../../../../data/zha";
import { navigate } from "../../../../../common/navigate";
import { showZHADeviceZigbeeInfoDialog } from "../../../../../dialogs/zha-device-zigbee-signature-dialog/show-dialog-zha-device-zigbee-info";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
@customElement("ha-device-actions-zha")
export class HaDeviceActionsZha extends LitElement {
@property() public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@property() private _zhaDevice?: ZHADevice;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const zigbeeConnection = this.device.connections.find(
(conn) => conn[0] === "zigbee"
);
if (!zigbeeConnection) {
return;
}
fetchZHADevice(this.hass, zigbeeConnection[1]).then((device) => {
this._zhaDevice = device;
});
}
}
protected render(): TemplateResult {
if (!this._zhaDevice) {
return html``;
}
return html`
${this._zhaDevice.device_type !== "Coordinator"
? html`
<mwc-button @click=${this._onReconfigureNodeClick}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.reconfigure"
)}
</mwc-button>
`
: ""}
${this._zhaDevice.power_source === "Mains" &&
(this._zhaDevice.device_type === "Router" ||
this._zhaDevice.device_type === "Coordinator")
? html`
<mwc-button @click=${this._onAddDevicesClick}>
${this.hass!.localize("ui.dialogs.zha_device_info.buttons.add")}
</mwc-button>
`
: ""}
${this._zhaDevice.device_type !== "Coordinator"
? html`
<mwc-button @click=${this._handleZigbeeInfoClicked}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.zigbee_information"
)}
</mwc-button>
<mwc-button class="warning" @click=${this._removeDevice}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.remove"
)}
</mwc-button>
`
: ""}
`;
}
private async _onReconfigureNodeClick(): Promise<void> {
if (!this.hass) {
return;
}
reconfigureNode(this.hass, this._zhaDevice!.ieee);
}
private _onAddDevicesClick() {
navigate(this, "/config/zha/add/" + this._zhaDevice!.ieee);
}
private async _handleZigbeeInfoClicked() {
showZHADeviceZigbeeInfoDialog(this, { device: this._zhaDevice! });
}
private async _removeDevice() {
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
),
});
if (!confirmed) {
return;
}
this.hass.callService("zha", "remove", {
ieee_address: this._zhaDevice!.ieee,
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host {
display: flex;
flex-direction: column;
align-items: flex-start;
}
`,
];
}
}

View File

@ -0,0 +1,93 @@
import {
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
css,
PropertyValues,
} from "lit-element";
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { ZHADevice, fetchZHADevice } from "../../../../../data/zha";
import { formatAsPaddedHex } from "../../../integrations/integration-panels/zha/functions";
@customElement("ha-device-info-zha")
export class HaDeviceActionsZha extends LitElement {
@property() public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@property() private _zhaDevice?: ZHADevice;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const zigbeeConnection = this.device.connections.find(
(conn) => conn[0] === "zigbee"
);
if (!zigbeeConnection) {
return;
}
fetchZHADevice(this.hass, zigbeeConnection[1]).then((device) => {
this._zhaDevice = device;
});
}
}
protected render(): TemplateResult {
if (!this._zhaDevice) {
return html``;
}
return html`
<h4>Zigbee info</h4>
<div>IEEE: ${this._zhaDevice.ieee}</div>
<div>Nwk: ${formatAsPaddedHex(this._zhaDevice.nwk)}</div>
<div>Device Type: ${this._zhaDevice.device_type}</div>
<div>
LQI:
${this._zhaDevice.lqi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
RSSI:
${this._zhaDevice.rssi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.last_seen")}:
${this._zhaDevice.last_seen ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.power_source")}:
${this._zhaDevice.power_source ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
${this._zhaDevice.quirk_applied
? html`
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.quirk")}:
${this._zhaDevice.quirk_class}
</div>
`
: ""}
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
h4 {
margin-bottom: 4px;
}
div {
word-break: break-all;
margin-top: 2px;
}
`,
];
}
}

View File

@ -6,6 +6,7 @@ import {
html, html,
LitElement, LitElement,
property, property,
TemplateResult,
} from "lit-element"; } from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -38,13 +39,12 @@ import "../../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "./device-detail/ha-device-card-mqtt";
import "./device-detail/ha-device-entities-card"; import "./device-detail/ha-device-entities-card";
import "./device-detail/ha-device-info-card"; import "./device-detail/ha-device-info-card";
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation"; import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
export interface EntityRegistryStateEntry extends EntityRegistryEntry { export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string; stateName?: string | null;
} }
@customElement("ha-config-device-page") @customElement("ha-config-device-page")
@ -226,16 +226,7 @@ export class HaConfigDevicePage extends LitElement {
.devices=${this.devices} .devices=${this.devices}
.device=${device} .device=${device}
> >
${ ${this._renderIntegrationInfo(device, integrations)}
integrations.includes("mqtt")
? html`
<ha-device-card-mqtt
.hass=${this.hass}
.device=${device}
></ha-device-card-mqtt>
`
: html``
}
</ha-device-info-card> </ha-device-info-card>
${ ${
@ -439,7 +430,7 @@ export class HaConfigDevicePage extends LitElement {
</hass-tabs-subpage> `; </hass-tabs-subpage> `;
} }
private _computeEntityName(entity) { private _computeEntityName(entity: EntityRegistryEntry) {
if (entity.name) { if (entity.name) {
return entity.name; return entity.name;
} }
@ -480,6 +471,41 @@ export class HaConfigDevicePage extends LitElement {
}); });
} }
private _renderIntegrationInfo(
device,
integrations: string[]
): TemplateResult[] {
const templates: TemplateResult[] = [];
if (integrations.includes("mqtt")) {
import("./device-detail/integration-elements/ha-device-actions-mqtt");
templates.push(html`
<div class="card-actions" slot="actions">
<ha-device-actions-mqtt
.hass=${this.hass}
.device=${device}
></ha-device-actions-mqtt>
</div>
`);
}
if (integrations.includes("zha")) {
import("./device-detail/integration-elements/ha-device-actions-zha");
import("./device-detail/integration-elements/ha-device-info-zha");
templates.push(html`
<ha-device-info-zha
.hass=${this.hass}
.device=${device}
></ha-device-info-zha>
<div class="card-actions" slot="actions">
<ha-device-actions-zha
.hass=${this.hass}
.device=${device}
></ha-device-actions-zha>
</div>
`);
}
return templates;
}
private async _showSettings() { private async _showSettings() {
const device = this._device(this.deviceId, this.devices)!; const device = this._device(this.deviceId, this.devices)!;
showDeviceRegistryDetailDialog(this, { showDeviceRegistryDetailDialog(this, {

View File

@ -88,31 +88,31 @@ export class HaEntityRegistryBasicEditor extends LitElement {
.checked=${!this._disabledBy} .checked=${!this._disabledBy}
@change=${this._disabledByChanged} @change=${this._disabledByChanged}
> >
<div>
<div>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_label"
)}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
)}
<br />${this.hass.localize(
"ui.dialogs.entity_registry.editor.note"
)}
</div>
</div>
</ha-switch> </ha-switch>
<div>
<div>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_label"
)}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
)}
<br />${this.hass.localize(
"ui.dialogs.entity_registry.editor.note"
)}
</div>
</div>
</div> </div>
`; `;
} }
@ -127,9 +127,14 @@ export class HaEntityRegistryBasicEditor extends LitElement {
static get styles() { static get styles() {
return css` return css`
ha-switch {
margin-right: 16px;
}
.row { .row {
margin-top: 8px; margin-top: 8px;
color: var(--primary-text-color); color: var(--primary-text-color);
display: flex;
align-items: center;
} }
.secondary { .secondary {
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -120,31 +120,31 @@ export class EntityRegistrySettings extends LitElement {
.checked=${!this._disabledBy} .checked=${!this._disabledBy}
@change=${this._disabledByChanged} @change=${this._disabledByChanged}
> >
<div>
<div>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_label"
)}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
)}
<br />${this.hass.localize(
"ui.dialogs.entity_registry.editor.note"
)}
</div>
</div>
</ha-switch> </ha-switch>
<div>
<div>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_label"
)}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
)}
<br />${this.hass.localize(
"ui.dialogs.entity_registry.editor.note"
)}
</div>
</div>
</div> </div>
</div> </div>
</paper-dialog-scrollable> </paper-dialog-scrollable>
@ -247,9 +247,14 @@ export class EntityRegistrySettings extends LitElement {
mwc-button.warning { mwc-button.warning {
margin-right: auto; margin-right: auto;
} }
ha-switch {
margin-right: 16px;
}
.row { .row {
margin-top: 8px; margin-top: 8px;
color: var(--primary-text-color); color: var(--primary-text-color);
display: flex;
align-items: center;
} }
`, `,
]; ];

View File

@ -1,3 +1,4 @@
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
@ -31,6 +32,7 @@ import type {
RowClickedEvent, RowClickedEvent,
SelectionChangedEvent, SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button-menu";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import { import {
@ -53,6 +55,7 @@ import {
loadEntityEditorDialog, loadEntityEditorDialog,
showEntityEditorDialog, showEntityEditorDialog,
} from "./show-dialog-entity-editor"; } from "./show-dialog-entity-editor";
import { mdiFilterVariant } from "@mdi/js";
export interface StateEntity extends EntityRegistryEntry { export interface StateEntity extends EntityRegistryEntry {
readonly?: boolean; readonly?: boolean;
@ -442,47 +445,55 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
> >
</div>` </div>`
: ""} : ""}
<paper-menu-button no-animations horizontal-align="right"> <ha-button-menu corner="BOTTOM_START">
<ha-icon-button <mwc-icon-button
aria-label=${this.hass!.localize( slot="trigger"
.label=${this.hass!.localize(
"ui.panel.config.entities.picker.filter.filter" "ui.panel.config.entities.picker.filter.filter"
)} )}
title="${this.hass!.localize( .title=${this.hass!.localize(
"ui.panel.config.entities.picker.filter.filter" "ui.panel.config.entities.picker.filter.filter"
)}" )}
icon="hass:filter-variant" >
slot="dropdown-trigger" <ha-svg-icon path=${mdiFilterVariant}></ha-svg-icon>
></ha-icon-button> </mwc-icon-button>
<paper-listbox slot="dropdown-content"> <mwc-list-item
<paper-icon-item @tap="${this._showDisabledChanged}"> @click="${this._showDisabledChanged}"
<paper-checkbox graphic="control"
.checked=${this._showDisabled} >
slot="item-icon" <ha-checkbox
></paper-checkbox> slot="graphic"
${this.hass!.localize( .checked=${this._showDisabled}
"ui.panel.config.entities.picker.filter.show_disabled" ></ha-checkbox>
)} ${this.hass!.localize(
</paper-icon-item> "ui.panel.config.entities.picker.filter.show_disabled"
<paper-icon-item @tap="${this._showRestoredChanged}"> )}
<paper-checkbox </mwc-list-item>
.checked=${this._showUnavailable} <mwc-list-item
slot="item-icon" @click="${this._showRestoredChanged}"
></paper-checkbox> graphic="control"
${this.hass!.localize( >
"ui.panel.config.entities.picker.filter.show_unavailable" <ha-checkbox
)} slot="graphic"
</paper-icon-item> .checked=${this._showUnavailable}
<paper-icon-item @tap="${this._showReadOnlyChanged}"> ></ha-checkbox>
<paper-checkbox ${this.hass!.localize(
.checked=${this._showReadOnly} "ui.panel.config.entities.picker.filter.show_unavailable"
slot="item-icon" )}
></paper-checkbox> </mwc-list-item>
${this.hass!.localize( <mwc-list-item
"ui.panel.config.entities.picker.filter.show_readonly" @click="${this._showReadOnlyChanged}"
)} graphic="control"
</paper-icon-item> >
</paper-listbox> <ha-checkbox
</paper-menu-button> slot="graphic"
.checked=${this._showReadOnly}
></ha-checkbox>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_readonly"
)}
</mwc-list-item>
</ha-button-menu>
`; `;
return html` return html`
@ -730,8 +741,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
height: calc(100vh - 65px); height: calc(100vh - 65px);
display: block; display: block;
} }
ha-switch { ha-button-menu {
margin-top: 16px; margin-right: 8px;
} }
.table-header { .table-header {
display: flex; display: flex;

View File

@ -9,6 +9,25 @@ import "../../layouts/hass-loading-screen";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../types"; import { HomeAssistant, Route } from "../../types";
import {
mdiPuzzle,
mdiDevices,
mdiShape,
mdiSofa,
mdiRobot,
mdiPalette,
mdiScriptText,
mdiTools,
mdiViewDashboard,
mdiAccount,
mdiMapMarkerRadius,
mdiAccountBadgeHorizontal,
mdiHomeAssistant,
mdiServer,
mdiInformation,
mdiMathLog,
mdiPencil,
} from "@mdi/js";
declare global { declare global {
// for fire event // for fire event
@ -23,28 +42,28 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "integrations", component: "integrations",
path: "/config/integrations", path: "/config/integrations",
translationKey: "ui.panel.config.integrations.caption", translationKey: "ui.panel.config.integrations.caption",
icon: "hass:puzzle", iconPath: mdiPuzzle,
core: true, core: true,
}, },
{ {
component: "devices", component: "devices",
path: "/config/devices", path: "/config/devices",
translationKey: "ui.panel.config.devices.caption", translationKey: "ui.panel.config.devices.caption",
icon: "hass:devices", iconPath: mdiDevices,
core: true, core: true,
}, },
{ {
component: "entities", component: "entities",
path: "/config/entities", path: "/config/entities",
translationKey: "ui.panel.config.entities.caption", translationKey: "ui.panel.config.entities.caption",
icon: "hass:shape", iconPath: mdiShape,
core: true, core: true,
}, },
{ {
component: "areas", component: "areas",
path: "/config/areas", path: "/config/areas",
translationKey: "ui.panel.config.areas.caption", translationKey: "ui.panel.config.areas.caption",
icon: "hass:sofa", iconPath: mdiSofa,
core: true, core: true,
}, },
], ],
@ -53,25 +72,25 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "automation", component: "automation",
path: "/config/automation", path: "/config/automation",
translationKey: "ui.panel.config.automation.caption", translationKey: "ui.panel.config.automation.caption",
icon: "hass:robot", iconPath: mdiRobot,
}, },
{ {
component: "scene", component: "scene",
path: "/config/scene", path: "/config/scene",
translationKey: "ui.panel.config.scene.caption", translationKey: "ui.panel.config.scene.caption",
icon: "hass:palette", iconPath: mdiPalette,
}, },
{ {
component: "script", component: "script",
path: "/config/script", path: "/config/script",
translationKey: "ui.panel.config.script.caption", translationKey: "ui.panel.config.script.caption",
icon: "hass:script-text", iconPath: mdiScriptText,
}, },
{ {
component: "helpers", component: "helpers",
path: "/config/helpers", path: "/config/helpers",
translationKey: "ui.panel.config.helpers.caption", translationKey: "ui.panel.config.helpers.caption",
icon: "hass:tools", iconPath: mdiTools,
core: true, core: true,
}, },
], ],
@ -80,7 +99,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "lovelace", component: "lovelace",
path: "/config/lovelace/dashboards", path: "/config/lovelace/dashboards",
translationKey: "ui.panel.config.lovelace.caption", translationKey: "ui.panel.config.lovelace.caption",
icon: "hass:view-dashboard", iconPath: mdiViewDashboard,
}, },
], ],
persons: [ persons: [
@ -88,19 +107,19 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "person", component: "person",
path: "/config/person", path: "/config/person",
translationKey: "ui.panel.config.person.caption", translationKey: "ui.panel.config.person.caption",
icon: "hass:account", iconPath: mdiAccount,
}, },
{ {
component: "zone", component: "zone",
path: "/config/zone", path: "/config/zone",
translationKey: "ui.panel.config.zone.caption", translationKey: "ui.panel.config.zone.caption",
icon: "hass:map-marker-radius", iconPath: mdiMapMarkerRadius,
}, },
{ {
component: "users", component: "users",
path: "/config/users", path: "/config/users",
translationKey: "ui.panel.config.users.caption", translationKey: "ui.panel.config.users.caption",
icon: "hass:account-badge-horizontal", iconPath: mdiAccountBadgeHorizontal,
core: true, core: true,
}, },
], ],
@ -109,39 +128,41 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "core", component: "core",
path: "/config/core", path: "/config/core",
translationKey: "ui.panel.config.core.caption", translationKey: "ui.panel.config.core.caption",
icon: "hass:home-assistant", iconPath: mdiHomeAssistant,
core: true, core: true,
}, },
{ {
component: "server_control", component: "server_control",
path: "/config/server_control", path: "/config/server_control",
translationKey: "ui.panel.config.server_control.caption", translationKey: "ui.panel.config.server_control.caption",
icon: "hass:server", iconPath: mdiServer,
core: true, core: true,
}, },
{
component: "logs",
path: "/config/logs",
translationKey: "ui.panel.config.logs.caption",
iconPath: mdiMathLog,
core: true,
},
{
component: "info",
path: "/config/info",
translationKey: "ui.panel.config.info.caption",
iconPath: mdiInformation,
core: true,
},
],
advanced: [
{ {
component: "customize", component: "customize",
path: "/config/customize", path: "/config/customize",
translationKey: "ui.panel.config.customize.caption", translationKey: "ui.panel.config.customize.caption",
icon: "hass:pencil", iconPath: mdiPencil,
core: true, core: true,
advancedOnly: true, advancedOnly: true,
}, },
], ],
other: [
{
component: "zha",
path: "/config/zha",
translationKey: "component.zha.title",
icon: "hass:zigbee",
},
{
component: "zwave",
path: "/config/zwave",
translationKey: "component.zwave.title",
icon: "hass:z-wave",
},
],
}; };
@customElement("ha-panel-config") @customElement("ha-panel-config")
@ -197,6 +218,20 @@ class HaPanelConfig extends HassRouterPage {
/* webpackChunkName: "panel-config-server-control" */ "./server_control/ha-config-server-control" /* webpackChunkName: "panel-config-server-control" */ "./server_control/ha-config-server-control"
), ),
}, },
logs: {
tag: "ha-config-logs",
load: () =>
import(
/* webpackChunkName: "panel-config-logs" */ "./logs/ha-config-logs"
),
},
info: {
tag: "ha-config-info",
load: () =>
import(
/* webpackChunkName: "panel-config-info" */ "./info/ha-config-info"
),
},
customize: { customize: {
tag: "ha-config-customize", tag: "ha-config-customize",
load: () => load: () =>
@ -278,14 +313,14 @@ class HaPanelConfig extends HassRouterPage {
tag: "zha-config-dashboard-router", tag: "zha-config-dashboard-router",
load: () => load: () =>
import( import(
/* webpackChunkName: "panel-config-zha" */ "./zha/zha-config-dashboard-router" /* webpackChunkName: "panel-config-zha" */ "./integrations/integration-panels/zha/zha-config-dashboard-router"
), ),
}, },
zwave: { zwave: {
tag: "ha-config-zwave", tag: "ha-config-zwave",
load: () => load: () =>
import( import(
/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave" /* webpackChunkName: "panel-config-zwave" */ "./integrations/integration-panels/zwave/ha-config-zwave"
), ),
}, },
}, },

View File

@ -10,7 +10,6 @@ import {
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-input"; import "../../../../components/ha-icon-input";
import "../../../../components/ha-switch";
import { InputBoolean } from "../../../../data/input_boolean"; import { InputBoolean } from "../../../../data/input_boolean";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";

View File

@ -12,7 +12,6 @@ import {
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-input"; import "../../../../components/ha-icon-input";
import "../../../../components/ha-switch";
import { InputDateTime } from "../../../../data/input_datetime"; import { InputDateTime } from "../../../../data/input_datetime";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";

View File

@ -12,7 +12,6 @@ import {
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-input"; import "../../../../components/ha-icon-input";
import "../../../../components/ha-switch";
import { InputNumber } from "../../../../data/input_number"; import { InputNumber } from "../../../../data/input_number";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";

View File

@ -16,7 +16,6 @@ import {
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-input"; import "../../../../components/ha-icon-input";
import "../../../../components/ha-switch";
import type { InputSelect } from "../../../../data/input_select"; import type { InputSelect } from "../../../../data/input_select";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";

View File

@ -12,7 +12,6 @@ import {
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-input"; import "../../../../components/ha-icon-input";
import "../../../../components/ha-switch";
import { InputText } from "../../../../data/input_text"; import { InputText } from "../../../../data/input_text";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";

View File

@ -0,0 +1,209 @@
import {
css,
CSSResult,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import "./integrations-card";
import "./system-health-card";
import { configSections } from "../ha-panel-config";
import "../../../layouts/hass-tabs-subpage";
const JS_TYPE = __BUILD__;
const JS_VERSION = __VERSION__;
class HaConfigInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public isWide!: boolean;
@property() public showAdvanced!: boolean;
@property() public route!: Route;
protected render(): TemplateResult {
const hass = this.hass;
const customUiList: Array<{ name: string; url: string; version: string }> =
(window as any).CUSTOM_UI_LIST || [];
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.general}
>
<div class="about">
<a
href="https://www.home-assistant.io"
target="_blank"
rel="noreferrer"
><img
src="/static/icons/favicon-192x192.png"
height="192"
alt="${this.hass.localize(
"ui.panel.config.info.home_assistant_logo"
)}"
/></a>
<br />
<h2>Home Assistant ${hass.connection.haVersion}</h2>
<p>
${this.hass.localize(
"ui.panel.config.info.path_configuration",
"path",
hass.config.config_dir
)}
</p>
<p class="develop">
<a
href="https://www.home-assistant.io/developers/credits/"
target="_blank"
rel="noreferrer"
>
${this.hass.localize("ui.panel.config.info.developed_by")}
</a>
</p>
<p>
${this.hass.localize("ui.panel.config.info.license")}<br />
${this.hass.localize("ui.panel.config.info.source")}
<a
href="https://github.com/home-assistant/core"
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.config.info.server")}</a
>
&mdash;
<a
href="https://github.com/home-assistant/frontend"
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.config.info.frontend")}</a
>
</p>
<p>
${this.hass.localize("ui.panel.config.info.built_using")}
<a href="https://www.python.org" target="_blank" rel="noreferrer"
>Python 3</a
>,
<a
href="https://www.polymer-project.org"
target="_blank"
rel="noreferrer"
>Polymer</a
>, ${this.hass.localize("ui.panel.config.info.icons_by")}
<a
href="https://www.google.com/design/icons/"
target="_blank"
rel="noreferrer"
>Google</a
>
${this.hass.localize("ui.common.and")}
<a
href="https://MaterialDesignIcons.com"
target="_blank"
rel="noreferrer"
>MaterialDesignIcons.com</a
>.
</p>
<p>
${this.hass.localize(
"ui.panel.config.info.frontend_version",
"version",
JS_VERSION,
"type",
JS_TYPE
)}
${customUiList.length > 0
? html`
<div>
${this.hass.localize("ui.panel.config.info.custom_uis")}
${customUiList.map(
(item) => html`
<div>
<a href="${item.url}" target="_blank"> ${item.name}</a
>: ${item.version}
</div>
`
)}
</div>
`
: ""}
</p>
</div>
<div class="content">
<system-health-card .hass=${this.hass}></system-health-card>
<integrations-card .hass=${this.hass}></integrations-card>
</div>
</hass-tabs-subpage>
`;
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
// Legacy custom UI can be slow to register, give them time.
const customUI = ((window as any).CUSTOM_UI_LIST || []).length;
setTimeout(() => {
if (((window as any).CUSTOM_UI_LIST || []).length !== customUI.length) {
this.requestUpdate();
}
}, 1000);
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
:host {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
.content {
direction: ltr;
}
.about {
text-align: center;
line-height: 2em;
}
.version {
@apply --paper-font-headline;
}
.develop {
@apply --paper-font-subhead;
}
.about a {
color: var(--primary-color);
}
system-health-card,
integrations-card {
display: block;
max-width: 600px;
margin: 0 auto;
padding-bottom: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-info": HaConfigInfo;
}
}
customElements.define("ha-config-info", HaConfigInfo);

View File

@ -35,9 +35,7 @@ class IntegrationsCard extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-card <ha-card
.header=${this.hass.localize( .header=${this.hass.localize("ui.panel.config.info.integrations")}
"ui.panel.developer-tools.tabs.info.integrations"
)}
> >
<table class="card-content"> <table class="card-content">
<tbody> <tbody>
@ -67,7 +65,7 @@ class IntegrationsCard extends LitElement {
rel="noreferrer" rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.developer-tools.tabs.info.documentation" "ui.panel.config.info.documentation"
)} )}
</a> </a>
</td> </td>
@ -83,7 +81,7 @@ class IntegrationsCard extends LitElement {
rel="noreferrer" rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.developer-tools.tabs.info.issues" "ui.panel.config.info.issues"
)} )}
</a> </a>
</td> </td>

View File

@ -97,9 +97,7 @@ class SystemHealthCard extends LitElement {
} catch (err) { } catch (err) {
this._info = { this._info = {
system_health: { system_health: {
error: this.hass.localize( error: this.hass.localize("ui.panel.config.info.system_health_error"),
"ui.panel.developer-tools.tabs.info.system_health_error"
),
}, },
}; };
} }

View File

@ -1,4 +1,9 @@
import "@material/mwc-fab";
import "@material/mwc-icon-button";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiPlus } from "@mdi/js";
import "@polymer/app-route/app-route"; import "@polymer/app-route/app-route";
import Fuse from "fuse.js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
@ -11,13 +16,16 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import Fuse from "fuse.js"; import { HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../common/search/search-input";
import { caseInsensitiveCompare } from "../../../common/string/compare"; import { caseInsensitiveCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl"; import { computeRTL } from "../../../common/util/compute_rtl";
import { nextRender } from "../../../common/util/render-status"; import { nextRender } from "../../../common/util/render-status";
import "../../../components/entity/ha-state-icon"; import "../../../components/entity/ha-state-icon";
import "../../../components/ha-button-menu";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "@material/mwc-fab"; import "../../../components/ha-svg-icon";
import { import {
ConfigEntry, ConfigEntry,
deleteConfigEntry, deleteConfigEntry,
@ -42,23 +50,18 @@ import {
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage";
import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "../../../common/search/search-input";
import "./ha-integration-card"; import "./ha-integration-card";
import type { import type {
ConfigEntryRemovedEvent, ConfigEntryRemovedEvent,
ConfigEntryUpdatedEvent, ConfigEntryUpdatedEvent,
HaIntegrationCard, HaIntegrationCard,
} from "./ha-integration-card"; } from "./ha-integration-card";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-svg-icon";
import { mdiPlus } from "@mdi/js";
import { LocalizeFunc } from "../../../common/translations/localize";
interface DataEntryFlowProgressExtended extends DataEntryFlowProgress { interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
localized_title?: string; localized_title?: string;
@ -258,28 +261,22 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
</div> </div>
` `
: ""} : ""}
<paper-menu-button <ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
close-on-activate <mwc-icon-button
no-animations .title=${this.hass.localize("ui.common.menu")}
horizontal-align="right" .label=${this.hass.localize("ui.common.overflow_menu")}
horizontal-offset="-5" slot="trigger"
slot="toolbar-icon" >
> <ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
<ha-icon-button </mwc-icon-button>
icon="hass:dots-vertical" <mwc-list-item @click=${this._toggleShowIgnored}>
slot="dropdown-trigger" ${this.hass.localize(
alt="menu" this._showIgnored
></ha-icon-button> ? "ui.panel.config.integrations.ignore.hide_ignored"
<paper-listbox slot="dropdown-content" role="listbox"> : "ui.panel.config.integrations.ignore.show_ignored"
<paper-item @tap=${this._toggleShowIgnored}> )}
${this.hass.localize( </mwc-list-item>
this._showIgnored </ha-button-menu>
? "ui.panel.config.integrations.ignore.hide_ignored"
: "ui.panel.config.integrations.ignore.show_ignored"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
${!this.narrow ${!this.narrow
? html` ? html`
@ -302,7 +299,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
${this._showIgnored ${this._showIgnored
? ignoredConfigEntries.map( ? ignoredConfigEntries.map(
(item: ConfigEntryExtended) => html` (item: ConfigEntryExtended) => html`
<ha-card class="ignored"> <ha-card outlined class="ignored">
<div class="header"> <div class="header">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.ignore.ignored" "ui.panel.config.integrations.ignore.ignored"
@ -338,7 +335,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
${configEntriesInProgress.length ${configEntriesInProgress.length
? configEntriesInProgress.map( ? configEntriesInProgress.map(
(flow: DataEntryFlowProgressExtended) => html` (flow: DataEntryFlowProgressExtended) => html`
<ha-card class="discovered"> <ha-card outlined class="discovered">
<div class="header"> <div class="header">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.discovered" "ui.panel.config.integrations.discovered"
@ -399,7 +396,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
) )
: !this._configEntries.length : !this._configEntries.length
? html` ? html`
<ha-card> <ha-card outlined>
<div class="card-content"> <div class="card-content">
<h1> <h1>
${this.hass.localize("ui.panel.config.integrations.none")} ${this.hass.localize("ui.panel.config.integrations.none")}
@ -616,7 +613,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
justify-content: space-between; justify-content: space-between;
} }
.discovered { .discovered {
border: 1px solid var(--primary-color); --ha-card-border-color: var(--primary-color);
} }
.discovered .header { .discovered .header {
background: var(--primary-color); background: var(--primary-color);
@ -625,7 +622,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
text-align: center; text-align: center;
} }
.ignored { .ignored {
border: 1px solid var(--light-theme-disabled-color); --ha-card-border-color: var(--light-theme-disabled-color);
} }
.ignored img { .ignored img {
filter: grayscale(1); filter: grayscale(1);

View File

@ -27,6 +27,7 @@ import {
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { mdiDotsVertical } from "@mdi/js";
export interface ConfigEntryUpdatedEvent { export interface ConfigEntryUpdatedEvent {
entry: ConfigEntry; entry: ConfigEntry;
@ -44,6 +45,17 @@ declare global {
} }
} }
const integrationsWithPanel = {
zha: {
buttonLocalizeKey: "ui.panel.config.zha.button",
path: "/config/zha/dashboard",
},
zwave: {
buttonLocalizeKey: "ui.panel.config.zwave.button",
path: "/config/zwave",
},
};
@customElement("ha-integration-card") @customElement("ha-integration-card")
export class HaIntegrationCard extends LitElement { export class HaIntegrationCard extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@ -75,7 +87,7 @@ export class HaIntegrationCard extends LitElement {
private _renderGroupedIntegration(): TemplateResult { private _renderGroupedIntegration(): TemplateResult {
return html` return html`
<ha-card class="group"> <ha-card outlined class="group">
<div class="group-header"> <div class="group-header">
<img <img
src="https://brands.home-assistant.io/${this.domain}/icon.png" src="https://brands.home-assistant.io/${this.domain}/icon.png"
@ -111,6 +123,7 @@ export class HaIntegrationCard extends LitElement {
const entities = this._getEntities(item); const entities = this._getEntities(item);
return html` return html`
<ha-card <ha-card
outlined
class="single integration" class="single integration"
.configEntry=${item} .configEntry=${item}
.id=${item.entry_id} .id=${item.entry_id}
@ -178,42 +191,46 @@ export class HaIntegrationCard extends LitElement {
"ui.panel.config.integrations.config_entry.rename" "ui.panel.config.integrations.config_entry.rename"
)}</mwc-button )}</mwc-button
> >
${item.supports_options ${item.domain in integrationsWithPanel
? html`<a
href=${`${
integrationsWithPanel[item.domain].path
}?config_entry=${item.entry_id}`}
><mwc-button>
${this.hass.localize(
integrationsWithPanel[item.domain].buttonLocalizeKey
)}
</mwc-button></a
>`
: item.supports_options
? html` ? html`
<mwc-button @click=${this._showOptions} <mwc-button @click=${this._showOptions}>
>${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.config_entry.options" "ui.panel.config.integrations.config_entry.options"
)}</mwc-button )}
> </mwc-button>
` `
: ""} : ""}
</div> </div>
<paper-menu-button <ha-button-menu corner="BOTTOM_START">
horizontal-align="right" <mwc-icon-button
vertical-align="top" .title=${this.hass.localize("ui.common.menu")}
vertical-offset="40" .label=${this.hass.localize("ui.common.overflow_menu")}
close-on-activate slot="trigger"
> >
<ha-icon-button <ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
icon="hass:dots-vertical" </mwc-icon-button>
slot="dropdown-trigger" <mwc-list-item @click=${this._showSystemOptions}>
aria-label=${this.hass!.localize( ${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.options" "ui.panel.config.integrations.config_entry.system_options"
)} )}
></ha-icon-button> </mwc-list-item>
<paper-listbox slot="dropdown-content"> <mwc-list-item class="warning" @click=${this._removeIntegration}>
<paper-item @tap=${this._showSystemOptions}> ${this.hass.localize(
${this.hass.localize( "ui.panel.config.integrations.config_entry.delete"
"ui.panel.config.integrations.config_entry.system_options" )}
)}</paper-item </mwc-list-item>
> </ha-button-menu>
<paper-item class="warning" @tap=${this._removeIntegration}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}</paper-item
>
</paper-listbox>
</paper-menu-button>
</div> </div>
</ha-card> </ha-card>
`; `;
@ -379,9 +396,8 @@ export class HaIntegrationCard extends LitElement {
margin-top: 0; margin-top: 0;
min-height: 24px; min-height: 24px;
} }
paper-menu-button { ha-button-menu {
color: var(--secondary-text-color); color: var(--secondary-text-color);
padding: 0;
} }
@media (min-width: 563px) { @media (min-width: 563px) {
paper-listbox { paper-listbox {

View File

@ -1,4 +1,4 @@
import { Cluster, ZHADevice, ZHAGroup } from "../../../data/zha"; import { Cluster, ZHADevice, ZHAGroup } from "../../../../../data/zha";
export const formatAsPaddedHex = (value: string | number): string => { export const formatAsPaddedHex = (value: string | number): string => {
let hex = value; let hex = value;
@ -8,6 +8,9 @@ export const formatAsPaddedHex = (value: string | number): string => {
return "0x" + hex.toString(16).padStart(4, "0"); return "0x" + hex.toString(16).padStart(4, "0");
}; };
export const getIeeeTail = (ieee: string) =>
ieee.split(":").slice(-4).reverse().join("");
export const sortZHADevices = (a: ZHADevice, b: ZHADevice): number => { export const sortZHADevices = (a: ZHADevice, b: ZHADevice): number => {
const nameA = a.user_given_name ? a.user_given_name : a.name; const nameA = a.user_given_name ? a.user_given_name : a.name;
const nameb = b.user_given_name ? b.user_given_name : b.name; const nameb = b.user_given_name ? b.user_given_name : b.name;

View File

@ -1,4 +1,4 @@
import { Cluster, ZHADevice } from "../../../data/zha"; import { Cluster, ZHADevice } from "../../../../../data/zha";
export interface PickerTarget extends EventTarget { export interface PickerTarget extends EventTarget {
selected: number; selected: number;

View File

@ -1,5 +1,5 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "../../../components/ha-icon-button"; import "../../../../../components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import { import {
css, css,
@ -9,19 +9,24 @@ import {
LitElement, LitElement,
property, property,
TemplateResult, TemplateResult,
PropertyValues,
} from "lit-element"; } from "lit-element";
import "../../../components/ha-service-description"; import "../../../../../components/ha-service-description";
import "../../../components/ha-textarea"; import "@polymer/paper-input/paper-textarea";
import { ZHADevice } from "../../../data/zha"; import { ZHADevice } from "../../../../../data/zha";
import "../../../layouts/hass-subpage"; import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../../../types";
import "./zha-device-card"; import "./zha-device-card";
import { zhaTabs } from "./zha-config-dashboard";
import { IronAutogrowTextareaElement } from "@polymer/iron-autogrow-textarea";
@customElement("zha-add-devices-page") @customElement("zha-add-devices-page")
class ZHAAddDevicesPage extends LitElement { class ZHAAddDevicesPage extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public narrow?: boolean;
@property() public isWide?: boolean; @property() public isWide?: boolean;
@property() public route?: Route; @property() public route?: Route;
@ -36,6 +41,8 @@ class ZHAAddDevicesPage extends LitElement {
@property() private _showHelp = false; @property() private _showHelp = false;
@property() private _showLogs = false;
private _ieeeAddress?: string; private _ieeeAddress?: string;
private _addDevicesTimeoutHandle: any = undefined; private _addDevicesTimeoutHandle: any = undefined;
@ -60,58 +67,63 @@ class ZHAAddDevicesPage extends LitElement {
this._formattedEvents = ""; this._formattedEvents = "";
} }
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (
changedProps.has("hass") &&
!this._active &&
!changedProps.get("hass")
) {
this._subscribe();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<hass-subpage <hass-tabs-subpage
header="${this.hass!.localize( .hass=${this.hass}
"ui.panel.config.zha.add_device_page.header" .narrow=${this.narrow}
)}" .route=${this.route}
.tabs=${zhaTabs}
> >
${this._active <mwc-button slot="toolbar-icon" @click=${this._toggleLogs}
? html` >${this._showLogs ? "Hide logs" : "Show logs"}</mwc-button
<h2> >
<paper-spinner <div class="searching">
?active="${this._active}" ${this._active
alt="Searching" ? html`
></paper-spinner> <h1>
${this.hass!.localize(
"ui.panel.config.zha.add_device_page.spinner"
)}
</h2>
`
: html`
<div class="card-actions">
<mwc-button @click=${this._subscribe} class="search-button">
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.zha.add_device_page.search_again" "ui.panel.config.zha.add_device_page.spinner"
)} )}
</mwc-button> </h1>
<ha-icon-button <paper-spinner active alt="Searching"></paper-spinner>
class="toggle-help-icon" `
@click="${this._onHelpTap}" : html`
icon="hass:help-circle" <div>
></ha-icon-button> <mwc-button @click=${this._subscribe} class="search-button">
${this._showHelp ${this.hass!.localize(
? html` "ui.panel.config.zha.add_device_page.search_again"
<ha-service-description )}
.hass=${this.hass} </mwc-button>
domain="zha" </div>
service="permit" `}
class="help-text" </div>
></ha-service-description>
`
: ""}
</div>
`}
${this._error ? html` <div class="error">${this._error}</div> ` : ""} ${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="content-header"></div>
<div class="content"> <div class="content">
${this._discoveredDevices.length < 1 ${this._discoveredDevices.length < 1
? html` ? html`
<div class="discovery-text"> <div class="discovery-text">
<h4> <h4>
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.zha.add_device_page.discovery_text" "ui.panel.config.zha.add_device_page.pairing_mode"
)}
</h4>
<h4>
${this.hass!.localize(
this._active
? "ui.panel.config.zha.add_device_page.discovered_text"
: "ui.panel.config.zha.add_device_page.no_devices_found"
)} )}
</h4> </h4>
</div> </div>
@ -123,27 +135,38 @@ class ZHAAddDevicesPage extends LitElement {
class="card" class="card"
.hass=${this.hass} .hass=${this.hass}
.device=${device} .device=${device}
.narrow=${!this.isWide} .narrow=${this.narrow}
.showHelp=${this._showHelp} .showHelp=${this._showHelp}
.showActions=${!this._active}
.showEntityDetail=${false}
></zha-device-card> ></zha-device-card>
` `
)} )}
`} `}
</div> </div>
<ha-textarea class="events" value="${this._formattedEvents}"> ${this._showLogs
</ha-textarea> ? html`<paper-textarea
</hass-subpage> readonly
max-rows="10"
class="log"
value="${this._formattedEvents}"
>
</paper-textarea>`
: ""}
</hass-tabs-subpage>
`; `;
} }
private _toggleLogs() {
this._showLogs = !this._showLogs;
}
private _handleMessage(message: any): void { private _handleMessage(message: any): void {
if (message.type === "log_output") { if (message.type === "log_output") {
this._formattedEvents += message.log_entry.message + "\n"; this._formattedEvents += message.log_entry.message + "\n";
if (this.shadowRoot) { if (this.shadowRoot) {
const textArea = this.shadowRoot.querySelector("ha-textarea"); const paperTextArea = this.shadowRoot.querySelector("paper-textarea");
if (textArea) { if (paperTextArea) {
const textArea = (paperTextArea.inputElement as IronAutogrowTextareaElement)
.textarea;
textArea.scrollTop = textArea.scrollHeight; textArea.scrollTop = textArea.scrollHeight;
} }
} }
@ -165,69 +188,58 @@ class ZHAAddDevicesPage extends LitElement {
} }
private _subscribe(): void { private _subscribe(): void {
if (!this.hass) {
return;
}
this._active = true;
const data: any = { type: "zha/devices/permit" }; const data: any = { type: "zha/devices/permit" };
if (this._ieeeAddress) { if (this._ieeeAddress) {
data.ieee = this._ieeeAddress; data.ieee = this._ieeeAddress;
} }
this._subscribed = this.hass!.connection.subscribeMessage( this._subscribed = this.hass.connection.subscribeMessage(
(message) => this._handleMessage(message), (message) => this._handleMessage(message),
data data
); );
this._active = true;
this._addDevicesTimeoutHandle = setTimeout( this._addDevicesTimeoutHandle = setTimeout(
() => this._unsubscribe(), () => this._unsubscribe(),
120000 120000
); );
} }
private _onHelpTap(): void {
this._showHelp = !this._showHelp;
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,
css` css`
.discovery-text, .discovery-text {
.content-header { width: 100%;
margin: 16px; padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
} }
.content { .content {
border-top: 1px solid var(--light-primary-color);
min-height: 500px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding: 4px; padding: 4px;
justify-content: left; justify-content: center;
overflow: scroll;
} }
.error { .error {
color: var(--google-red-500); color: var(--google-red-500);
} }
paper-spinner { paper-spinner {
display: none; padding: 20px;
margin-right: 20px;
margin-left: 16px;
} }
paper-spinner[active] { .searching {
display: block; margin-top: 20px;
float: left; display: flex;
margin-right: 20px; flex-direction: column;
margin-left: 16px; align-items: center;
} }
.card { .card {
margin-left: 16px; margin: 8px;
margin-right: 16px;
margin-bottom: 0px;
margin-top: 10px;
} }
.events { .log {
margin: 16px; padding: 16px;
border-top: 1px solid var(--light-primary-color);
padding-top: 16px;
min-height: 200px;
max-height: 200px;
overflow: scroll;
} }
.toggle-help-icon { .toggle-help-icon {
position: absolute; position: absolute;

View File

@ -12,20 +12,20 @@ import {
PropertyValues, PropertyValues,
query, query,
} from "lit-element"; } from "lit-element";
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../../../common/navigate";
import type { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; import type { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
import { import {
addGroup, addGroup,
fetchGroupableDevices, fetchGroupableDevices,
ZHAGroup, ZHAGroup,
ZHADeviceEndpoint, ZHADeviceEndpoint,
} from "../../../data/zha"; } from "../../../../../data/zha";
import "../../../layouts/hass-error-screen"; import "../../../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage"; import "../../../../../layouts/hass-subpage";
import type { PolymerChangedEvent } from "../../../polymer-types"; import type { PolymerChangedEvent } from "../../../../../polymer-types";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-config-section"; import "../../../ha-config-section";
import "./zha-device-endpoint-data-table"; import "./zha-device-endpoint-data-table";
import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table"; import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table";

View File

@ -1,6 +1,6 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../components/ha-icon-button"; import "../../../../../components/ha-icon-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
@ -13,9 +13,9 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../components/buttons/ha-call-service-button"; import "../../../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../components/ha-service-description"; import "../../../../../components/ha-service-description";
import { import {
Attribute, Attribute,
Cluster, Cluster,
@ -23,10 +23,10 @@ import {
ReadAttributeServiceData, ReadAttributeServiceData,
readAttributeValue, readAttributeValue,
ZHADevice, ZHADevice,
} from "../../../data/zha"; } from "../../../../../data/zha";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../../../types";
import "../ha-config-section"; import "../../../ha-config-section";
import { formatAsPaddedHex } from "./functions"; import { formatAsPaddedHex } from "./functions";
import { import {
ChangeEvent, ChangeEvent,

View File

@ -1,5 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../components/ha-icon-button"; import "../../../../../components/ha-icon-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
@ -12,18 +12,18 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../components/buttons/ha-call-service-button"; import "../../../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../components/ha-service-description"; import "../../../../../components/ha-service-description";
import { import {
Cluster, Cluster,
Command, Command,
fetchCommandsForCluster, fetchCommandsForCluster,
ZHADevice, ZHADevice,
} from "../../../data/zha"; } from "../../../../../data/zha";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../../../types";
import "../ha-config-section"; import "../../../ha-config-section";
import { formatAsPaddedHex } from "./functions"; import { formatAsPaddedHex } from "./functions";
import { import {
ChangeEvent, ChangeEvent,

View File

@ -7,14 +7,14 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import "../../../components/data-table/ha-data-table"; import "../../../../../components/data-table/ha-data-table";
import type { import type {
DataTableColumnContainer, DataTableColumnContainer,
HaDataTable, HaDataTable,
} from "../../../components/data-table/ha-data-table"; } from "../../../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-state-icon"; import "../../../../../components/entity/ha-state-icon";
import type { Cluster } from "../../../data/zha"; import type { Cluster } from "../../../../../data/zha";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../../../types";
import { formatAsPaddedHex } from "./functions"; import { formatAsPaddedHex } from "./functions";
export interface ClusterRowData extends Cluster { export interface ClusterRowData extends Cluster {

View File

@ -1,5 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../components/ha-icon-button"; import "../../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { import {
@ -11,14 +11,18 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../components/buttons/ha-call-service-button"; import "../../../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../components/ha-service-description"; import "../../../../../components/ha-service-description";
import { Cluster, fetchClustersForZhaNode, ZHADevice } from "../../../data/zha"; import {
import { haStyle } from "../../../resources/styles"; Cluster,
import { HomeAssistant } from "../../../types"; fetchClustersForZhaNode,
import "../ha-config-section"; ZHADevice,
} from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import { computeClusterKey } from "./functions"; import { computeClusterKey } from "./functions";
import { ItemSelectedEvent } from "./types"; import { ItemSelectedEvent } from "./types";

View File

@ -2,8 +2,9 @@ import { customElement, property } from "lit-element";
import { import {
HassRouterPage, HassRouterPage,
RouterOptions, RouterOptions,
} from "../../../layouts/hass-router-page"; } from "../../../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../../../types";
import { navigate } from "../../../../../common/navigate";
@customElement("zha-config-dashboard-router") @customElement("zha-config-dashboard-router")
class ZHAConfigDashboardRouter extends HassRouterPage { class ZHAConfigDashboardRouter extends HassRouterPage {
@ -13,6 +14,10 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
@property() public narrow!: boolean; @property() public narrow!: boolean;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = { protected routerOptions: RouterOptions = {
defaultPage: "dashboard", defaultPage: "dashboard",
showLoading: true, showLoading: true,
@ -24,13 +29,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
/* webpackChunkName: "zha-config-dashboard" */ "./zha-config-dashboard" /* webpackChunkName: "zha-config-dashboard" */ "./zha-config-dashboard"
), ),
}, },
device: {
tag: "zha-device-page",
load: () =>
import(
/* webpackChunkName: "zha-devices-page" */ "./zha-device-page"
),
},
add: { add: {
tag: "zha-add-devices-page", tag: "zha-add-devices-page",
load: () => load: () =>
@ -65,11 +63,24 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
el.hass = this.hass; el.hass = this.hass;
el.isWide = this.isWide; el.isWide = this.isWide;
el.narrow = this.narrow; el.narrow = this.narrow;
el.configEntryId = this._configEntry;
if (this._currentPage === "group") { if (this._currentPage === "group") {
el.groupId = this.routeTail.path.substr(1); el.groupId = this.routeTail.path.substr(1);
} else if (this._currentPage === "device") { } else if (this._currentPage === "device") {
el.ieee = this.routeTail.path.substr(1); el.ieee = this.routeTail.path.substr(1);
} }
const searchParams = new URLSearchParams(window.location.search);
if (this._configEntry && !searchParams.has("config_entry")) {
searchParams.append("config_entry", this._configEntry);
navigate(
this,
`${this.routeTail.prefix}${
this.routeTail.path
}?${searchParams.toString()}`,
true
);
}
} }
} }

View File

@ -0,0 +1,132 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@material/mwc-fab";
import {
css,
CSSResultArray,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { mdiNetwork, mdiFolderMultipleOutline, mdiPlus } from "@mdi/js";
import "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { computeRTL } from "../../../../../common/util/compute_rtl";
export const zhaTabs: PageNavigation[] = [
{
translationKey: "ui.panel.config.zha.network.caption",
path: `/config/zha/dashboard`,
iconPath: mdiNetwork,
},
{
translationKey: "ui.panel.config.zha.groups.caption",
path: `/config/zha/groups`,
iconPath: mdiFolderMultipleOutline,
},
];
@customElement("zha-config-dashboard")
class ZHAConfigDashboard extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${zhaTabs}
back-path="/config/integrations"
>
<ha-card header="Zigbee Network">
<div class="card-content">
Network info/settings for specific config entry
</div>
${this.configEntryId
? html`<div class="card-actions">
<a
href="${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}"
>
<mwc-button>Devices</mwc-button>
</a>
<a
href="${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}"
>
<mwc-button>Entities</mwc-button>
</a>
</div>`
: ""}
</ha-card>
<a href="/config/zha/add">
<mwc-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
title=${this.hass.localize("ui.panel.config.zha.add_device")}
?rtl=${computeRTL(this.hass)}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</a>
</hass-tabs-subpage>
`;
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
ha-card {
margin: auto;
margin-top: 16px;
max-width: 500px;
}
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-config-dashboard": ZHAConfigDashboard;
}
}

View File

@ -1,6 +1,6 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../components/ha-icon-button"; import "../../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { import {
@ -13,13 +13,13 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../components/buttons/ha-call-service-button"; import "../../../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../components/ha-service-description"; import "../../../../../components/ha-service-description";
import { bindDevices, unbindDevices, ZHADevice } from "../../../data/zha"; import { bindDevices, unbindDevices, ZHADevice } from "../../../../../data/zha";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../../../types";
import "../ha-config-section"; import "../../../ha-config-section";
import { ItemSelectedEvent } from "./types"; import { ItemSelectedEvent } from "./types";
@customElement("zha-device-binding-control") @customElement("zha-device-binding-control")

View File

@ -0,0 +1,243 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-listbox/paper-listbox";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/entity/state-badge";
import "../../../../../components/ha-card";
import "../../../../../components/ha-service-description";
import { updateDeviceRegistryEntry } from "../../../../../data/device_registry";
import { ZHADevice } from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-area-picker";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import {
subscribeEntityRegistry,
EntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../../../data/entity_registry";
import { createValidEntityId } from "../../../../../common/entity/valid_entity_id";
import memoizeOne from "memoize-one";
import { EntityRegistryStateEntry } from "../../../devices/ha-config-device-page";
import { compare } from "../../../../../common/string/compare";
import { getIeeeTail } from "./functions";
@customElement("zha-device-card")
class ZHADeviceCard extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public device?: ZHADevice;
@property({ type: Boolean }) public narrow?: boolean;
@property() private _entities: EntityRegistryEntry[] = [];
private _deviceEntities = memoizeOne(
(
deviceId: string,
entities: EntityRegistryEntry[]
): EntityRegistryStateEntry[] =>
entities
.filter((entity) => entity.device_id === deviceId)
.map((entity) => {
return { ...entity, stateName: this._computeEntityName(entity) };
})
.sort((ent1, ent2) =>
compare(
ent1.stateName || `zzz${ent1.entity_id}`,
ent2.stateName || `zzz${ent2.entity_id}`
)
)
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection, (entities) => {
this._entities = entities;
}),
];
}
protected render(): TemplateResult {
if (!this.hass || !this.device) {
return html``;
}
const entities = this._deviceEntities(
this.device.device_reg_id,
this._entities
);
return html`
<ha-card .header=${this.device.user_given_name || this.device.name}>
<div class="card-content">
<div class="info">
<div class="model">${this.device.model}</div>
<div class="manuf">
${this.hass.localize(
"ui.dialogs.zha_device_info.manuf",
"manufacturer",
this.device.manufacturer
)}
</div>
</div>
<div class="device-entities">
${entities.map(
(entity) => html`
<state-badge
@click="${this._openMoreInfo}"
.title=${entity.stateName!}
.stateObj="${this.hass!.states[entity.entity_id]}"
slot="item-icon"
></state-badge>
`
)}
</div>
<paper-input
type="string"
@change=${this._rename}
.value=${this.device.user_given_name || this.device.name}
.label=${this.hass.localize(
"ui.dialogs.zha_device_info.zha_device_card.device_name_placeholder"
)}
></paper-input>
<ha-area-picker
.hass=${this.hass}
.device=${this.device.device_reg_id}
@value-changed=${this._areaPicked}
></ha-area-picker>
</div>
</ha-card>
`;
}
private async _rename(event): Promise<void> {
if (!this.hass || !this.device) {
return;
}
const device = this.device;
const oldDeviceName = device.user_given_name || device.name;
const newDeviceName = event.target.value;
this.device.user_given_name = newDeviceName;
await updateDeviceRegistryEntry(this.hass, device.device_reg_id, {
name_by_user: newDeviceName,
});
if (!oldDeviceName || !newDeviceName || oldDeviceName === newDeviceName) {
return;
}
const entities = this._deviceEntities(device.device_reg_id, this._entities);
const oldDeviceEntityId = createValidEntityId(oldDeviceName);
const newDeviceEntityId = createValidEntityId(newDeviceName);
const ieeeTail = getIeeeTail(device.ieee);
const updateProms = entities.map((entity) => {
const name = entity.name || entity.stateName;
let newEntityId: string | null = null;
let newName: string | null = null;
if (name && name.includes(oldDeviceName)) {
newName = name.replace(` ${ieeeTail}`, "");
newName = newName.replace(oldDeviceName, newDeviceName);
newEntityId = entity.entity_id.replace(`_${ieeeTail}`, "");
newEntityId = newEntityId.replace(oldDeviceEntityId, newDeviceEntityId);
}
if (!newName && !newEntityId) {
return new Promise((resolve) => resolve());
}
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
name: newName || name,
disabled_by: entity.disabled_by,
new_entity_id: newEntityId || entity.entity_id,
});
});
await Promise.all(updateProms);
}
private _openMoreInfo(ev: MouseEvent): void {
fireEvent(this, "hass-more-info", {
entityId: (ev.currentTarget as any).stateObj.entity_id,
});
}
private _computeEntityName(entity: EntityRegistryEntry): string {
if (this.hass.states[entity.entity_id]) {
return computeStateName(this.hass.states[entity.entity_id]);
}
return entity.name;
}
private async _areaPicked(ev: CustomEvent) {
const picker = ev.currentTarget as any;
const area = ev.detail.value;
try {
await updateDeviceRegistryEntry(this.hass, this.device!.device_reg_id, {
area_id: area,
});
this.device!.area_id = area;
} catch (err) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_area",
"error",
err.message
),
});
picker.value = null;
}
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.device-entities {
display: flex;
flex-wrap: wrap;
padding: 4px;
justify-content: left;
min-height: 48px;
}
.device {
width: 30%;
}
.device .name {
font-weight: bold;
}
.device .manuf {
color: var(--secondary-text-color);
margin-bottom: 20px;
}
.extra-info {
margin-top: 8px;
}
state-badge {
cursor: pointer;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-device-card": ZHADeviceCard;
}
}

View File

@ -9,16 +9,18 @@ import {
CSSResult, CSSResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import "../../../components/data-table/ha-data-table"; import "../../../../../components/data-table/ha-data-table";
import type { import type {
DataTableColumnContainer, DataTableColumnContainer,
HaDataTable, HaDataTable,
DataTableRowData, DataTableRowData,
} from "../../../components/data-table/ha-data-table"; } from "../../../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-state-icon"; import "../../../../../components/entity/ha-state-icon";
import type { ZHADeviceEndpoint, ZHAEntityReference } from "../../../data/zha"; import type {
import { showZHADeviceInfoDialog } from "../../../dialogs/zha-device-info-dialog/show-dialog-zha-device-info"; ZHADeviceEndpoint,
import type { HomeAssistant } from "../../../types"; ZHAEntityReference,
} from "../../../../../data/zha";
import type { HomeAssistant } from "../../../../../types";
export interface DeviceEndpointRowData extends DataTableRowData { export interface DeviceEndpointRowData extends DataTableRowData {
id: string; id: string;
@ -55,6 +57,7 @@ export class ZHADeviceEndpointDataTable extends LitElement {
ieee: deviceEndpoint.device.ieee, ieee: deviceEndpoint.device.ieee,
endpoint_id: deviceEndpoint.endpoint_id, endpoint_id: deviceEndpoint.endpoint_id,
entities: deviceEndpoint.entities, entities: deviceEndpoint.entities,
dev_id: deviceEndpoint.device.device_reg_id,
}); });
}); });
@ -72,14 +75,10 @@ export class ZHADeviceEndpointDataTable extends LitElement {
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (name) => html` template: (name, device: any) => html`
<div <a href="${`/config/devices/device/${device.dev_id}`}">
class="mdc-data-table__cell table-cell-text"
@click=${this._handleClicked}
style="cursor: pointer;"
>
${name} ${name}
</div> </a>
`, `,
}, },
endpoint_id: { endpoint_id: {
@ -95,14 +94,10 @@ export class ZHADeviceEndpointDataTable extends LitElement {
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (name) => html` template: (name, device: any) => html`
<div <a href="${`/config/devices/device/${device.dev_id}`}">
class="mdc-data-table__cell table-cell-text"
@click=${this._handleClicked}
style="cursor: pointer;"
>
${name} ${name}
</div> </a>
`, `,
}, },
endpoint_id: { endpoint_id: {
@ -156,14 +151,6 @@ export class ZHADeviceEndpointDataTable extends LitElement {
`; `;
} }
private async _handleClicked(ev: CustomEvent) {
const rowId = ((ev.target as HTMLElement).closest(
".mdc-data-table__row"
) as any).rowId;
const ieee = rowId.substring(0, rowId.indexOf("_"));
showZHADeviceInfoDialog(this, { ieee });
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
css` css`

View File

@ -1,6 +1,6 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "../../../components/ha-icon-button"; import "../../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { import {
@ -14,11 +14,11 @@ import {
query, query,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import "../../../components/buttons/ha-call-service-button"; import "../../../../../components/buttons/ha-call-service-button";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; import { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
import "../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../components/ha-service-description"; import "../../../../../components/ha-service-description";
import { import {
bindDeviceToGroup, bindDeviceToGroup,
Cluster, Cluster,
@ -26,10 +26,10 @@ import {
unbindDeviceFromGroup, unbindDeviceFromGroup,
ZHADevice, ZHADevice,
ZHAGroup, ZHAGroup,
} from "../../../data/zha"; } from "../../../../../data/zha";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-config-section"; import "../../../ha-config-section";
import { ItemSelectedEvent } from "./types"; import { ItemSelectedEvent } from "./types";
import "./zha-clusters-data-table"; import "./zha-clusters-data-table";
import type { ZHAClustersDataTable } from "./zha-clusters-data-table"; import type { ZHAClustersDataTable } from "./zha-clusters-data-table";

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