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

View File

@ -20,6 +20,7 @@ gulp.task(
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-cast",
"gen-index-cast-dev",
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 paths = require("../paths");
const zopfliOptions = { threshold: 150 };
gulp.task("compress-app", function compressApp() {
const jsLatest = gulp
.src(path.resolve(paths.app_output_latest, "**/*.js"))
.pipe(zopfli({ threshold: 150 }))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.app_output_latest));
const jsEs5 = gulp
.src(path.resolve(paths.app_output_es5, "**/*.js"))
.pipe(zopfli({ threshold: 150 }))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.app_output_es5));
const polyfills = gulp
.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")));
const translations = gulp
.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")));
const icons = gulp
.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")));
return merge(jsLatest, jsEs5, polyfills, translations, icons);
@ -38,6 +40,6 @@ gulp.task("compress-app", function compressApp() {
gulp.task("compress-hassio", function compressApp() {
return gulp
.src(path.resolve(paths.hassio_output_root, "**/*.js"))
.pipe(zopfli())
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.hassio_output_root));
});

View File

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

View File

@ -1,6 +1,9 @@
const gulp = require("gulp");
const fs = require("fs");
const path = require("path");
const env = require("../env");
const paths = require("../paths");
require("./clean.js");
require("./gen-icons-json.js");
@ -8,6 +11,24 @@ require("./webpack.js");
require("./compress.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(
"develop-hassio",
gulp.series(
@ -16,6 +37,7 @@ gulp.task(
},
"clean-hassio",
"gen-icons-json",
writeEntrypointJS,
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
)
);
@ -29,6 +51,7 @@ gulp.task(
"clean-hassio",
"gen-icons-json",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
writeEntrypointJS,
...// Don't compress running tests
(env.isTest() ? [] : ["compress-hassio"])
)

View File

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

View File

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

View File

@ -34,7 +34,7 @@ module.exports = {
hassio_dir: path.resolve(__dirname, "../hassio"),
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"),
};

View File

@ -70,7 +70,9 @@ const createWebpackConfig = ({
if (
!context.includes("/node_modules/") ||
// 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;
}
@ -80,7 +82,11 @@ const createWebpackConfig = ({
? path.resolve(context, resource)
: require.resolve(resource);
} catch (err) {
console.error("Error in ignore plugin", resource, context);
console.error(
"Error in Home Assistant ignore plugin",
resource,
context
);
throw err;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,8 @@
const { createHassioConfig } = require("../build-scripts/webpack.js");
const { isProdBuild } = require("../build-scripts/env.js");
// File just used for stats builds
const latestBuild = false;
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
module.exports = createHassioConfig({
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",
"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",
"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"
"test": "npm run lint && npm run mocha"
},
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
@ -89,7 +87,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^5.2.1",
"home-assistant-js-websocket": "^5.3.0",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9",
"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(
name="home-assistant-frontend",
version="20200603.3",
version="20200613.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@ -6,7 +6,7 @@ export const isValidEntityId = (entityId: string) =>
export const createValidEntityId = (input: string) =>
input
.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(/_{2,}/g, "_") // replace multiple underscores with 1
.replace(/_$/, ""); // remove underscores at the end

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,8 @@ import {
class HaCard extends LitElement {
@property() public header?: string;
@property({ type: Boolean, reflect: true }) public outlined = false;
static get styles(): CSSResult {
return css`
:host {
@ -19,12 +21,12 @@ class HaCard extends LitElement {
--ha-card-background,
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(
--ha-card-box-shadow,
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2)
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12)
);
color: var(--primary-text-color);
display: block;
@ -32,6 +34,16 @@ class HaCard extends LitElement {
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,
:host ::slotted(.card-header) {
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
aria-label=${hass.localize("ui.dialogs.generic.close")}
dialogAction="close"
class="close_button"
class="header_button"
>
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
@ -25,6 +25,9 @@ export class HaDialog extends MwcDialog {
return [
style,
css`
.mdc-dialog {
z-index: var(--dialog-z-index, 7);
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
}
@ -35,10 +38,15 @@ export class HaDialog extends MwcDialog {
display: block;
height: 20px;
}
.close_button {
.mdc-dialog__content {
padding: var(--dialog-content-padding, 20px 24px);
}
.header_button {
position: absolute;
right: 16px;
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() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this.icon =
this._icon =
window.getComputedStyle(this).direction === "ltr"
? "hass:arrow-right"
: "hass:arrow-left";
? mdiArrowRight
: mdiArrowLeft;
}, 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 {
@ -19,5 +39,3 @@ declare global {
"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 "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-arrow-prev")
export class HaIconButtonArrowPrev extends LitElement {
@property({ type: Boolean }) public disabled = false;
@ -32,5 +39,3 @@ declare global {
"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() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this.icon =
this._icon =
window.getComputedStyle(this).direction === "ltr"
? "hass:chevron-right"
: "hass:chevron-left";
? mdiChevronRight
: mdiChevronLeft;
}, 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 {
@ -19,5 +39,3 @@ declare global {
"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() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this.icon =
this._icon =
window.getComputedStyle(this).direction === "ltr"
? "hass:chevron-left"
: "hass:chevron-right";
? mdiChevronLeft
: mdiChevronRight;
}, 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 {
@ -19,5 +39,3 @@ declare global {
"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) {
super.update(changedProps);
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 {
css,
@ -12,8 +14,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { subscribeNotifications } from "../data/persistent_notification";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import { mdiMenu } from "@mdi/js";
import "./ha-svg-icon";
@customElement("ha-menu-button")
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 "./ha-icon-button";
import "@polymer/paper-item/paper-icon-item";
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
@ -29,9 +35,9 @@ import {
getExternalConfig,
} from "../external_app/external_config";
import type { HomeAssistant, PanelInfo } from "../types";
import "./ha-svg-icon";
import "./ha-icon";
import "./ha-menu-button";
import "./ha-svg-icon";
import "./user/ha-user-badge";
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
@ -153,13 +159,16 @@ class HaSidebar extends LitElement {
<div class="menu">
${!this.narrow
? html`
<ha-icon-button
aria-label=${hass.localize("ui.sidebar.sidebar_toggle")}
.icon=${hass.dockedSidebar === "docked"
? "hass:menu-open"
: "hass:menu"}
<mwc-icon-button
.label=${hass.localize("ui.sidebar.sidebar_toggle")}
@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>
@ -174,14 +183,16 @@ class HaSidebar extends LitElement {
>
${this._renderPanel(
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) =>
this._renderPanel(
panel.url_path,
hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon,
hass.localize(`panel.${panel.title}`) || panel.title
undefined
)
)}
<div class="spacer" disabled></div>
@ -189,8 +200,9 @@ class HaSidebar extends LitElement {
${afterSpacer.map((panel) =>
this._renderPanel(
panel.url_path,
hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon,
hass.localize(`panel.${panel.title}`) || panel.title
undefined
)
)}
${this._externalConfig && this._externalConfig.hasSettingsScreen
@ -443,7 +455,12 @@ class HaSidebar extends LitElement {
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`
<a
aria-role="option"
@ -454,7 +471,12 @@ class HaSidebar extends LitElement {
@mouseleave=${this._itemMouseLeave}
>
<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>
</paper-icon-item>
</a>
@ -496,13 +518,13 @@ class HaSidebar extends LitElement {
width: 256px;
}
.menu ha-icon-button {
.menu mwc-icon-button {
color: var(--sidebar-icon-color);
}
:host([expanded]) .menu ha-icon-button {
:host([expanded]) .menu mwc-icon-button {
margin-right: 23px;
}
:host([expanded][_rtl]) .menu ha-icon-button {
:host([expanded][_rtl]) .menu mwc-icon-button {
margin-right: 0px;
margin-left: 23px;
}
@ -714,7 +736,7 @@ class HaSidebar extends LitElement {
font-weight: 500;
}
:host([_rtl]) .menu ha-icon-button {
:host([_rtl]) .menu mwc-icon-button {
-webkit-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 type { Switch } from "@material/mwc-switch";
import { style } from "@material/mwc-switch/mwc-switch-css";
import {
css,
CSSResult,
customElement,
html,
property,
query,
} from "lit-element";
import { css, CSSResult, customElement, property } from "lit-element";
import { forwardHaptic } from "../data/haptics";
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.
@property({ type: Boolean }) public haptic = false;
@query("slot") private _slot!: HTMLSlotElement;
protected firstUpdated() {
super.firstUpdated();
this.style.setProperty(
"--mdc-theme-secondary",
"var(--switch-checked-color)"
);
this.classList.toggle(
"slotted",
Boolean(this._slot.assignedNodes().length)
);
this.addEventListener("change", () => {
if (this.haptic) {
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[] {
return [
style,
css`
:host {
display: flex;
flex-direction: row;
align-items: center;
}
.mdc-switch.mdc-switch--checked .mdc-switch__thumb {
background-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);
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 {

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;
}
export interface CheckConfigResult {
result: "valid" | "invalid";
errors: string | null;
}
export const saveCoreConfig = (
hass: HomeAssistant,
values: Partial<ConfigUpdateValues>
@ -25,3 +30,6 @@ export const detectCoreConfig = (hass: HomeAssistant) =>
hass.callWS<Partial<ConfigUpdateValues>>({
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";
import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-switch";
import "../../components/ha-formfield";
import type { HaSwitch } from "../../components/ha-switch";
import {
getConfigEntrySystemOptions,
@ -82,13 +83,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
? html` <div class="error">${this._error}</div> `
: ""}
<div class="form">
<ha-switch
.checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting}
>
<div>
<p>
<ha-formfield
.label=${html`<p>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_label"
)}
@ -101,9 +97,15 @@ class DialogConfigEntrySystemOptions extends LitElement {
`component.${this._params.entry.domain}.title`
) || this._params.entry.domain
)}
</p>
</div>
</ha-switch>
</p>`}
>
<ha-switch
.checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting}
>
</ha-switch>
</ha-formfield>
</div>
`}
</paper-dialog-scrollable>
@ -172,9 +174,6 @@ class DialogConfigEntrySystemOptions extends LitElement {
padding-bottom: 24px;
color: var(--primary-text-color);
}
p {
margin: 0;
}
.secondary {
color: var(--secondary-text-color);
}

View File

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

View File

@ -2,8 +2,21 @@ import { css } from "lit-element";
export const configFlowContentStyles = css`
h2 {
margin-top: 24px;
margin: 24px 0 0;
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 {

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import {
css,
@ -11,7 +10,7 @@ import {
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-dialog";
import "../../components/ha-switch";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
@ -41,21 +40,17 @@ class DialogBox extends LitElement {
const confirmPrompt = this._params.confirmation || this._params.prompt;
return html`
<ha-paper-dialog
with-backdrop
opened
modal
@opened-changed="${this._openedChanged}"
<ha-dialog
open
scrimClickAction
escapeKeyAction
@close=${this._close}
.heading=${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize("ui.dialogs.generic.default_confirmation_title")}
>
<h2>
${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize(
"ui.dialogs.generic.default_confirmation_title"
)}
</h2>
<paper-dialog-scrollable>
<div>
${this._params.text
? html`
<p
@ -83,23 +78,21 @@ class DialogBox extends LitElement {
></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>
</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();
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;
}
private _close(): void {
this._params = undefined;
}
static get styles(): CSSResult[] {
@ -141,15 +132,6 @@ class DialogBox extends LitElement {
pointer-events: 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 {
color: var(--primary-color);
}
@ -165,6 +147,10 @@ class DialogBox extends LitElement {
.secondary {
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 "../../components/ha-dialog";
import "../../components/ha-switch";
import "../../components/ha-formfield";
import type { HaSwitch } from "../../components/ha-switch";
import { computeDeviceName } from "../../data/device_registry";
import { fetchMQTTDebugInfo, MQTTDeviceDebugInfo } from "../../data/mqtt";
@ -61,22 +62,28 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
"ui.dialogs.mqtt_device_debug_info.payload_display"
)}
</h4>
<ha-switch
.checked=${this._showDeserialized}
@change=${this._showDeserializedChanged}
>
${this.hass!.localize(
<ha-formfield
.label=${this.hass!.localize(
"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"
)}
</ha-switch>
>
<ha-switch
.checked=${this._showAsYaml}
@change=${this._showAsYamlChanged}
>
</ha-switch>
</ha-formfield>
<h4>
${this.hass!.localize("ui.dialogs.mqtt_device_debug_info.entities")}
</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() {
// Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
_ls("/static/polyfills/custom-elements-es5-adapter.js");
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5PageJS %>");

View File

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

View File

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

View File

@ -91,6 +91,20 @@ class PartialPanelResolver extends HassRouterPage {
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) {
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"]) {
this.routerOptions = getRoutes(this.hass.panels);

View File

@ -1,9 +1,12 @@
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-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
import {
css,
CSSResult,
@ -96,56 +99,64 @@ export default class HaAutomationActionRow extends LitElement {
<div class="card-menu">
${this.index !== 0
? html`
<ha-icon-button
icon="hass:arrow-up"
<mwc-icon-button
.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}
></ha-icon-button>
>
<ha-svg-icon path=${mdiArrowUp}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
${this.index !== this.totalActions - 1
? html`
<ha-icon-button
icon="hass:arrow-down"
<mwc-icon-button
.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}
></ha-icon-button>
>
<ha-svg-icon path=${mdiArrowDown}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
<paper-menu-button
no-animations
horizontal-align="right"
horizontal-offset="-5"
vertical-offset="-5"
close-on-activate
>
<ha-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></ha-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item
@tap=${this._switchYamlMode}
.disabled=${selected === -1}
>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</paper-item>
<paper-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</paper-item>
<paper-item @tap=${this._onDelete}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
<ha-button-menu corner="BOTTOM_START">
<mwc-icon-button
slot="trigger"
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item
@tap=${this._switchYamlMode}
.disabled=${selected === -1}
>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<mwc-list-item @tap=${this._onDelete}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</mwc-list-item>
</ha-button-menu>
</div>
${yamlMode
? html`
@ -259,14 +270,17 @@ export default class HaAutomationActionRow extends LitElement {
top: 0;
right: 0;
z-index: 3;
color: var(--primary-text-color);
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
.card-menu paper-item {
cursor: pointer;
ha-button-menu {
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-textarea";
import { customElement, LitElement, property } from "lit-element";
import { html } from "lit-html";
import { WaitAction } from "../../../../../data/script";
@ -19,7 +20,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
const { wait_template, timeout } = this.action;
return html`
<ha-textarea
<paper-textarea
.label=${this.hass.localize(
"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-changed=${this._valueChanged}
dir="ltr"
></ha-textarea>
></paper-textarea>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_template.timeout"

View File

@ -1,6 +1,8 @@
import "../../../../components/ha-icon-button";
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 {
css,
CSSResult,
@ -61,39 +63,33 @@ export default class HaAutomationConditionRow extends LitElement {
<ha-card>
<div class="card-content">
<div class="card-menu">
<paper-menu-button
no-animations
horizontal-align="right"
horizontal-offset="-5"
vertical-offset="-5"
close-on-activate
>
<ha-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></ha-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @tap=${this._switchYamlMode}>
${this._yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</paper-item>
<paper-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.duplicate"
)}
</paper-item>
<paper-item @tap=${this._onDelete}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.delete"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
<ha-button-menu corner="BOTTOM_START">
<mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
slot="trigger"
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon
></mwc-icon-button>
<mwc-list-item @tap=${this._switchYamlMode}>
${this._yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<mwc-list-item @tap=${this._onDelete}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</mwc-list-item>
</ha-button-menu>
</div>
<ha-automation-condition-editor
.yamlMode=${this._yamlMode}
@ -129,14 +125,17 @@ export default class HaAutomationConditionRow extends LitElement {
top: 0;
right: 0;
z-index: 3;
color: var(--primary-text-color);
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
.card-menu paper-item {
cursor: pointer;
ha-button-menu {
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 { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-textarea";
import "@polymer/paper-input/paper-textarea";
import { NumericStateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-condition-row";
@ -45,7 +45,7 @@ export default class HaNumericStateCondition extends LitElement {
.value=${below}
@value-changed=${this._valueChanged}
></paper-input>
<ha-textarea
<paper-textarea
.label=${this.hass.localize(
"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-changed=${this._valueChanged}
dir="ltr"
></ha-textarea>
></paper-textarea>
`;
}

View File

@ -1,5 +1,5 @@
import { customElement, html, LitElement, property } from "lit-element";
import "../../../../../components/ha-textarea";
import "@polymer/paper-input/paper-textarea";
import { TemplateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-condition-row";
@ -17,7 +17,7 @@ export class HaTemplateCondition extends LitElement {
protected render() {
const { value_template } = this.condition;
return html`
<ha-textarea
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.template.value_template"
)}
@ -25,7 +25,7 @@ export class HaTemplateCondition extends LitElement {
.value=${value_template}
@value-changed=${this._valueChanged}
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-toolbar/app-toolbar";
import "@polymer/paper-input/paper-textarea";
import "../../../components/ha-icon-button";
import {
css,
@ -117,7 +118,7 @@ export class HaAutomationEditor extends LitElement {
@value-changed=${this._valueChanged}
>
</paper-input>
<ha-textarea
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
@ -127,7 +128,7 @@ export class HaAutomationEditor extends LitElement {
name="description"
.value=${this._config.description}
@value-changed=${this._valueChanged}
></ha-textarea>
></paper-textarea>
</div>
${stateObj
? html`

View File

@ -2,8 +2,10 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "../../../../components/ha-icon-button";
import "@polymer/paper-item/paper-item";
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 "@polymer/paper-menu-button/paper-menu-button";
import {
css,
CSSResult,
@ -90,42 +92,36 @@ export default class HaAutomationTriggerRow extends LitElement {
<ha-card>
<div class="card-content">
<div class="card-menu">
<paper-menu-button
no-animations
horizontal-align="right"
horizontal-offset="-5"
vertical-offset="-5"
close-on-activate
>
<ha-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></ha-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item
@tap=${this._switchYamlMode}
.disabled=${selected === -1}
>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</paper-item>
<paper-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
</paper-item>
<paper-item @tap=${this._onDelete}>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.delete"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
<ha-button-menu corner="BOTTOM_START">
<mwc-icon-button
slot="trigger"
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon
></mwc-icon-button>
<mwc-list-item
@tap=${this._switchYamlMode}
.disabled=${selected === -1}
>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<mwc-list-item @tap=${this._onDelete}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</mwc-list-item>
</ha-button-menu>
</div>
${yamlMode
? html`
@ -232,14 +228,17 @@ export default class HaAutomationTriggerRow extends LitElement {
top: 0;
right: 0;
z-index: 3;
color: var(--primary-text-color);
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
.card-menu paper-item {
cursor: pointer;
ha-button-menu {
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 { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-textarea";
import "@polymer/paper-input/paper-textarea";
import { ForDict, NumericStateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row";
@ -61,7 +61,7 @@ export default class HaNumericStateTrigger extends LitElement {
.value=${below}
@value-changed=${this._valueChanged}
></paper-input>
<ha-textarea
<paper-textarea
.label=${this.hass.localize(
"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-changed=${this._valueChanged}
dir="ltr"
></ha-textarea>
></paper-textarea>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for"

View File

@ -1,5 +1,5 @@
import { customElement, html, LitElement, property } from "lit-element";
import "../../../../../components/ha-textarea";
import "@polymer/paper-input/paper-textarea";
import { TemplateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row";
@ -17,7 +17,7 @@ export class HaTemplateTrigger extends LitElement {
protected render() {
const { value_template } = this.trigger;
return html`
<ha-textarea
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.template.value_template"
)}
@ -25,7 +25,7 @@ export class HaTemplateTrigger extends LitElement {
.value=${value_template}
@value-changed=${this._valueChanged}
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-subpage";
import type { HomeAssistant } from "../../../../types";
import "../../../../components/ha-formfield";
const DEFAULT_CONFIG_EXPOSE = true;
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
@ -127,14 +128,19 @@ class CloudAlexa extends LitElement {
)
.join(", ")}
</state-info>
<ha-switch
.entityId=${entity.entity_id}
.disabled=${!emptyFilter}
.checked=${isExposed}
@change=${this._exposeChanged}
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.cloud.alexa.expose"
)}
>
${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>
</ha-card>
`);

View File

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

View File

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

View File

@ -38,7 +38,10 @@ class HaConfigNavigation extends LitElement {
tabindex="-1"
>
<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>
${this.hass.localize(
page.translationKey ||
@ -88,7 +91,7 @@ class HaConfigNavigation extends LitElement {
display: block;
outline: 0;
}
ha-icon,
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
}

View File

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

View File

@ -5,16 +5,17 @@ import {
LitElement,
property,
TemplateResult,
css,
} from "lit-element";
import { DeviceRegistryEntry } from "../../../../data/device_registry";
import { removeMQTTDeviceEntry } from "../../../../data/mqtt";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { showMQTTDeviceDebugInfoDialog } from "../../../../dialogs/mqtt-device-debug-info-dialog/show-dialog-mqtt-device-debug-info";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
import { removeMQTTDeviceEntry } from "../../../../../data/mqtt";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { showMQTTDeviceDebugInfoDialog } from "../../../../../dialogs/mqtt-device-debug-info-dialog/show-dialog-mqtt-device-debug-info";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
@customElement("ha-device-card-mqtt")
export class HaDeviceCardMqtt extends LitElement {
@customElement("ha-device-actions-mqtt")
export class HaDeviceActionsMqtt extends LitElement {
@property() public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@ -47,7 +48,15 @@ export class HaDeviceCardMqtt extends LitElement {
await showMQTTDeviceDebugInfoDialog(this, { device });
}
static get styles(): CSSResult {
return haStyle;
static get styles(): CSSResult[] {
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,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
@ -38,13 +39,12 @@ import "../../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
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-info-card";
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string;
stateName?: string | null;
}
@customElement("ha-config-device-page")
@ -226,16 +226,7 @@ export class HaConfigDevicePage extends LitElement {
.devices=${this.devices}
.device=${device}
>
${
integrations.includes("mqtt")
? html`
<ha-device-card-mqtt
.hass=${this.hass}
.device=${device}
></ha-device-card-mqtt>
`
: html``
}
${this._renderIntegrationInfo(device, integrations)}
</ha-device-info-card>
${
@ -439,7 +430,7 @@ export class HaConfigDevicePage extends LitElement {
</hass-tabs-subpage> `;
}
private _computeEntityName(entity) {
private _computeEntityName(entity: EntityRegistryEntry) {
if (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() {
const device = this._device(this.deviceId, this.devices)!;
showDeviceRegistryDetailDialog(this, {

View File

@ -88,31 +88,31 @@ export class HaEntityRegistryBasicEditor extends LitElement {
.checked=${!this._disabledBy}
@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>
<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>
`;
}
@ -127,9 +127,14 @@ export class HaEntityRegistryBasicEditor extends LitElement {
static get styles() {
return css`
ha-switch {
margin-right: 16px;
}
.row {
margin-top: 8px;
color: var(--primary-text-color);
display: flex;
align-items: center;
}
.secondary {
color: var(--secondary-text-color);

View File

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

View File

@ -9,6 +9,25 @@ import "../../layouts/hass-loading-screen";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
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 {
// for fire event
@ -23,28 +42,28 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "integrations",
path: "/config/integrations",
translationKey: "ui.panel.config.integrations.caption",
icon: "hass:puzzle",
iconPath: mdiPuzzle,
core: true,
},
{
component: "devices",
path: "/config/devices",
translationKey: "ui.panel.config.devices.caption",
icon: "hass:devices",
iconPath: mdiDevices,
core: true,
},
{
component: "entities",
path: "/config/entities",
translationKey: "ui.panel.config.entities.caption",
icon: "hass:shape",
iconPath: mdiShape,
core: true,
},
{
component: "areas",
path: "/config/areas",
translationKey: "ui.panel.config.areas.caption",
icon: "hass:sofa",
iconPath: mdiSofa,
core: true,
},
],
@ -53,25 +72,25 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "automation",
path: "/config/automation",
translationKey: "ui.panel.config.automation.caption",
icon: "hass:robot",
iconPath: mdiRobot,
},
{
component: "scene",
path: "/config/scene",
translationKey: "ui.panel.config.scene.caption",
icon: "hass:palette",
iconPath: mdiPalette,
},
{
component: "script",
path: "/config/script",
translationKey: "ui.panel.config.script.caption",
icon: "hass:script-text",
iconPath: mdiScriptText,
},
{
component: "helpers",
path: "/config/helpers",
translationKey: "ui.panel.config.helpers.caption",
icon: "hass:tools",
iconPath: mdiTools,
core: true,
},
],
@ -80,7 +99,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "lovelace",
path: "/config/lovelace/dashboards",
translationKey: "ui.panel.config.lovelace.caption",
icon: "hass:view-dashboard",
iconPath: mdiViewDashboard,
},
],
persons: [
@ -88,19 +107,19 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "person",
path: "/config/person",
translationKey: "ui.panel.config.person.caption",
icon: "hass:account",
iconPath: mdiAccount,
},
{
component: "zone",
path: "/config/zone",
translationKey: "ui.panel.config.zone.caption",
icon: "hass:map-marker-radius",
iconPath: mdiMapMarkerRadius,
},
{
component: "users",
path: "/config/users",
translationKey: "ui.panel.config.users.caption",
icon: "hass:account-badge-horizontal",
iconPath: mdiAccountBadgeHorizontal,
core: true,
},
],
@ -109,39 +128,41 @@ export const configSections: { [name: string]: PageNavigation[] } = {
component: "core",
path: "/config/core",
translationKey: "ui.panel.config.core.caption",
icon: "hass:home-assistant",
iconPath: mdiHomeAssistant,
core: true,
},
{
component: "server_control",
path: "/config/server_control",
translationKey: "ui.panel.config.server_control.caption",
icon: "hass:server",
iconPath: mdiServer,
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",
path: "/config/customize",
translationKey: "ui.panel.config.customize.caption",
icon: "hass:pencil",
iconPath: mdiPencil,
core: 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")
@ -197,6 +218,20 @@ class HaPanelConfig extends HassRouterPage {
/* 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: {
tag: "ha-config-customize",
load: () =>
@ -278,14 +313,14 @@ class HaPanelConfig extends HassRouterPage {
tag: "zha-config-dashboard-router",
load: () =>
import(
/* webpackChunkName: "panel-config-zha" */ "./zha/zha-config-dashboard-router"
/* webpackChunkName: "panel-config-zha" */ "./integrations/integration-panels/zha/zha-config-dashboard-router"
),
},
zwave: {
tag: "ha-config-zwave",
load: () =>
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";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-input";
import "../../../../components/ha-switch";
import { InputBoolean } from "../../../../data/input_boolean";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ import {
} from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-input";
import "../../../../components/ha-switch";
import { InputText } from "../../../../data/input_text";
import { haStyle } from "../../../../resources/styles";
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 {
return html`
<ha-card
.header=${this.hass.localize(
"ui.panel.developer-tools.tabs.info.integrations"
)}
.header=${this.hass.localize("ui.panel.config.info.integrations")}
>
<table class="card-content">
<tbody>
@ -67,7 +65,7 @@ class IntegrationsCard extends LitElement {
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.developer-tools.tabs.info.documentation"
"ui.panel.config.info.documentation"
)}
</a>
</td>
@ -83,7 +81,7 @@ class IntegrationsCard extends LitElement {
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.developer-tools.tabs.info.issues"
"ui.panel.config.info.issues"
)}
</a>
</td>

View File

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

View File

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

View File

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

View File

@ -12,20 +12,20 @@ import {
PropertyValues,
query,
} from "lit-element";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import type { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import type { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
import {
addGroup,
fetchGroupableDevices,
ZHAGroup,
ZHADeviceEndpoint,
} from "../../../data/zha";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
import type { PolymerChangedEvent } from "../../../polymer-types";
import type { HomeAssistant } from "../../../types";
import "../ha-config-section";
} from "../../../../../data/zha";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-subpage";
import type { PolymerChangedEvent } from "../../../../../polymer-types";
import type { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import "./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 "@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-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
@ -13,9 +13,9 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-service-description";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-service-description";
import {
Attribute,
Cluster,
@ -23,10 +23,10 @@ import {
ReadAttributeServiceData,
readAttributeValue,
ZHADevice,
} from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
} from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import { formatAsPaddedHex } from "./functions";
import {
ChangeEvent,

View File

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

View File

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

View File

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

View File

@ -2,8 +2,9 @@ import { customElement, property } from "lit-element";
import {
HassRouterPage,
RouterOptions,
} from "../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../types";
} from "../../../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../../../types";
import { navigate } from "../../../../../common/navigate";
@customElement("zha-config-dashboard-router")
class ZHAConfigDashboardRouter extends HassRouterPage {
@ -13,6 +14,10 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
@property() public narrow!: boolean;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
@ -24,13 +29,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
/* webpackChunkName: "zha-config-dashboard" */ "./zha-config-dashboard"
),
},
device: {
tag: "zha-device-page",
load: () =>
import(
/* webpackChunkName: "zha-devices-page" */ "./zha-device-page"
),
},
add: {
tag: "zha-add-devices-page",
load: () =>
@ -65,11 +63,24 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
if (this._currentPage === "group") {
el.groupId = this.routeTail.path.substr(1);
} else if (this._currentPage === "device") {
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 "@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-listbox/paper-listbox";
import {
@ -13,13 +13,13 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-service-description";
import { bindDevices, unbindDevices, ZHADevice } from "../../../data/zha";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-service-description";
import { bindDevices, unbindDevices, ZHADevice } from "../../../../../data/zha";
import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import { ItemSelectedEvent } from "./types";
@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,
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../components/data-table/ha-data-table";
import "../../../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
HaDataTable,
DataTableRowData,
} from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-state-icon";
import type { ZHADeviceEndpoint, ZHAEntityReference } from "../../../data/zha";
import { showZHADeviceInfoDialog } from "../../../dialogs/zha-device-info-dialog/show-dialog-zha-device-info";
import type { HomeAssistant } from "../../../types";
} from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/entity/ha-state-icon";
import type {
ZHADeviceEndpoint,
ZHAEntityReference,
} from "../../../../../data/zha";
import type { HomeAssistant } from "../../../../../types";
export interface DeviceEndpointRowData extends DataTableRowData {
id: string;
@ -55,6 +57,7 @@ export class ZHADeviceEndpointDataTable extends LitElement {
ieee: deviceEndpoint.device.ieee,
endpoint_id: deviceEndpoint.endpoint_id,
entities: deviceEndpoint.entities,
dev_id: deviceEndpoint.device.device_reg_id,
});
});
@ -72,14 +75,10 @@ export class ZHADeviceEndpointDataTable extends LitElement {
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
<div
class="mdc-data-table__cell table-cell-text"
@click=${this._handleClicked}
style="cursor: pointer;"
>
template: (name, device: any) => html`
<a href="${`/config/devices/device/${device.dev_id}`}">
${name}
</div>
</a>
`,
},
endpoint_id: {
@ -95,14 +94,10 @@ export class ZHADeviceEndpointDataTable extends LitElement {
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
<div
class="mdc-data-table__cell table-cell-text"
@click=${this._handleClicked}
style="cursor: pointer;"
>
template: (name, device: any) => html`
<a href="${`/config/devices/device/${device.dev_id}`}">
${name}
</div>
</a>
`,
},
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[] {
return [
css`

View File

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