diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/bug_report.md
similarity index 84%
rename from .github/ISSUE_TEMPLATE.md
rename to .github/ISSUE_TEMPLATE/bug_report.md
index 0aa8195d5a..2a6ed52bff 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,10 +1,24 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ""
+labels: bug
+assignees: ""
+---
+
+**Checklist:**
+
+- [ ] I updated to the latest version available
+- [ ] I cleared the cache of my browser
+
**Home Assistant release with the issue:**
+
**Browser and Operating System:**
+
**Description of problem:**
+
-
**Javascript errors shown in the web inspector (if applicable):**
+
```
```
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000000..51d1465c16
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,19 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ""
+labels: feature request
+assignees: ""
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.travis.yml b/.travis.yml
index 4b357440b7..24433d2e24 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,19 +8,20 @@ install: yarn install
script:
- npm run build
- hassio/script/build_hassio
+ # Because else eslint fails because hassio has cleaned that build
+ - ./node_modules/.bin/gulp gen-icons-app
- npm run test
# - xvfb-run wct --module-resolution=node --npm
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
services:
- docker
before_deploy:
- - 'docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21'
+ - "docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21"
deploy:
provider: script
script: script/travis_deploy
- 'on':
+ "on":
branch: master
dist: trusty
addons:
sauce_connect: true
-
diff --git a/build-scripts/babel.js b/build-scripts/babel.js
index 3951116cbd..67021b92c4 100644
--- a/build-scripts/babel.js
+++ b/build-scripts/babel.js
@@ -3,7 +3,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
throw Error("latestBuild not defined for babel loader config");
}
return {
- test: /\.m?js$/,
+ test: /\.m?js$|\.tsx?$/,
use: {
loader: "babel-loader",
options: {
@@ -12,6 +12,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
require("@babel/preset-env").default,
{ modules: false },
],
+ [
+ require("@babel/preset-typescript").default,
+ {
+ jsxPragma: "h",
+ },
+ ],
].filter(Boolean),
plugins: [
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
@@ -21,6 +27,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
],
// Only support the syntax, Webpack will handle it.
"@babel/syntax-dynamic-import",
+ [
+ "@babel/transform-react-jsx",
+ {
+ pragma: "h",
+ },
+ ],
[
require("@babel/plugin-proposal-decorators").default,
{ decoratorsBeforeExport: true },
diff --git a/build-scripts/env.js b/build-scripts/env.js
new file mode 100644
index 0000000000..101858a367
--- /dev/null
+++ b/build-scripts/env.js
@@ -0,0 +1,6 @@
+module.exports = {
+ isProdBuild: process.env.NODE_ENV === "production",
+ isStatsBuild: process.env.STATS === "1",
+ isTravis: process.env.TRAVIS === "true",
+ isNetlify: process.env.NETLIFY === "true",
+};
diff --git a/build-scripts/gulp/app.js b/build-scripts/gulp/app.js
index 1396af5528..985067c71f 100644
--- a/build-scripts/gulp/app.js
+++ b/build-scripts/gulp/app.js
@@ -1,10 +1,13 @@
// Run HA develop mode
const gulp = require("gulp");
+const envVars = require("../env");
+
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
+require("./compress.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
@@ -18,7 +21,7 @@ gulp.task(
"clean",
gulp.parallel(
"gen-service-worker-dev",
- "gen-icons",
+ gulp.parallel("gen-icons-app", "gen-icons-mdi"),
"gen-pages-dev",
"gen-index-app-dev",
gulp.series("create-test-translation", "build-translations")
@@ -35,13 +38,11 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean",
- gulp.parallel("gen-icons", "build-translations"),
+ gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
"copy-static",
- gulp.parallel(
- "webpack-prod-app",
- // Do not compress static files in CI, it's SLOW.
- ...(process.env.CI === "true" ? [] : ["compress-static"])
- ),
+ "webpack-prod-app",
+ ...// Don't compress running tests
+ (envVars.isTravis ? [] : ["compress-app"]),
gulp.parallel(
"gen-pages-prod",
"gen-index-app-prod",
diff --git a/build-scripts/gulp/cast.js b/build-scripts/gulp/cast.js
index 974d2c29cf..34fcbb67a4 100644
--- a/build-scripts/gulp/cast.js
+++ b/build-scripts/gulp/cast.js
@@ -1,4 +1,3 @@
-// Run cast develop mode
const gulp = require("gulp");
require("./clean.js");
@@ -16,7 +15,12 @@ gulp.task(
process.env.NODE_ENV = "development";
},
"clean-cast",
- gulp.parallel("gen-icons", "gen-index-cast-dev", "build-translations"),
+ gulp.parallel(
+ "gen-icons-app",
+ "gen-icons-mdi",
+ "gen-index-cast-dev",
+ "build-translations"
+ ),
"copy-static-cast",
"webpack-dev-server-cast"
)
@@ -29,7 +33,7 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean-cast",
- gulp.parallel("gen-icons", "build-translations"),
+ gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
"copy-static-cast",
"webpack-prod-cast",
"gen-index-cast-prod"
diff --git a/build-scripts/gulp/clean.js b/build-scripts/gulp/clean.js
index f81728fcb3..41fec51e69 100644
--- a/build-scripts/gulp/clean.js
+++ b/build-scripts/gulp/clean.js
@@ -9,15 +9,31 @@ gulp.task(
return del([config.root, config.build_dir]);
})
);
+
gulp.task(
"clean-demo",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.demo_root, config.build_dir]);
})
);
+
gulp.task(
"clean-cast",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.cast_root, config.build_dir]);
})
);
+
+gulp.task(
+ "clean-hassio",
+ gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
+ return del([config.hassio_root, config.build_dir]);
+ })
+);
+
+gulp.task(
+ "clean-gallery",
+ gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
+ return del([config.gallery_root, config.build_dir]);
+ })
+);
diff --git a/build-scripts/gulp/compress.js b/build-scripts/gulp/compress.js
new file mode 100644
index 0000000000..de604f7fa3
--- /dev/null
+++ b/build-scripts/gulp/compress.js
@@ -0,0 +1,38 @@
+// Tasks to compress
+
+const gulp = require("gulp");
+const zopfli = require("gulp-zopfli-green");
+const merge = require("merge-stream");
+const path = require("path");
+const paths = require("../paths");
+
+gulp.task("compress-app", function compressApp() {
+ const jsLatest = gulp
+ .src(path.resolve(paths.output, "**/*.js"))
+ .pipe(zopfli())
+ .pipe(gulp.dest(paths.output));
+
+ const jsEs5 = gulp
+ .src(path.resolve(paths.output_es5, "**/*.js"))
+ .pipe(zopfli())
+ .pipe(gulp.dest(paths.output_es5));
+
+ const polyfills = gulp
+ .src(path.resolve(paths.static, "polyfills/*.js"))
+ .pipe(zopfli())
+ .pipe(gulp.dest(path.resolve(paths.static, "polyfills")));
+
+ const translations = gulp
+ .src(path.resolve(paths.static, "translations/*.json"))
+ .pipe(zopfli())
+ .pipe(gulp.dest(path.resolve(paths.static, "translations")));
+
+ return merge(jsLatest, jsEs5, polyfills, translations);
+});
+
+gulp.task("compress-hassio", function compressApp() {
+ return gulp
+ .src(path.resolve(paths.hassio_root, "**/*.js"))
+ .pipe(zopfli())
+ .pipe(gulp.dest(paths.hassio_root));
+});
diff --git a/build-scripts/gulp/demo.js b/build-scripts/gulp/demo.js
index 2c8070962d..de51760281 100644
--- a/build-scripts/gulp/demo.js
+++ b/build-scripts/gulp/demo.js
@@ -17,7 +17,8 @@ gulp.task(
},
"clean-demo",
gulp.parallel(
- "gen-icons",
+ "gen-icons-app",
+ "gen-icons-mdi",
"gen-icons-demo",
"gen-index-demo-dev",
"build-translations"
@@ -34,7 +35,12 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean-demo",
- gulp.parallel("gen-icons", "gen-icons-demo", "build-translations"),
+ gulp.parallel(
+ "gen-icons-app",
+ "gen-icons-mdi",
+ "gen-icons-demo",
+ "build-translations"
+ ),
"copy-static-demo",
"webpack-prod-demo",
"gen-index-demo-prod"
diff --git a/build-scripts/gulp/entry-html.js b/build-scripts/gulp/entry-html.js
index 33abdff712..e8ad05e8b4 100644
--- a/build-scripts/gulp/entry-html.js
+++ b/build-scripts/gulp/entry-html.js
@@ -11,12 +11,6 @@ const config = require("../paths.js");
const templatePath = (tpl) =>
path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`);
-const demoTemplatePath = (tpl) =>
- path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`);
-
-const castTemplatePath = (tpl) =>
- path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`);
-
const readFile = (pth) => fs.readFileSync(pth).toString();
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
@@ -25,10 +19,19 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
};
const renderDemoTemplate = (pth, data = {}) =>
- renderTemplate(pth, data, demoTemplatePath);
+ renderTemplate(pth, data, (tpl) =>
+ path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`)
+ );
const renderCastTemplate = (pth, data = {}) =>
- renderTemplate(pth, data, castTemplatePath);
+ renderTemplate(pth, data, (tpl) =>
+ path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`)
+ );
+
+const renderGalleryTemplate = (pth, data = {}) =>
+ renderTemplate(pth, data, (tpl) =>
+ path.resolve(config.gallery_dir, "src/html/", `${tpl}.html.template`)
+ );
const minifyHtml = (content) =>
minify(content, {
@@ -209,8 +212,33 @@ gulp.task("gen-index-demo-prod", (done) => {
es5Compatibility: es5Manifest["compatibility.js"],
es5DemoJS: es5Manifest["main.js"],
});
- const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
+ const minified = minifyHtml(content);
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), minified);
done();
});
+
+gulp.task("gen-index-gallery-dev", (done) => {
+ // In dev mode we don't mangle names, so we hardcode urls. That way we can
+ // run webpack as last in watch mode, which blocks output.
+ const content = renderGalleryTemplate("index", {
+ latestGalleryJS: "./entrypoint.js",
+ });
+
+ fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), content);
+ done();
+});
+
+gulp.task("gen-index-gallery-prod", (done) => {
+ const latestManifest = require(path.resolve(
+ config.gallery_output,
+ "manifest.json"
+ ));
+ const content = renderGalleryTemplate("index", {
+ latestGalleryJS: latestManifest["entrypoint.js"],
+ });
+ const minified = minifyHtml(content);
+
+ fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), minified);
+ done();
+});
diff --git a/build-scripts/gulp/gallery.js b/build-scripts/gulp/gallery.js
new file mode 100644
index 0000000000..a46c6c1939
--- /dev/null
+++ b/build-scripts/gulp/gallery.js
@@ -0,0 +1,38 @@
+// Run demo develop mode
+const gulp = require("gulp");
+
+require("./clean.js");
+require("./translations.js");
+require("./gen-icons.js");
+require("./gather-static.js");
+require("./webpack.js");
+require("./service-worker.js");
+require("./entry-html.js");
+
+gulp.task(
+ "develop-gallery",
+ gulp.series(
+ async function setEnv() {
+ process.env.NODE_ENV = "development";
+ },
+ "clean-gallery",
+ gulp.parallel("gen-icons-app", "gen-icons-app", "build-translations"),
+ "copy-static-gallery",
+ "gen-index-gallery-dev",
+ "webpack-dev-server-gallery"
+ )
+);
+
+gulp.task(
+ "build-gallery",
+ gulp.series(
+ async function setEnv() {
+ process.env.NODE_ENV = "production";
+ },
+ "clean-gallery",
+ gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
+ "copy-static-gallery",
+ "webpack-prod-gallery",
+ "gen-index-gallery-prod"
+ )
+);
diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js
index 3d80035e17..17a1a87ae8 100644
--- a/build-scripts/gulp/gather-static.js
+++ b/build-scripts/gulp/gather-static.js
@@ -4,8 +4,6 @@ const gulp = require("gulp");
const path = require("path");
const cpx = require("cpx");
const fs = require("fs-extra");
-const zopfli = require("gulp-zopfli-green");
-const merge = require("merge-stream");
const paths = require("../paths");
const npmPath = (...parts) =>
@@ -67,20 +65,6 @@ function copyMapPanel(staticDir) {
);
}
-function compressStatic(staticDir) {
- const staticPath = genStaticPath(staticDir);
- const polyfills = gulp
- .src(staticPath("polyfills/*.js"))
- .pipe(zopfli())
- .pipe(gulp.dest(staticPath("polyfills")));
- const translations = gulp
- .src(staticPath("translations/*.json"))
- .pipe(zopfli())
- .pipe(gulp.dest(staticPath("translations")));
-
- return merge(polyfills, translations);
-}
-
gulp.task("copy-static", (done) => {
const staticDir = paths.static;
const staticPath = genStaticPath(paths.static);
@@ -100,8 +84,6 @@ gulp.task("copy-static", (done) => {
done();
});
-gulp.task("compress-static", () => compressStatic(paths.static));
-
gulp.task("copy-static-demo", (done) => {
// Copy app static files
fs.copySync(
@@ -129,3 +111,15 @@ gulp.task("copy-static-cast", (done) => {
copyTranslations(paths.cast_static);
done();
});
+
+gulp.task("copy-static-gallery", (done) => {
+ // Copy app static files
+ fs.copySync(polyPath("public/static"), paths.gallery_static);
+ // Copy gallery static files
+ fs.copySync(path.resolve(paths.gallery_dir, "public"), paths.gallery_root);
+
+ copyMapPanel(paths.gallery_static);
+ copyFonts(paths.gallery_static);
+ copyTranslations(paths.gallery_static);
+ done();
+});
diff --git a/build-scripts/gulp/gen-icons.js b/build-scripts/gulp/gen-icons.js
index 11548794c4..b116b8abea 100644
--- a/build-scripts/gulp/gen-icons.js
+++ b/build-scripts/gulp/gen-icons.js
@@ -57,18 +57,6 @@ function generateIconset(iconsetName, iconNames) {
return `${iconDefs} `;
}
-// Generate the full MDI iconset
-function genMDIIcons() {
- const meta = JSON.parse(
- fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
- );
- const iconNames = meta.map((iconInfo) => iconInfo.name);
- if (!fs.existsSync(OUTPUT_DIR)) {
- fs.mkdirSync(OUTPUT_DIR);
- }
- fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
-}
-
// Helper function to map recursively over files in a folder and it's subfolders
function mapFiles(startPath, filter, mapFunc) {
const files = fs.readdirSync(startPath);
@@ -101,24 +89,27 @@ function findIcons(searchPath, iconsetName) {
return icons;
}
-function genHassIcons() {
+gulp.task("gen-icons-mdi", (done) => {
+ const meta = JSON.parse(
+ fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
+ );
+ const iconNames = meta.map((iconInfo) => iconInfo.name);
+ if (!fs.existsSync(OUTPUT_DIR)) {
+ fs.mkdirSync(OUTPUT_DIR);
+ }
+ fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
+ done();
+});
+
+gulp.task("gen-icons-app", (done) => {
const iconNames = findIcons("./src", "hass");
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
-}
-
-gulp.task("gen-icons-mdi", (done) => {
- genMDIIcons();
done();
});
-gulp.task("gen-icons-hass", (done) => {
- genHassIcons();
- done();
-});
-gulp.task("gen-icons", gulp.series("gen-icons-hass", "gen-icons-mdi"));
gulp.task("gen-icons-demo", (done) => {
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
@@ -129,8 +120,21 @@ gulp.task("gen-icons-demo", (done) => {
done();
});
-module.exports = {
- findIcons,
- generateIconset,
- genMDIIcons,
-};
+gulp.task("gen-icons-hassio", (done) => {
+ const iconNames = findIcons(
+ path.resolve(paths.hassio_dir, "./src"),
+ "hassio"
+ );
+ // Find hassio icons inside HA main repo.
+ for (const item of findIcons(
+ path.resolve(paths.polymer_dir, "./src"),
+ "hassio"
+ )) {
+ iconNames.add(item);
+ }
+ fs.writeFileSync(
+ path.resolve(paths.hassio_dir, "hassio-icons.html"),
+ generateIconset("hassio", iconNames)
+ );
+ done();
+});
diff --git a/build-scripts/gulp/hassio.js b/build-scripts/gulp/hassio.js
new file mode 100644
index 0000000000..164a5b9deb
--- /dev/null
+++ b/build-scripts/gulp/hassio.js
@@ -0,0 +1,34 @@
+const gulp = require("gulp");
+
+const envVars = require("../env");
+
+require("./clean.js");
+require("./gen-icons.js");
+require("./webpack.js");
+require("./compress.js");
+
+gulp.task(
+ "develop-hassio",
+ gulp.series(
+ async function setEnv() {
+ process.env.NODE_ENV = "development";
+ },
+ "clean-hassio",
+ gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
+ "webpack-watch-hassio"
+ )
+);
+
+gulp.task(
+ "build-hassio",
+ gulp.series(
+ async function setEnv() {
+ process.env.NODE_ENV = "production";
+ },
+ "clean-hassio",
+ gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
+ "webpack-prod-hassio",
+ ...// Don't compress running tests
+ (envVars.isTravis ? [] : ["compress-hassio"])
+ )
+);
diff --git a/build-scripts/gulp/webpack.js b/build-scripts/gulp/webpack.js
index 6015009d2d..46a114540b 100644
--- a/build-scripts/gulp/webpack.js
+++ b/build-scripts/gulp/webpack.js
@@ -1,6 +1,5 @@
// Tasks to run webpack.
const gulp = require("gulp");
-const path = require("path");
const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server");
const log = require("fancy-log");
@@ -9,8 +8,33 @@ const {
createAppConfig,
createDemoConfig,
createCastConfig,
+ createHassioConfig,
+ createGalleryConfig,
} = require("../webpack");
+const bothBuilds = (createConfigFunc, params) => [
+ createConfigFunc({ ...params, latestBuild: true }),
+ createConfigFunc({ ...params, latestBuild: false }),
+];
+
+const runDevServer = ({
+ compiler,
+ contentBase,
+ port,
+ listenHost = "localhost",
+}) =>
+ new WebpackDevServer(compiler, {
+ open: true,
+ watchContentBase: true,
+ contentBase,
+ }).listen(port, listenHost, function(err) {
+ if (err) {
+ throw err;
+ }
+ // Server listening
+ log("[webpack-dev-server]", `http://localhost:${port}`);
+ });
+
const handler = (done) => (err, stats) => {
if (err) {
console.log(err.stack || err);
@@ -32,20 +56,11 @@ const handler = (done) => (err, stats) => {
};
gulp.task("webpack-watch-app", () => {
- const compiler = webpack([
- createAppConfig({
- isProdBuild: false,
- latestBuild: true,
- isStatsBuild: false,
- }),
- createAppConfig({
- isProdBuild: false,
- latestBuild: false,
- isStatsBuild: false,
- }),
- ]);
- compiler.watch({}, handler());
// we are not calling done, so this command will run forever
+ webpack(bothBuilds(createAppConfig, { isProdBuild: false })).watch(
+ {},
+ handler()
+ );
});
gulp.task(
@@ -53,47 +68,17 @@ gulp.task(
() =>
new Promise((resolve) =>
webpack(
- [
- createAppConfig({
- isProdBuild: true,
- latestBuild: true,
- isStatsBuild: false,
- }),
- createAppConfig({
- isProdBuild: true,
- latestBuild: false,
- isStatsBuild: false,
- }),
- ],
+ bothBuilds(createAppConfig, { isProdBuild: true }),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-demo", () => {
- const compiler = webpack([
- createDemoConfig({
- isProdBuild: false,
- latestBuild: false,
- isStatsBuild: false,
- }),
- createDemoConfig({
- isProdBuild: false,
- latestBuild: true,
- isStatsBuild: false,
- }),
- ]);
-
- new WebpackDevServer(compiler, {
- open: true,
- watchContentBase: true,
- contentBase: path.resolve(paths.demo_dir, "dist"),
- }).listen(8090, "localhost", function(err) {
- if (err) {
- throw err;
- }
- // Server listening
- log("[webpack-dev-server]", "http://localhost:8090");
+ runDevServer({
+ compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
+ contentBase: paths.demo_root,
+ port: 8090,
});
});
@@ -102,51 +87,22 @@ gulp.task(
() =>
new Promise((resolve) =>
webpack(
- [
- createDemoConfig({
- isProdBuild: true,
- latestBuild: false,
- isStatsBuild: false,
- }),
- createDemoConfig({
- isProdBuild: true,
- latestBuild: true,
- isStatsBuild: false,
- }),
- ],
+ bothBuilds(createDemoConfig, {
+ isProdBuild: true,
+ }),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-cast", () => {
- const compiler = webpack([
- createCastConfig({
- isProdBuild: false,
- latestBuild: false,
- }),
- createCastConfig({
- isProdBuild: false,
- latestBuild: true,
- }),
- ]);
-
- new WebpackDevServer(compiler, {
- open: true,
- watchContentBase: true,
- contentBase: path.resolve(paths.cast_dir, "dist"),
- }).listen(
- 8080,
+ runDevServer({
+ compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
+ contentBase: paths.cast_root,
+ port: 8080,
// Accessible from the network, because that's how Cast hits it.
- "0.0.0.0",
- function(err) {
- if (err) {
- throw err;
- }
- // Server listening
- log("[webpack-dev-server]", "http://localhost:8080");
- }
- );
+ listenHost: "0.0.0.0",
+ });
});
gulp.task(
@@ -154,16 +110,59 @@ gulp.task(
() =>
new Promise((resolve) =>
webpack(
- [
- createCastConfig({
- isProdBuild: true,
- latestBuild: false,
- }),
- createCastConfig({
- isProdBuild: true,
- latestBuild: true,
- }),
- ],
+ bothBuilds(createCastConfig, {
+ isProdBuild: true,
+ }),
+
+ handler(resolve)
+ )
+ )
+);
+
+gulp.task("webpack-watch-hassio", () => {
+ // we are not calling done, so this command will run forever
+ webpack(
+ createHassioConfig({
+ isProdBuild: false,
+ latestBuild: false,
+ })
+ ).watch({}, handler());
+});
+
+gulp.task(
+ "webpack-prod-hassio",
+ () =>
+ new Promise((resolve) =>
+ webpack(
+ createHassioConfig({
+ isProdBuild: true,
+ latestBuild: false,
+ }),
+ handler(resolve)
+ )
+ )
+);
+
+gulp.task("webpack-dev-server-gallery", () => {
+ runDevServer({
+ compiler: webpack(
+ createGalleryConfig({ latestBuild: true, isProdBuild: false })
+ ),
+ contentBase: paths.gallery_root,
+ port: 8100,
+ });
+});
+
+gulp.task(
+ "webpack-prod-gallery",
+ () =>
+ new Promise((resolve) =>
+ webpack(
+ createGalleryConfig({
+ isProdBuild: true,
+ latestBuild: true,
+ }),
+
handler(resolve)
)
)
diff --git a/build-scripts/paths.js b/build-scripts/paths.js
index f59dc35399..4a26ab882c 100644
--- a/build-scripts/paths.js
+++ b/build-scripts/paths.js
@@ -20,4 +20,13 @@ module.exports = {
cast_static: path.resolve(__dirname, "../cast/dist/static"),
cast_output: path.resolve(__dirname, "../cast/dist/frontend_latest"),
cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"),
+
+ gallery_dir: path.resolve(__dirname, "../gallery"),
+ gallery_root: path.resolve(__dirname, "../gallery/dist"),
+ gallery_output: path.resolve(__dirname, "../gallery/dist/frontend_latest"),
+ gallery_static: path.resolve(__dirname, "../gallery/dist/static"),
+
+ hassio_dir: path.resolve(__dirname, "../hassio"),
+ hassio_root: path.resolve(__dirname, "../hassio/build"),
+ hassio_publicPath: "/api/hassio/app",
};
diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js
index b7d8948f5d..ac04587333 100644
--- a/build-scripts/webpack.js
+++ b/build-scripts/webpack.js
@@ -3,8 +3,6 @@ const fs = require("fs");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
-const CompressionPlugin = require("compression-webpack-plugin");
-const zopfli = require("@gfx/zopfli");
const ManifestPlugin = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const { babelLoaderConfig } = require("./babel.js");
@@ -17,288 +15,246 @@ if (!version) {
}
version = version[0];
-const genMode = (isProdBuild) => (isProdBuild ? "production" : "development");
-const genDevTool = (isProdBuild) =>
- isProdBuild ? "source-map" : "inline-cheap-module-source-map";
-const genFilename = (isProdBuild, dontHash = new Set()) => ({ chunk }) => {
- if (!isProdBuild || dontHash.has(chunk.name)) {
- return `${chunk.name}.js`;
- }
- return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
-};
-const genChunkFilename = (isProdBuild, isStatsBuild) =>
- isProdBuild && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
-
-const resolve = {
- extensions: [".ts", ".js", ".json", ".tsx"],
- alias: {
- react: "preact-compat",
- "react-dom": "preact-compat",
- // Not necessary unless you consume a module using `createClass`
- "create-react-class": "preact-compat/lib/create-react-class",
- // Not necessary unless you consume a module requiring `react-dom-factories`
- "react-dom-factories": "preact-compat/lib/react-dom-factories",
- },
-};
-
-const tsLoader = (latestBuild) => ({
- test: /\.ts|tsx$/,
- exclude: [path.resolve(paths.polymer_dir, "node_modules")],
- use: [
- {
- loader: "ts-loader",
- options: {
- compilerOptions: latestBuild
- ? { noEmit: false }
- : { target: "es5", noEmit: false },
- },
- },
- ],
-});
-const cssLoader = {
- test: /\.css$/,
- use: "raw-loader",
-};
-const htmlLoader = {
- test: /\.(html)$/,
- use: {
- loader: "html-loader",
- options: {
- exportAsEs6Default: true,
- },
- },
-};
-
-const plugins = [
- // Ignore moment.js locales
- new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
- // Color.js is bloated, it contains all color definitions for all material color sets.
- new webpack.NormalModuleReplacementPlugin(
- /@polymer\/paper-styles\/color\.js$/,
- path.resolve(paths.polymer_dir, "src/util/empty.js")
- ),
- // Ignore roboto pointing at CDN. We use local font-roboto-local.
- new webpack.NormalModuleReplacementPlugin(
- /@polymer\/font-roboto\/roboto\.js$/,
- path.resolve(paths.polymer_dir, "src/util/empty.js")
- ),
- // Ignore mwc icons pointing at CDN.
- new webpack.NormalModuleReplacementPlugin(
- /@material\/mwc-icon\/mwc-icon-font\.js$/,
- path.resolve(paths.polymer_dir, "src/util/empty.js")
- ),
-];
-
-const optimization = (latestBuild) => ({
- minimizer: [
- new TerserPlugin({
- cache: true,
- parallel: true,
- extractComments: true,
- sourceMap: true,
- terserOptions: {
- safari10: true,
- ecma: latestBuild ? undefined : 5,
- },
- }),
- ],
-});
-
-const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
- const isCI = process.env.CI === "true";
-
- // Create an object mapping browser urls to their paths during build
- const translationMetadata = require("../build-translations/translationMetadata.json");
- const workBoxTranslationsTemplatedURLs = {};
- const englishFP = translationMetadata.translations.en.fingerprints;
- Object.keys(englishFP).forEach((key) => {
- workBoxTranslationsTemplatedURLs[
- `/static/translations/${englishFP[key]}`
- ] = `build-translations/output/${key}.json`;
- });
-
- const entry = {
- app: "./src/entrypoints/app.ts",
- authorize: "./src/entrypoints/authorize.ts",
- onboarding: "./src/entrypoints/onboarding.ts",
- core: "./src/entrypoints/core.ts",
- compatibility: "./src/entrypoints/compatibility.ts",
- "custom-panel": "./src/entrypoints/custom-panel.ts",
- "hass-icons": "./src/entrypoints/hass-icons.ts",
- };
-
- const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
- if (!latestBuild) {
- rules.push(babelLoaderConfig({ latestBuild }));
- }
-
+const createWebpackConfig = ({
+ entry,
+ outputRoot,
+ defineOverlay,
+ isProdBuild,
+ latestBuild,
+ isStatsBuild,
+}) => {
return {
- mode: genMode(isProdBuild),
- devtool: genDevTool(isProdBuild),
+ mode: isProdBuild ? "production" : "development",
+ devtool: isProdBuild ? "source-map" : "inline-cheap-module-source-map",
entry,
module: {
- rules,
+ rules: [
+ babelLoaderConfig({ latestBuild }),
+ {
+ test: /\.css$/,
+ use: "raw-loader",
+ },
+ {
+ test: /\.(html)$/,
+ use: {
+ loader: "html-loader",
+ options: {
+ exportAsEs6Default: true,
+ },
+ },
+ },
+ ],
+ },
+ optimization: {
+ minimizer: [
+ new TerserPlugin({
+ cache: true,
+ parallel: true,
+ extractComments: true,
+ sourceMap: true,
+ terserOptions: {
+ safari10: true,
+ ecma: latestBuild ? undefined : 5,
+ },
+ }),
+ ],
},
- optimization: optimization(latestBuild),
plugins: [
new ManifestPlugin(),
new webpack.DefinePlugin({
- __DEV__: JSON.stringify(!isProdBuild),
- __DEMO__: false,
+ __DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(version),
+ __DEMO__: false,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
+ ...defineOverlay,
}),
- ...plugins,
- isProdBuild &&
- !isCI &&
- !isStatsBuild &&
- new CompressionPlugin({
- cache: true,
- exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
- algorithm(input, compressionOptions, callback) {
- return zopfli.gzip(input, compressionOptions, callback);
- },
- }),
- latestBuild &&
- new WorkboxPlugin.InjectManifest({
- swSrc: "./src/entrypoints/service-worker-hass.js",
- swDest: "service_worker.js",
- importWorkboxFrom: "local",
- include: [/\.js$/],
- templatedURLs: {
- ...workBoxTranslationsTemplatedURLs,
- "/static/icons/favicon-192x192.png":
- "public/icons/favicon-192x192.png",
- "/static/fonts/roboto/Roboto-Light.woff2":
- "node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
- "/static/fonts/roboto/Roboto-Medium.woff2":
- "node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
- "/static/fonts/roboto/Roboto-Regular.woff2":
- "node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
- "/static/fonts/roboto/Roboto-Bold.woff2":
- "node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
- },
- }),
+ // Ignore moment.js locales
+ new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
+ // Color.js is bloated, it contains all color definitions for all material color sets.
+ new webpack.NormalModuleReplacementPlugin(
+ /@polymer\/paper-styles\/color\.js$/,
+ path.resolve(paths.polymer_dir, "src/util/empty.js")
+ ),
+ // Ignore roboto pointing at CDN. We use local font-roboto-local.
+ new webpack.NormalModuleReplacementPlugin(
+ /@polymer\/font-roboto\/roboto\.js$/,
+ path.resolve(paths.polymer_dir, "src/util/empty.js")
+ ),
+ // Ignore mwc icons pointing at CDN.
+ new webpack.NormalModuleReplacementPlugin(
+ /@material\/mwc-icon\/mwc-icon-font\.js$/,
+ path.resolve(paths.polymer_dir, "src/util/empty.js")
+ ),
].filter(Boolean),
+ resolve: {
+ extensions: [".ts", ".js", ".json", ".tsx"],
+ alias: {
+ react: "preact-compat",
+ "react-dom": "preact-compat",
+ // Not necessary unless you consume a module using `createClass`
+ "create-react-class": "preact-compat/lib/create-react-class",
+ // Not necessary unless you consume a module requiring `react-dom-factories`
+ "react-dom-factories": "preact-compat/lib/react-dom-factories",
+ },
+ },
output: {
- filename: genFilename(isProdBuild),
- chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
- path: latestBuild ? paths.output : paths.output_es5,
+ filename: ({ chunk }) => {
+ const dontHash = new Set();
+
+ if (!isProdBuild || dontHash.has(chunk.name)) {
+ return `${chunk.name}.js`;
+ }
+ return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
+ },
+ chunkFilename:
+ isProdBuild && !isStatsBuild
+ ? "chunk.[chunkhash].js"
+ : "[name].chunk.js",
+ path: path.resolve(
+ outputRoot,
+ latestBuild ? "frontend_latest" : "frontend_es5"
+ ),
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
// For workerize loader
globalObject: "self",
},
- resolve,
};
};
+const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
+ const config = createWebpackConfig({
+ entry: {
+ app: "./src/entrypoints/app.ts",
+ authorize: "./src/entrypoints/authorize.ts",
+ onboarding: "./src/entrypoints/onboarding.ts",
+ core: "./src/entrypoints/core.ts",
+ compatibility: "./src/entrypoints/compatibility.ts",
+ "custom-panel": "./src/entrypoints/custom-panel.ts",
+ "hass-icons": "./src/entrypoints/hass-icons.ts",
+ },
+ outputRoot: paths.root,
+ isProdBuild,
+ latestBuild,
+ isStatsBuild,
+ });
+
+ if (latestBuild) {
+ // Create an object mapping browser urls to their paths during build
+ const translationMetadata = require("../build-translations/translationMetadata.json");
+ const workBoxTranslationsTemplatedURLs = {};
+ const englishFP = translationMetadata.translations.en.fingerprints;
+ Object.keys(englishFP).forEach((key) => {
+ workBoxTranslationsTemplatedURLs[
+ `/static/translations/${englishFP[key]}`
+ ] = `build-translations/output/${key}.json`;
+ });
+
+ config.plugins.push(
+ new WorkboxPlugin.InjectManifest({
+ swSrc: "./src/entrypoints/service-worker-hass.js",
+ swDest: "service_worker.js",
+ importWorkboxFrom: "local",
+ include: [/\.js$/],
+ templatedURLs: {
+ ...workBoxTranslationsTemplatedURLs,
+ "/static/icons/favicon-192x192.png":
+ "public/icons/favicon-192x192.png",
+ "/static/fonts/roboto/Roboto-Light.woff2":
+ "node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
+ "/static/fonts/roboto/Roboto-Medium.woff2":
+ "node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
+ "/static/fonts/roboto/Roboto-Regular.woff2":
+ "node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
+ "/static/fonts/roboto/Roboto-Bold.woff2":
+ "node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
+ },
+ })
+ );
+ }
+
+ return config;
+};
+
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
- const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
- if (!latestBuild) {
- rules.push(babelLoaderConfig({ latestBuild }));
- }
-
- return {
- mode: genMode(isProdBuild),
- devtool: genDevTool(isProdBuild),
+ return createWebpackConfig({
entry: {
- main: "./demo/src/entrypoint.ts",
- compatibility: "./src/entrypoints/compatibility.ts",
- },
- module: {
- rules,
- },
- optimization: optimization(latestBuild),
- plugins: [
- new ManifestPlugin(),
- new webpack.DefinePlugin({
- __DEV__: !isProdBuild,
- __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
- __VERSION__: JSON.stringify(`DEMO-${version}`),
- __DEMO__: true,
- __STATIC_PATH__: "/static/",
- "process.env.NODE_ENV": JSON.stringify(
- isProdBuild ? "production" : "development"
- ),
- }),
- ...plugins,
- ].filter(Boolean),
- resolve,
- output: {
- filename: genFilename(isProdBuild),
- chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
- path: path.resolve(
- paths.demo_root,
- latestBuild ? "frontend_latest" : "frontend_es5"
+ main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
+ compatibility: path.resolve(
+ paths.polymer_dir,
+ "src/entrypoints/compatibility.ts"
),
- publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
- // For workerize loader
- globalObject: "self",
},
- };
+ outputRoot: paths.demo_root,
+ defineOverlay: {
+ __VERSION__: JSON.stringify(`DEMO-${version}`),
+ __DEMO__: true,
+ },
+ isProdBuild,
+ latestBuild,
+ isStatsBuild,
+ });
};
const createCastConfig = ({ isProdBuild, latestBuild }) => {
- const isStatsBuild = false;
const entry = {
- launcher: "./cast/src/launcher/entrypoint.ts",
+ launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
};
if (latestBuild) {
- entry.receiver = "./cast/src/receiver/entrypoint.ts";
+ entry.receiver = path.resolve(paths.cast_dir, "src/receiver/entrypoint.ts");
}
- const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
- if (!latestBuild) {
- rules.push(babelLoaderConfig({ latestBuild }));
- }
-
- return {
- mode: genMode(isProdBuild),
- devtool: genDevTool(isProdBuild),
+ return createWebpackConfig({
entry,
- module: {
- rules,
+ outputRoot: paths.cast_root,
+ isProdBuild,
+ latestBuild,
+ });
+};
+
+const createHassioConfig = ({ isProdBuild, latestBuild }) => {
+ if (latestBuild) {
+ throw new Error("Hass.io does not support latest build!");
+ }
+ const config = createWebpackConfig({
+ entry: {
+ entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.js"),
},
- optimization: optimization(latestBuild),
- plugins: [
- new ManifestPlugin(),
- new webpack.DefinePlugin({
- __DEV__: !isProdBuild,
- __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
- __VERSION__: JSON.stringify(version),
- __DEMO__: false,
- __STATIC_PATH__: "/static/",
- "process.env.NODE_ENV": JSON.stringify(
- isProdBuild ? "production" : "development"
- ),
- }),
- ...plugins,
- ].filter(Boolean),
- resolve,
- output: {
- filename: genFilename(isProdBuild),
- chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
- path: path.resolve(
- paths.cast_root,
- latestBuild ? "frontend_latest" : "frontend_es5"
- ),
- publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
- // For workerize loader
- globalObject: "self",
+ outputRoot: "",
+ isProdBuild,
+ latestBuild,
+ });
+
+ config.output.path = paths.hassio_root;
+ config.output.publicPath = paths.hassio_publicPath;
+
+ return config;
+};
+
+const createGalleryConfig = ({ isProdBuild, latestBuild }) => {
+ if (!latestBuild) {
+ throw new Error("Gallery only supports latest build!");
+ }
+ const config = createWebpackConfig({
+ entry: {
+ entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"),
},
- };
+ outputRoot: paths.gallery_root,
+ isProdBuild,
+ latestBuild,
+ });
+
+ return config;
};
module.exports = {
- resolve,
- plugins,
- optimization,
createAppConfig,
createDemoConfig,
createCastConfig,
+ createHassioConfig,
+ createGalleryConfig,
};
diff --git a/cast/webpack.config.js b/cast/webpack.config.js
new file mode 100644
index 0000000000..bb9746cd11
--- /dev/null
+++ b/cast/webpack.config.js
@@ -0,0 +1,11 @@
+const { createCastConfig } = require("../build-scripts/webpack.js");
+const { isProdBuild } = require("../build-scripts/env.js");
+
+// File just used for stats builds
+
+const latestBuild = true;
+
+module.exports = createCastConfig({
+ isProdBuild,
+ latestBuild,
+});
diff --git a/demo/src/configs/arsaboo/entities.ts b/demo/src/configs/arsaboo/entities.ts
index 77a4d1c16a..fcae1bc846 100644
--- a/demo/src/configs/arsaboo/entities.ts
+++ b/demo/src/configs/arsaboo/entities.ts
@@ -217,6 +217,18 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
icon: "hademo:currency-usd",
},
},
+ "sensor.study_temp": {
+ entity_id: "sensor.study_temp",
+ state: "20.9",
+ attributes: {
+ unit_of_measurement: "°C",
+ device_class: "temperature",
+ friendly_name: localize(
+ "ui.panel.page-demo.config.arsaboo.names.temperature_study"
+ ),
+ icon: "hademo:thermometer",
+ },
+ },
"cover.garagedoor": {
entity_id: "cover.garagedoor",
state: "closed",
diff --git a/demo/src/configs/arsaboo/lovelace.ts b/demo/src/configs/arsaboo/lovelace.ts
index ddb95ad98b..79a275c578 100644
--- a/demo/src/configs/arsaboo/lovelace.ts
+++ b/demo/src/configs/arsaboo/lovelace.ts
@@ -446,6 +446,11 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
"script.tv_off",
],
},
+ {
+ type: "sensor",
+ entity: "sensor.study_temp",
+ graph: "line",
+ },
{
type: "entities",
title: "Doorbell",
diff --git a/demo/src/configs/jimpower/theme.ts b/demo/src/configs/jimpower/theme.ts
index 8f9e28fe60..655c6da45d 100644
--- a/demo/src/configs/jimpower/theme.ts
+++ b/demo/src/configs/jimpower/theme.ts
@@ -23,27 +23,24 @@ export const demoThemeJimpower = () => ({
"paper-listbox-background-color": "#2E333A",
"table-row-background-color": "#353840",
"paper-grey-50": "var(--primary-text-color)",
- "paper-toggle-button-checked-button-color": "var(--accent-color)",
+ "switch-checked-color": "var(--accent-color)",
"paper-dialog-background-color": "#434954",
"secondary-text-color": "#5294E2",
"google-red-500": "#E45E65",
"divider-color": "rgba(0, 0, 0, .12)",
- "paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
"google-green-500": "#39E949",
- "paper-toggle-button-unchecked-button-color": "var(--disabled-text-color)",
+ "switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green",
"paper-listbox-color": "var(--primary-color)",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
- "paper-toggle-button-checked-ink-color": "var(--accent-color)",
"paper-card-background-color": "#434954",
"label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-color)",
- "paper-toggle-button-unchecked-bar-color": "var(--disabled-text-color)",
+ "switch-unchecked-track-color": "var(--disabled-text-color)",
"dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#F9C536",
"accent-color": "#E45E65",
- "paper-toggle-button-checked-bar-color": "var(--accent-color)",
"table-row-alternative-background-color": "#3E424B",
});
diff --git a/demo/src/configs/kernehed/theme.ts b/demo/src/configs/kernehed/theme.ts
index a4efa29f40..078dc5ac93 100644
--- a/demo/src/configs/kernehed/theme.ts
+++ b/demo/src/configs/kernehed/theme.ts
@@ -24,27 +24,24 @@ export const demoThemeKernehed = () => ({
"paper-listbox-background-color": "#141414",
"table-row-background-color": "#292929",
"paper-grey-50": "var(--primary-text-color)",
- "paper-toggle-button-checked-button-color": "var(--accent-color)",
+ "switch-checked-color": "var(--accent-color)",
"paper-dialog-background-color": "#292929",
"secondary-text-color": "#b58e31",
"google-red-500": "#b58e31",
"divider-color": "rgba(0, 0, 0, .12)",
- "paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
"google-green-500": "#2980b9",
- "paper-toggle-button-unchecked-button-color": "var(--disabled-text-color)",
+ "switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green",
"paper-listbox-color": "#777777",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
- "paper-toggle-button-checked-ink-color": "var(--accent-color)",
"paper-card-background-color": "#292929",
"label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-color)",
- "paper-toggle-button-unchecked-bar-color": "var(--disabled-text-color)",
+ "switch-unchecked-track-color": "var(--disabled-text-color)",
"dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#b58e31",
"accent-color": "#2980b9",
- "paper-toggle-button-checked-bar-color": "var(--accent-color)",
"table-row-alternative-background-color": "#292929",
});
diff --git a/demo/src/configs/teachingbirds/theme.ts b/demo/src/configs/teachingbirds/theme.ts
index 890100d90e..8861bd0663 100644
--- a/demo/src/configs/teachingbirds/theme.ts
+++ b/demo/src/configs/teachingbirds/theme.ts
@@ -12,8 +12,7 @@ export const demoThemeTeachingbirds = () => ({
"paper-slider-knob-color": "var(--primary-color)",
"paper-listbox-color": "#FFFFFF",
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
- "paper-toggle-button-checked-ink-color": "var(--dark-primary-color)",
- "paper-toggle-button-unchecked-bar-color": "var(--primary-text-color)",
+ "switch-unchecked-track-color": "var(--primary-text-color)",
"paper-card-background-color": "#4e4e4e",
"label-badge-text-color": "var(--text-primary-color)",
"primary-background-color": "#303030",
@@ -22,7 +21,7 @@ export const demoThemeTeachingbirds = () => ({
"secondary-background-color": "#2b2b2b",
"paper-slider-knob-start-color": "var(--primary-color)",
"paper-item-icon-active-color": "#d8bf50",
- "paper-toggle-button-checked-button-color": "var(--primary-color)",
+ "switch-checked-color": "var(--primary-color)",
"secondary-text-color": "#389638",
"disabled-text-color": "#545454",
"paper-item-icon_-_color": "var(--primary-text-color)",
diff --git a/demo/src/custom-cards/ha-demo-card.ts b/demo/src/custom-cards/ha-demo-card.ts
index c9b4f1d060..45c887db9d 100644
--- a/demo/src/custom-cards/ha-demo-card.ts
+++ b/demo/src/custom-cards/ha-demo-card.ts
@@ -1,10 +1,4 @@
-import {
- LitElement,
- html,
- CSSResult,
- css,
- PropertyDeclarations,
-} from "lit-element";
+import { LitElement, html, CSSResult, css, property } from "lit-element";
import { until } from "lit-html/directives/until";
import "@material/mwc-button";
import "@polymer/paper-spinner/paper-spinner-lite";
@@ -20,19 +14,11 @@ import {
} from "../configs/demo-configs";
export class HADemoCard extends LitElement implements LovelaceCard {
- public lovelace?: Lovelace;
- public hass!: MockHomeAssistant;
- private _switching?: boolean;
+ @property() public lovelace?: Lovelace;
+ @property() public hass!: MockHomeAssistant;
+ @property() private _switching?: boolean;
private _hidden = localStorage.hide_demo_card;
- static get properties(): PropertyDeclarations {
- return {
- lovelace: {},
- hass: {},
- _switching: {},
- };
- }
-
public getCardSize() {
return this._hidden ? 0 : 2;
}
diff --git a/demo/webpack.config.js b/demo/webpack.config.js
index 266523cdbe..9ccb790f8b 100644
--- a/demo/webpack.config.js
+++ b/demo/webpack.config.js
@@ -1,10 +1,9 @@
const { createDemoConfig } = require("../build-scripts/webpack.js");
+const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
-// This file exists because we haven't migrated the stats script yet
+// File just used for stats builds
-const isProdBuild = process.env.NODE_ENV === "production";
-const isStatsBuild = process.env.STATS === "1";
-const latestBuild = false;
+const latestBuild = true;
module.exports = createDemoConfig({
isProdBuild,
diff --git a/gallery/public/index.html b/gallery/public/index.html
deleted file mode 100644
index 7339634a60..0000000000
--- a/gallery/public/index.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
- HAGallery
-
-
-
-
-
diff --git a/gallery/script/build_gallery b/gallery/script/build_gallery
index cbf70c6d4e..3a7c77dde7 100755
--- a/gallery/script/build_gallery
+++ b/gallery/script/build_gallery
@@ -4,14 +4,6 @@
# Stop on errors
set -e
-cd "$(dirname "$0")/.."
+cd "$(dirname "$0")/../.."
-OUTPUT_DIR=dist
-
-rm -rf $OUTPUT_DIR
-
-cd ..
-./node_modules/.bin/gulp build-translations gen-icons
-cd gallery
-
-NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js
+./node_modules/.bin/gulp build-gallery
diff --git a/gallery/script/develop_gallery b/gallery/script/develop_gallery
index 56bb2c678b..b346ea60bc 100755
--- a/gallery/script/develop_gallery
+++ b/gallery/script/develop_gallery
@@ -4,10 +4,6 @@
# Stop on errors
set -e
-cd "$(dirname "$0")/.."
+cd "$(dirname "$0")/../.."
-cd ..
-./node_modules/.bin/gulp build-translations gen-icons
-cd gallery
-
-../node_modules/.bin/webpack-dev-server
+./node_modules/.bin/gulp develop-gallery
diff --git a/gallery/src/components/demo-card.js b/gallery/src/components/demo-card.js
index adc7282ba1..b782aaf882 100644
--- a/gallery/src/components/demo-card.js
+++ b/gallery/src/components/demo-card.js
@@ -1,6 +1,6 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
-import JsYaml from "js-yaml";
+import { safeLoad } from "js-yaml";
import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";
@@ -62,7 +62,7 @@ class DemoCard extends PolymerElement {
card.removeChild(card.lastChild);
}
- const el = createCardElement(JsYaml.safeLoad(config.config)[0]);
+ const el = createCardElement(safeLoad(config.config)[0]);
el.hass = this.hass;
card.appendChild(el);
}
diff --git a/gallery/src/components/demo-cards.js b/gallery/src/components/demo-cards.js
index a73bc53a2f..db01c7fe13 100644
--- a/gallery/src/components/demo-cards.js
+++ b/gallery/src/components/demo-cards.js
@@ -26,7 +26,9 @@ class DemoCards extends PolymerElement {
- Show config
+
+ Show config
+
@@ -51,6 +53,10 @@ class DemoCards extends PolymerElement {
},
};
}
+
+ _showConfigToggled(ev) {
+ this._showConfig = ev.target.checked;
+ }
}
customElements.define("demo-cards", DemoCards);
diff --git a/gallery/src/demos/demo-util-long-press.ts b/gallery/src/demos/demo-util-long-press.ts
index 5f09e3598f..86d245fe50 100644
--- a/gallery/src/demos/demo-util-long-press.ts
+++ b/gallery/src/demos/demo-util-long-press.ts
@@ -12,7 +12,7 @@ export class DemoUtilLongPress extends LitElement {
() => html`
@@ -28,7 +28,7 @@ export class DemoUtilLongPress extends LitElement {
`;
}
- private _handleTap(ev: Event) {
+ private _handleClick(ev: Event) {
this._addValue(ev, "tap");
}
diff --git a/gallery/src/html/index.html.template b/gallery/src/html/index.html.template
new file mode 100644
index 0000000000..3240e59bb0
--- /dev/null
+++ b/gallery/src/html/index.html.template
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ HAGallery
+
+
+
+
+
+
diff --git a/gallery/webpack.config.js b/gallery/webpack.config.js
index 0c3e364379..9bc82eedc3 100644
--- a/gallery/webpack.config.js
+++ b/gallery/webpack.config.js
@@ -1,6 +1,6 @@
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
-const webpackBase = require("../build-scripts/webpack.js");
+const { createGalleryConfig } = require("../build-scripts/webpack.js");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
const isProd = process.env.NODE_ENV === "production";
@@ -9,80 +9,64 @@ const buildPath = path.resolve(__dirname, "dist");
const publicPath = isProd ? "./" : "http://localhost:8080/";
const latestBuild = true;
-const rules = [
- {
- exclude: [path.resolve(__dirname, "../node_modules")],
- test: /\.ts$/,
- use: [
- {
- loader: "ts-loader",
- options: {
- compilerOptions: latestBuild
- ? { noEmit: false }
- : {
- target: "es5",
- noEmit: false,
- },
+module.exports = createGalleryConfig({
+ latestBuild: true,
+});
+
+const bla = () => {
+ const oldExports = {
+ mode: isProd ? "production" : "development",
+ // Disabled in prod while we make Home Assistant able to serve the right files.
+ // Was source-map
+ devtool: isProd ? "none" : "inline-source-map",
+ entry: "./src/entrypoint.js",
+ module: {
+ rules: [
+ babelLoaderConfig({ latestBuild }),
+ {
+ test: /\.css$/,
+ use: "raw-loader",
},
- },
- ],
- },
- {
- test: /\.css$/,
- use: "raw-loader",
- },
- {
- test: /\.(html)$/,
- use: {
- loader: "html-loader",
- options: {
- exportAsEs6Default: true,
- },
+ {
+ test: /\.(html)$/,
+ use: {
+ loader: "html-loader",
+ options: {
+ exportAsEs6Default: true,
+ },
+ },
+ },
+ ],
},
- },
-];
-
-if (!latestBuild) {
- rules.push(babelLoaderConfig({ latestBuild }));
-}
-
-module.exports = {
- mode: isProd ? "production" : "development",
- // Disabled in prod while we make Home Assistant able to serve the right files.
- // Was source-map
- devtool: isProd ? "none" : "inline-source-map",
- entry: "./src/entrypoint.js",
- module: {
- rules,
- },
- optimization: webpackBase.optimization(latestBuild),
- plugins: [
- new CopyWebpackPlugin([
- "public",
- { from: "../public", to: "static" },
- { from: "../build-translations/output", to: "static/translations" },
- {
- from: "../node_modules/leaflet/dist/leaflet.css",
- to: "static/images/leaflet/",
- },
- {
- from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
- to: "static/fonts/roboto/",
- },
- {
- from: "../node_modules/leaflet/dist/images",
- to: "static/images/leaflet/",
- },
- ]),
- ].filter(Boolean),
- resolve: webpackBase.resolve,
- output: {
- filename: "[name].js",
- chunkFilename: chunkFilename,
- path: buildPath,
- publicPath,
- },
- devServer: {
- contentBase: "./public",
- },
+ optimization: webpackBase.optimization(latestBuild),
+ plugins: [
+ new CopyWebpackPlugin([
+ "public",
+ { from: "../public", to: "static" },
+ { from: "../build-translations/output", to: "static/translations" },
+ {
+ from: "../node_modules/leaflet/dist/leaflet.css",
+ to: "static/images/leaflet/",
+ },
+ {
+ from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
+ to: "static/fonts/roboto/",
+ },
+ {
+ from: "../node_modules/leaflet/dist/images",
+ to: "static/images/leaflet/",
+ },
+ ]),
+ ].filter(Boolean),
+ resolve: webpackBase.resolve,
+ output: {
+ filename: "[name].js",
+ chunkFilename: chunkFilename,
+ path: buildPath,
+ publicPath,
+ },
+ devServer: {
+ contentBase: "./public",
+ },
+ };
};
diff --git a/hassio/script/build_hassio b/hassio/script/build_hassio
index b8bd6d504e..193cbb0687 100755
--- a/hassio/script/build_hassio
+++ b/hassio/script/build_hassio
@@ -4,11 +4,6 @@
# Stop on errors
set -e
-cd "$(dirname "$0")/.."
+cd "$(dirname "$0")/../.."
-OUTPUT_DIR=build
-
-rm -rf $OUTPUT_DIR
-
-node script/gen-icons.js
-NODE_ENV=production CI=false ../node_modules/.bin/webpack -p --config webpack.config.js
+./node_modules/.bin/gulp build-hassio
diff --git a/hassio/script/develop b/hassio/script/develop
index c23fc2ea1f..0b62666b10 100755
--- a/hassio/script/develop
+++ b/hassio/script/develop
@@ -4,11 +4,6 @@
# Stop on errors
set -e
-cd "$(dirname "$0")/.."
+cd "$(dirname "$0")/../.."
-OUTPUT_DIR=build
-
-rm -rf $OUTPUT_DIR
-mkdir $OUTPUT_DIR
-node script/gen-icons.js
-../node_modules/.bin/webpack --watch --progress
+./node_modules/.bin/gulp develop-hassio
diff --git a/hassio/script/gen-icons.js b/hassio/script/gen-icons.js
deleted file mode 100755
index b355ef752b..0000000000
--- a/hassio/script/gen-icons.js
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env node
-const fs = require("fs");
-const {
- findIcons,
- generateIconset,
- genMDIIcons,
-} = require("../../build-scripts/gulp/gen-icons.js");
-
-function genHassioIcons() {
- const iconNames = findIcons("./src", "hassio");
-
- for (const item of findIcons("../src", "hassio")) {
- iconNames.add(item);
- }
-
- fs.writeFileSync("./hassio-icons.html", generateIconset("hassio", iconNames));
-}
-
-genMDIIcons();
-genHassioIcons();
diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts
index 07b532cbae..3414c5fdf7 100644
--- a/hassio/src/dashboard/hassio-update.ts
+++ b/hassio/src/dashboard/hassio-update.ts
@@ -7,6 +7,7 @@ import {
property,
customElement,
} from "lit-element";
+import "@polymer/iron-icon/iron-icon";
import { HomeAssistant } from "../../../src/types";
import {
@@ -33,12 +34,15 @@ export class HassioUpdate extends LitElement {
@property() public error?: string;
protected render(): TemplateResult | void {
- if (
- this.hassInfo.version === this.hassInfo.last_version &&
- this.supervisorInfo.version === this.supervisorInfo.last_version &&
- (!this.hassOsInfo ||
- this.hassOsInfo.version === this.hassOsInfo.version_latest)
- ) {
+ const updatesAvailable: number = [
+ this.hassInfo,
+ this.supervisorInfo,
+ this.hassOsInfo,
+ ].filter((value) => {
+ return !!value && value.version !== value.last_version;
+ }).length;
+
+ if (!updatesAvailable) {
return html``;
}
@@ -50,6 +54,11 @@ export class HassioUpdate extends LitElement {
`
: ""}
+
+ ${updatesAvailable > 1
+ ? "Updates Available 🎉"
+ : "Update Available 🎉"}
+
${this._renderUpdateCard(
"Home Assistant",
this.hassInfo.version,
@@ -57,7 +66,8 @@ export class HassioUpdate extends LitElement {
"hassio/homeassistant/update",
`https://${
this.hassInfo.last_version.includes("b") ? "rc" : "www"
- }.home-assistant.io/latest-release-notes/`
+ }.home-assistant.io/latest-release-notes/`,
+ "hassio:home-assistant"
)}
${this._renderUpdateCard(
"Hass.io Supervisor",
@@ -89,18 +99,31 @@ export class HassioUpdate extends LitElement {
curVersion: string,
lastVersion: string,
apiPath: string,
- releaseNotesUrl: string
+ releaseNotesUrl: string,
+ icon?: string
): TemplateResult {
if (lastVersion === curVersion) {
return html``;
}
return html`
-
+
- ${name} ${lastVersion} is available and you are currently running
- ${name} ${curVersion}.
+ ${icon
+ ? html`
+
+
+
+ `
+ : ""}
+
${name} ${lastVersion}
+
+ You are currently running version ${curVersion}
+
`;
@@ -140,6 +160,23 @@ export class HassioUpdate extends LitElement {
display: inline-block;
margin-bottom: 32px;
}
+ .icon {
+ --iron-icon-height: 48px;
+ --iron-icon-width: 48px;
+ float: right;
+ margin: 0 0 2px 10px;
+ }
+ .update-heading {
+ font-size: var(--paper-font-subhead_-_font-size);
+ font-weight: 500;
+ margin-bottom: 0.5em;
+ }
+ .warning {
+ color: var(--secondary-text-color);
+ }
+ .card-actions {
+ text-align: right;
+ }
.errors {
color: var(--google-red-500);
padding: 16px;
diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
old mode 100644
new mode 100755
index 3d1cc54cdd..f0ae53eac4
--- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
+++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
@@ -3,6 +3,7 @@ import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
+import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -94,13 +95,23 @@ class HassioSnapshotDialog extends PolymerElement {
.details {
color: var(--secondary-text-color);
}
- .download {
- color: var(--primary-color);
- }
.warning,
.error {
color: var(--google-red-500);
}
+ .buttons {
+ display: flex;
+ flex-direction: column;
+ }
+ .buttons li {
+ list-style-type: none;
+ }
+ .buttons .icon {
+ margin-right: 16px;
+ }
+ .no-margin-top {
+ margin-top: 0;
+ }
Add-ons:
-
+
[[item.name]] ([[item.version]])
@@ -151,28 +162,35 @@ class HassioSnapshotDialog extends PolymerElement {
Error: [[error]]
-
+
+
+
+ Delete Snapshot
+
+
+
`;
}
diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts
index 29966015f5..33bf94e573 100644
--- a/hassio/src/hassio-main.ts
+++ b/hassio/src/hassio-main.ts
@@ -3,7 +3,7 @@ import { PolymerElement } from "@polymer/polymer";
import "@polymer/paper-icon-button";
import "../../src/resources/ha-style";
-import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
+import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import {
HassRouterPage,
diff --git a/hassio/webpack.config.js b/hassio/webpack.config.js
index cdbd23911e..02e44c0d74 100644
--- a/hassio/webpack.config.js
+++ b/hassio/webpack.config.js
@@ -1,85 +1,11 @@
-const webpack = require("webpack");
-const CompressionPlugin = require("compression-webpack-plugin");
-const zopfli = require("@gfx/zopfli");
+const { createHassioConfig } = require("../build-scripts/webpack.js");
+const { isProdBuild } = require("../build-scripts/env.js");
-const config = require("./config.js");
-const webpackBase = require("../build-scripts/webpack.js");
-const { babelLoaderConfig } = require("../build-scripts/babel.js");
+// File just used for stats builds
-const isProdBuild = process.env.NODE_ENV === "production";
-const isCI = process.env.CI === "true";
-const chunkFilename = isProdBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const latestBuild = false;
-const rules = [
- {
- exclude: [config.nodeDir],
- test: /\.ts$/,
- use: [
- {
- loader: "ts-loader",
- options: {
- compilerOptions: latestBuild
- ? { noEmit: false }
- : {
- target: "es5",
- noEmit: false,
- },
- },
- },
- ],
- },
- {
- test: /\.(html)$/,
- use: {
- loader: "html-loader",
- options: {
- exportAsEs6Default: true,
- },
- },
- },
-];
-
-if (!latestBuild) {
- rules.push(babelLoaderConfig({ latestBuild }));
-}
-
-module.exports = {
- mode: isProdBuild ? "production" : "development",
- devtool: isProdBuild ? "source-map" : "inline-source-map",
- entry: {
- entrypoint: "./src/entrypoint.js",
- },
- module: {
- rules,
- },
- optimization: webpackBase.optimization(latestBuild),
- plugins: [
- new webpack.DefinePlugin({
- __DEV__: JSON.stringify(!isProdBuild),
- __DEMO__: false,
- __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
- "process.env.NODE_ENV": JSON.stringify(
- isProdBuild ? "production" : "development"
- ),
- }),
- isProdBuild &&
- !isCI &&
- new CompressionPlugin({
- cache: true,
- exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
- algorithm(input, compressionOptions, callback) {
- return zopfli.gzip(input, compressionOptions, callback);
- },
- }),
- ].filter(Boolean),
- resolve: {
- extensions: [".ts", ".js", ".json"],
- },
- output: {
- filename: "[name].js",
- chunkFilename,
- path: config.buildDir,
- publicPath: `${config.publicPath}/`,
- },
-};
+module.exports = createHassioConfig({
+ isProdBuild,
+ latestBuild,
+});
diff --git a/package.json b/package.json
index 00ead71952..92e12babb0 100644
--- a/package.json
+++ b/package.json
@@ -84,7 +84,6 @@
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^4.4.0",
"intl-messageformat": "^2.2.0",
- "jquery": "^3.4.0",
"js-yaml": "^3.13.1",
"leaflet": "^1.4.0",
"lit-element": "^2.2.1",
@@ -98,7 +97,6 @@
"react-big-calendar": "^0.20.4",
"regenerator-runtime": "^0.13.2",
"roboto-fontface": "^0.10.0",
- "round-slider": "^1.3.3",
"superstruct": "^0.6.1",
"tslib": "^1.10.0",
"unfetch": "^4.1.0",
@@ -112,19 +110,20 @@
"@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-proposal-object-rest-spread": "^7.4.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
- "@babel/preset-env": "^7.4.0",
- "@gfx/zopfli": "^1.0.11",
+ "@babel/plugin-transform-react-jsx": "^7.3.0",
+ "@babel/preset-env": "^7.4.2",
+ "@babel/preset-typescript": "^7.4.0",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1",
"@types/codemirror": "^0.0.78",
"@types/hls.js": "^0.12.3",
+ "@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6",
"babel-loader": "^8.0.5",
"chai": "^4.2.0",
- "compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^5.0.2",
"del": "^4.0.0",
"eslint": "^6.3.0",
@@ -160,7 +159,6 @@
"require-dir": "^1.2.0",
"sinon": "^7.3.1",
"terser-webpack-plugin": "^1.2.3",
- "ts-loader": "^6.1.1",
"ts-mocha": "^6.0.0",
"tslint": "^5.14.0",
"tslint-config-prettier": "^1.18.0",
diff --git a/setup.py b/setup.py
index a71239fc8f..90f675a805 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
- version="20191014.0",
+ version="20191023.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts
index 4799fbc155..5075c3963f 100644
--- a/src/auth/ha-auth-flow.ts
+++ b/src/auth/ha-auth-flow.ts
@@ -7,7 +7,7 @@ import {
css,
} from "lit-element";
import "@material/mwc-button";
-import "../components/ha-form";
+import "../components/ha-form/ha-form";
import "../components/ha-markdown";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { AuthProvider } from "../data/auth";
diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts
index 0a7ce2a70a..d5ba17aa2d 100644
--- a/src/auth/ha-authorize.ts
+++ b/src/auth/ha-authorize.ts
@@ -2,10 +2,10 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import {
LitElement,
html,
- PropertyDeclarations,
PropertyValues,
CSSResult,
css,
+ property,
} from "lit-element";
import "./ha-auth-flow";
import { AuthProvider, fetchAuthProviders } from "../data/auth";
@@ -20,11 +20,11 @@ interface QueryParams {
}
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
- public clientId?: string;
- public redirectUri?: string;
- public oauth2State?: string;
- private _authProvider?: AuthProvider;
- private _authProviders?: AuthProvider[];
+ @property() public clientId?: string;
+ @property() public redirectUri?: string;
+ @property() public oauth2State?: string;
+ @property() private _authProvider?: AuthProvider;
+ @property() private _authProviders?: AuthProvider[];
constructor() {
super();
@@ -48,16 +48,6 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
}
}
- static get properties(): PropertyDeclarations {
- return {
- _authProvider: {},
- _authProviders: {},
- clientId: {},
- redirectUri: {},
- oauth2State: {},
- };
- }
-
protected render() {
if (!this._authProviders) {
return html`
diff --git a/src/cards/ha-badges-card.js b/src/cards/ha-badges-card.js
deleted file mode 100644
index 6b62930888..0000000000
--- a/src/cards/ha-badges-card.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import "../components/entity/ha-state-label-badge";
-
-class HaBadgesCard extends PolymerElement {
- static get template() {
- return html`
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- hass: Object,
- states: Array,
- };
- }
-}
-customElements.define("ha-badges-card", HaBadgesCard);
diff --git a/src/cards/ha-badges-card.ts b/src/cards/ha-badges-card.ts
new file mode 100644
index 0000000000..7e12681456
--- /dev/null
+++ b/src/cards/ha-badges-card.ts
@@ -0,0 +1,45 @@
+import { TemplateResult, html } from "lit-html";
+import { customElement, LitElement, property } from "lit-element";
+import { HassEntity } from "home-assistant-js-websocket";
+
+import "../components/entity/ha-state-label-badge";
+
+import { HomeAssistant } from "../types";
+import { fireEvent } from "../common/dom/fire_event";
+
+@customElement("ha-badges-card")
+class HaBadgesCard extends LitElement {
+ @property() public hass?: HomeAssistant;
+ @property() public states?: HassEntity[];
+
+ protected render(): TemplateResult | void {
+ if (!this.hass || !this.states) {
+ return html``;
+ }
+
+ return html`
+ ${this.states.map(
+ (state) => html`
+
+ `
+ )}
+ `;
+ }
+
+ private _handleClick(ev: Event) {
+ const entityId = ((ev.target as any).state as HassEntity).entity_id;
+ fireEvent(this, "hass-more-info", {
+ entityId,
+ });
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-badges-card": HaBadgesCard;
+ }
+}
diff --git a/src/common/const.ts b/src/common/const.ts
index c1f27c4e9d..3abe76e91b 100644
--- a/src/common/const.ts
+++ b/src/common/const.ts
@@ -35,6 +35,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"camera",
"climate",
"configurator",
+ "counter",
"cover",
"fan",
"group",
diff --git a/src/common/dom/apply_themes_on_element.ts b/src/common/dom/apply_themes_on_element.ts
index 22b13893c0..5d5222ca7c 100644
--- a/src/common/dom/apply_themes_on_element.ts
+++ b/src/common/dom/apply_themes_on_element.ts
@@ -21,12 +21,12 @@ const hexToRgb = (hex: string): string | null => {
* localTheme: selected theme.
* updateMeta: boolean if we should update the theme-color meta element.
*/
-export default function applyThemesOnElement(
+export const applyThemesOnElement = (
element,
themes,
localTheme,
updateMeta = false
-) {
+) => {
if (!element._themes) {
element._themes = {};
}
@@ -76,4 +76,4 @@ export default function applyThemesOnElement(
styles["--primary-color"] || meta.getAttribute("default-content");
meta.setAttribute("content", themeColor);
}
-}
+};
diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts
index 9942a6d963..0bb2535a77 100644
--- a/src/common/entity/domain_icon.ts
+++ b/src/common/entity/domain_icon.ts
@@ -14,6 +14,7 @@ const fixedIcons = {
climate: "hass:thermostat",
configurator: "hass:settings",
conversation: "hass:text-to-speech",
+ counter: "hass:counter",
device_tracker: "hass:account",
fan: "hass:fan",
google_assistant: "hass:google-assistant",
diff --git a/src/components/buttons/ha-call-service-button.js b/src/components/buttons/ha-call-service-button.js
index 909919c9a1..6c2d6ad9f3 100644
--- a/src/components/buttons/ha-call-service-button.js
+++ b/src/components/buttons/ha-call-service-button.js
@@ -3,6 +3,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-progress-button";
import { EventsMixin } from "../../mixins/events-mixin";
+import { showConfirmationDialog } from "../../dialogs/confirmation/show-dialog-confirmation";
/*
* @appliesMixin EventsMixin
@@ -49,10 +50,7 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
};
}
- buttonTapped() {
- if (this.confirmation && !window.confirm(this.confirmation)) {
- return;
- }
+ callService() {
this.progress = true;
var el = this;
var eventData = {
@@ -79,6 +77,17 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
el.fire("hass-service-called", eventData);
});
}
+
+ buttonTapped() {
+ if (this.confirmation) {
+ showConfirmationDialog(this, {
+ text: this.confirmation,
+ confirm: () => this.callService(),
+ });
+ } else {
+ this.callService();
+ }
+ }
}
customElements.define("ha-call-service-button", HaCallServiceButton);
diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts
index d009bd8c77..0fa519b4d9 100644
--- a/src/components/data-table/ha-data-table.ts
+++ b/src/components/data-table/ha-data-table.ts
@@ -427,7 +427,7 @@ export class HaDataTable extends BaseElement {
}
.mdc-data-table {
- background-color: var(--card-background-color);
+ background-color: var(--data-table-background-color);
border-radius: 4px;
border-width: 1px;
border-style: solid;
diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts
index edea4378c5..80da813f6b 100644
--- a/src/components/device/ha-device-picker.ts
+++ b/src/components/device/ha-device-picker.ts
@@ -1,7 +1,7 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
-import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
+import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "@polymer/paper-listbox/paper-listbox";
import memoizeOne from "memoize-one";
import {
@@ -23,52 +23,165 @@ import {
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
+import { PolymerChangedEvent } from "../../polymer-types";
+import {
+ AreaRegistryEntry,
+ subscribeAreaRegistry,
+} from "../../data/area_registry";
+import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
+import {
+ EntityRegistryEntry,
+ subscribeEntityRegistry,
+} from "../../data/entity_registry";
+import { computeStateName } from "../../common/entity/compute_state_name";
+
+interface Device {
+ name: string;
+ area: string;
+ id: string;
+}
+
+const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
+ if (!root.firstElementChild) {
+ root.innerHTML = `
+
+
+
+ [[item.name]]
+ [[item.area]]
+
+
+ `;
+ }
+
+ root.querySelector(".name")!.textContent = model.item.name!;
+ root.querySelector("[secondary]")!.textContent = model.item.area!;
+};
@customElement("ha-device-picker")
class HaDevicePicker extends SubscribeMixin(LitElement) {
- @property() public hass?: HomeAssistant;
+ @property() public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public devices?: DeviceRegistryEntry[];
+ @property() public areas?: AreaRegistryEntry[];
+ @property() public entities?: EntityRegistryEntry[];
+ @property({ type: Boolean }) private _opened?: boolean;
- private _sortedDevices = memoizeOne((devices?: DeviceRegistryEntry[]) => {
- if (!devices || devices.length === 1) {
- return devices || [];
+ private _getDevices = memoizeOne(
+ (
+ devices: DeviceRegistryEntry[],
+ areas: AreaRegistryEntry[],
+ entities: EntityRegistryEntry[]
+ ): Device[] => {
+ if (!devices.length) {
+ return [];
+ }
+
+ const deviceEntityLookup: DeviceEntityLookup = {};
+ for (const entity of entities) {
+ if (!entity.device_id) {
+ continue;
+ }
+ if (!(entity.device_id in deviceEntityLookup)) {
+ deviceEntityLookup[entity.device_id] = [];
+ }
+ deviceEntityLookup[entity.device_id].push(entity);
+ }
+
+ const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
+ for (const area of areas) {
+ areaLookup[area.area_id] = area;
+ }
+
+ const outputDevices = devices.map((device) => {
+ return {
+ id: device.id,
+ name:
+ device.name_by_user ||
+ device.name ||
+ this._fallbackDeviceName(device.id, deviceEntityLookup) ||
+ "No name",
+ area: device.area_id ? areaLookup[device.area_id].name : "No area",
+ };
+ });
+ if (outputDevices.length === 1) {
+ return outputDevices;
+ }
+ return outputDevices.sort((a, b) => compare(a.name || "", b.name || ""));
}
- const sorted = [...devices];
- sorted.sort((a, b) => compare(a.name || "", b.name || ""));
- return sorted;
- });
+ );
public hassSubscribe(): UnsubscribeFunc[] {
return [
- subscribeDeviceRegistry(this.hass!.connection!, (devices) => {
+ subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this.devices = devices;
}),
+ subscribeAreaRegistry(this.hass.connection!, (areas) => {
+ this.areas = areas;
+ }),
+ subscribeEntityRegistry(this.hass.connection!, (entities) => {
+ this.entities = entities;
+ }),
];
}
protected render(): TemplateResult | void {
+ if (!this.devices || !this.areas || !this.entities) {
+ return;
+ }
+ const devices = this._getDevices(this.devices, this.areas, this.entities);
return html`
-
-
+
-
- No device
-
- ${this._sortedDevices(this.devices).map(
- (device) => html`
-
- ${device.name_by_user || device.name}
-
- `
- )}
-
-
+ ${this.value
+ ? html`
+
+ Clear
+
+ `
+ : ""}
+ ${devices.length > 0
+ ? html`
+
+ Toggle
+
+ `
+ : ""}
+
+
`;
}
@@ -76,8 +189,12 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
return this.value || "";
}
- private _deviceChanged(ev) {
- const newValue = ev.detail.item.dataset.deviceId;
+ private _openedChanged(ev: PolymerChangedEvent) {
+ this._opened = ev.detail.value;
+ }
+
+ private _deviceChanged(ev: PolymerChangedEvent) {
+ const newValue = ev.detail.value;
if (newValue !== this._value) {
this.value = newValue;
@@ -88,16 +205,30 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
}
}
+ private _fallbackDeviceName(
+ deviceId: string,
+ deviceEntityLookup: DeviceEntityLookup
+ ): string | undefined {
+ for (const entity of deviceEntityLookup[deviceId] || []) {
+ const stateObj = this.hass.states[entity.entity_id];
+ if (stateObj) {
+ return computeStateName(stateObj);
+ }
+ }
+
+ return undefined;
+ }
+
static get styles(): CSSResult {
return css`
- paper-dropdown-menu-light {
- width: 100%;
+ paper-input > paper-icon-button {
+ width: 24px;
+ height: 24px;
+ padding: 2px;
+ color: var(--secondary-text-color);
}
- paper-listbox {
- min-width: 200px;
- }
- paper-item {
- cursor: pointer;
+ [hidden] {
+ display: none;
}
`;
}
diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts
index 2d82e82095..33f7e1e5c1 100644
--- a/src/components/entity/ha-state-label-badge.ts
+++ b/src/components/entity/ha-state-label-badge.ts
@@ -11,7 +11,6 @@ import {
import { HassEntity } from "home-assistant-js-websocket";
import { classMap } from "lit-html/directives/class-map";
-import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
@@ -90,16 +89,6 @@ export class HaStateLabelBadge extends LitElement {
`;
}
- protected firstUpdated(changedProperties: PropertyValues): void {
- super.firstUpdated(changedProperties);
- this.addEventListener("click", (ev) => {
- ev.stopPropagation();
- if (this.state) {
- fireEvent(this, "hass-more-info", { entityId: this.state.entity_id });
- }
- });
- }
-
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
diff --git a/src/components/ha-checkbox.ts b/src/components/ha-checkbox.ts
index c4ef540352..232d663b13 100644
--- a/src/components/ha-checkbox.ts
+++ b/src/components/ha-checkbox.ts
@@ -1,8 +1,9 @@
-import { Constructor, customElement, CSSResult, css } from "lit-element";
+import { customElement, CSSResult, css } from "lit-element";
import "@material/mwc-checkbox";
// tslint:disable-next-line
import { Checkbox } from "@material/mwc-checkbox";
import { style } from "@material/mwc-checkbox/mwc-checkbox-css";
+import { Constructor } from "../types";
// tslint:disable-next-line
const MwcCheckbox = customElements.get("mwc-checkbox") as Constructor;
diff --git a/src/components/ha-fab.ts b/src/components/ha-fab.ts
index f7ffee55fe..2003ac0c41 100644
--- a/src/components/ha-fab.ts
+++ b/src/components/ha-fab.ts
@@ -1,12 +1,8 @@
-import {
- classMap,
- html,
- customElement,
- Constructor,
-} from "@material/mwc-base/base-element";
+import { classMap, html, customElement } from "@material/mwc-base/base-element";
import { ripple } from "@material/mwc-ripple/ripple-directive.js";
import "@material/mwc-fab";
+import { Constructor } from "../types";
// tslint:disable-next-line
import { Fab } from "@material/mwc-fab";
// tslint:disable-next-line
diff --git a/src/components/ha-form.js b/src/components/ha-form.js
deleted file mode 100644
index f6dfe613ee..0000000000
--- a/src/components/ha-form.js
+++ /dev/null
@@ -1,265 +0,0 @@
-import "@polymer/paper-checkbox/paper-checkbox";
-import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
-import "@polymer/paper-icon-button/paper-icon-button";
-import "@polymer/paper-input/paper-input";
-import "@polymer/paper-item/paper-item";
-import "@polymer/paper-listbox/paper-listbox";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import "./ha-paper-slider";
-import { EventsMixin } from "../mixins/events-mixin";
-
-/*
- * @appliesMixin EventsMixin
- */
-class HaForm extends EventsMixin(PolymerElement) {
- static get template() {
- return html`
-
-
-
- [[computeError(error.base, schema)]]
-
-
-
-
-
-
-
-
- [[computeError(error, schema)]]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- [[computeLabel(schema)]]
-
-
-
-
-
-
-
-
-
-
-
- [[computeSuffix(schema)]]
-
-
-
-
-
-
[[computeLabel(schema)]]
-
-
-
-
-
-
-
- [[_optionLabel(item)]]
-
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- data: {
- type: Object,
- notify: true,
- },
- schema: Object,
- error: Object,
-
- // A function that computes the label to be displayed for a given
- // schema object.
- computeLabel: {
- type: Function,
- value: () => (schema) => schema && schema.name,
- },
-
- // A function that computes the suffix to be displayed for a given
- // schema object.
- computeSuffix: {
- type: Function,
- value: () => (schema) =>
- schema &&
- schema.description &&
- schema.description.unit_of_measurement,
- },
-
- // A function that computes an error message to be displayed for a
- // given error ID, and relevant schema object
- computeError: {
- type: Function,
- value: () => (error, schema) => error, // eslint-disable-line no-unused-vars
- },
- };
- }
-
- focus() {
- const input = this.shadowRoot.querySelector(
- "ha-form, paper-input, ha-paper-slider, paper-checkbox, paper-dropdown-menu"
- );
-
- if (!input) {
- return;
- }
-
- input.focus();
- }
-
- _isArray(val) {
- return Array.isArray(val);
- }
-
- _isRange(schema) {
- return "valueMin" in schema && "valueMax" in schema;
- }
-
- _equals(a, b) {
- return a === b;
- }
-
- _includes(a, b) {
- return a.indexOf(b) >= 0;
- }
-
- _getValue(obj, item) {
- if (obj) {
- return obj[item.name];
- }
- return null;
- }
-
- _valueChanged(ev) {
- let value = ev.detail.value;
- if (ev.model.item.type === "integer") {
- value = Number(ev.detail.value);
- }
- this.set(["data", ev.model.item.name], value);
- }
-
- _passwordFieldType(unmaskedPassword) {
- return unmaskedPassword ? "text" : "password";
- }
-
- _passwordFieldIcon(unmaskedPassword) {
- return unmaskedPassword ? "hass:eye-off" : "hass:eye";
- }
-
- _optionValue(item) {
- return Array.isArray(item) ? item[0] : item;
- }
-
- _optionLabel(item) {
- return Array.isArray(item) ? item[1] : item;
- }
-}
-
-customElements.define("ha-form", HaForm);
diff --git a/src/components/ha-form/ha-form-boolean.ts b/src/components/ha-form/ha-form-boolean.ts
new file mode 100644
index 0000000000..5921cd3e43
--- /dev/null
+++ b/src/components/ha-form/ha-form-boolean.ts
@@ -0,0 +1,70 @@
+import {
+ customElement,
+ LitElement,
+ html,
+ property,
+ TemplateResult,
+ CSSResult,
+ css,
+ query,
+} from "lit-element";
+import {
+ HaFormElement,
+ HaFormBooleanData,
+ HaFormBooleanSchema,
+} from "./ha-form";
+import { fireEvent } from "../../common/dom/fire_event";
+
+import "@polymer/paper-checkbox/paper-checkbox";
+// Not duplicate, is for typing
+// tslint:disable-next-line
+import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
+
+@customElement("ha-form-boolean")
+export class HaFormBoolean extends LitElement implements HaFormElement {
+ @property() public schema!: HaFormBooleanSchema;
+ @property() public data!: HaFormBooleanData;
+ @property() public label!: string;
+ @property() public suffix!: string;
+ @query("paper-checkbox") private _input?: HTMLElement;
+
+ public focus() {
+ if (this._input) {
+ this._input.focus();
+ }
+ }
+
+ protected render(): TemplateResult {
+ return html`
+
+ ${this.label}
+
+ `;
+ }
+
+ private _valueChanged(ev: Event) {
+ fireEvent(
+ this,
+ "value-changed",
+ {
+ value: (ev.target as PaperCheckboxElement).checked,
+ },
+ { bubbles: false }
+ );
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ paper-checkbox {
+ display: inline-block;
+ padding: 22px 0;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-form-boolean": HaFormBoolean;
+ }
+}
diff --git a/src/components/ha-form/ha-form-float.ts b/src/components/ha-form/ha-form-float.ts
new file mode 100644
index 0000000000..d2d41990aa
--- /dev/null
+++ b/src/components/ha-form/ha-form-float.ts
@@ -0,0 +1,69 @@
+import {
+ customElement,
+ LitElement,
+ html,
+ property,
+ TemplateResult,
+ query,
+} from "lit-element";
+import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./ha-form";
+import { fireEvent } from "../../common/dom/fire_event";
+
+import "@polymer/paper-input/paper-input";
+// Not duplicate, is for typing
+// tslint:disable-next-line
+import { PaperInputElement } from "@polymer/paper-input/paper-input";
+
+@customElement("ha-form-float")
+export class HaFormFloat extends LitElement implements HaFormElement {
+ @property() public schema!: HaFormFloatSchema;
+ @property() public data!: HaFormFloatData;
+ @property() public label!: string;
+ @property() public suffix!: string;
+ @query("paper-input") private _input?: HTMLElement;
+
+ public focus() {
+ if (this._input) {
+ this._input.focus();
+ }
+ }
+
+ protected render(): TemplateResult {
+ return html`
+
+ ${this.suffix}
+
+ `;
+ }
+
+ private get _value() {
+ return this.data || 0;
+ }
+
+ private _valueChanged(ev: Event) {
+ const value = Number((ev.target as PaperInputElement).value);
+ if (this._value === value) {
+ return;
+ }
+ fireEvent(
+ this,
+ "value-changed",
+ {
+ value,
+ },
+ { bubbles: false }
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-form-float": HaFormFloat;
+ }
+}
diff --git a/src/components/ha-form/ha-form-integer.ts b/src/components/ha-form/ha-form-integer.ts
new file mode 100644
index 0000000000..996110852b
--- /dev/null
+++ b/src/components/ha-form/ha-form-integer.ts
@@ -0,0 +1,89 @@
+import {
+ customElement,
+ LitElement,
+ html,
+ property,
+ TemplateResult,
+ query,
+} from "lit-element";
+import {
+ HaFormElement,
+ HaFormIntegerData,
+ HaFormIntegerSchema,
+} from "./ha-form";
+import { fireEvent } from "../../common/dom/fire_event";
+
+import "../ha-paper-slider";
+import "@polymer/paper-input/paper-input";
+// Not duplicate, is for typing
+// tslint:disable-next-line
+import { PaperInputElement } from "@polymer/paper-input/paper-input";
+import { PaperSliderElement } from "@polymer/paper-slider/paper-slider";
+
+@customElement("ha-form-integer")
+export class HaFormInteger extends LitElement implements HaFormElement {
+ @property() public schema!: HaFormIntegerSchema;
+ @property() public data!: HaFormIntegerData;
+ @property() public label!: string;
+ @property() public suffix!: string;
+ @query("paper-input ha-paper-slider") private _input?: HTMLElement;
+
+ public focus() {
+ if (this._input) {
+ this._input.focus();
+ }
+ }
+
+ protected render(): TemplateResult {
+ return "valueMin" in this.schema && "valueMax" in this.schema
+ ? html`
+
+ ${this.label}
+
+
+ `
+ : html`
+
+ `;
+ }
+
+ private get _value() {
+ return this.data || 0;
+ }
+
+ private _valueChanged(ev: Event) {
+ const value = Number(
+ (ev.target as PaperInputElement | PaperSliderElement).value
+ );
+ if (this._value === value) {
+ return;
+ }
+ fireEvent(
+ this,
+ "value-changed",
+ {
+ value,
+ },
+ { bubbles: false }
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-form-integer": HaFormInteger;
+ }
+}
diff --git a/src/components/ha-form/ha-form-positive_time_period_dict.ts b/src/components/ha-form/ha-form-positive_time_period_dict.ts
new file mode 100644
index 0000000000..5794f34d2e
--- /dev/null
+++ b/src/components/ha-form/ha-form-positive_time_period_dict.ts
@@ -0,0 +1,119 @@
+import {
+ customElement,
+ LitElement,
+ html,
+ property,
+ TemplateResult,
+ query,
+} from "lit-element";
+import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./ha-form";
+import { fireEvent } from "../../common/dom/fire_event";
+
+@customElement("ha-form-positive_time_period_dict")
+export class HaFormTimePeriod extends LitElement implements HaFormElement {
+ @property() public schema!: HaFormTimeSchema;
+ @property() public data!: HaFormTimeData;
+ @property() public label!: string;
+ @property() public suffix!: string;
+ @query("paper-time-input") private _input?: HTMLElement;
+
+ public focus() {
+ if (this._input) {
+ this._input.focus();
+ }
+ }
+
+ protected render(): TemplateResult | void {
+ return html`
+
+ `;
+ }
+
+ private get _hours() {
+ return this.data && this.data.hours ? Number(this.data.hours) : 0;
+ }
+
+ private get _minutes() {
+ return this.data && this.data.minutes ? Number(this.data.minutes) : 0;
+ }
+
+ private get _seconds() {
+ return this.data && this.data.seconds ? Number(this.data.seconds) : 0;
+ }
+
+ private _parseDuration(value) {
+ return value.toString().padStart(2, "0");
+ }
+
+ private _hourChanged(ev) {
+ this._durationChanged(ev, "hours");
+ }
+
+ private _minChanged(ev) {
+ this._durationChanged(ev, "minutes");
+ }
+
+ private _secChanged(ev) {
+ this._durationChanged(ev, "seconds");
+ }
+
+ private _durationChanged(ev, unit) {
+ let value = Number(ev.detail.value);
+
+ if (value === this[`_${unit}`]) {
+ return;
+ }
+
+ let hours = this._hours;
+ let minutes = this._minutes;
+
+ if (unit === "seconds" && value > 59) {
+ minutes = minutes + Math.floor(value / 60);
+ value %= 60;
+ }
+
+ if (unit === "minutes" && value > 59) {
+ hours = hours + Math.floor(value / 60);
+ value %= 60;
+ }
+
+ fireEvent(
+ this,
+ "value-changed",
+ {
+ value: {
+ hours,
+ minutes,
+ seconds: this._seconds,
+ ...{ [unit]: value },
+ },
+ },
+ { bubbles: false }
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-form-positive_time_period_dict": HaFormTimePeriod;
+ }
+}
diff --git a/src/components/ha-form/ha-form-select.ts b/src/components/ha-form/ha-form-select.ts
new file mode 100644
index 0000000000..25a13abd23
--- /dev/null
+++ b/src/components/ha-form/ha-form-select.ts
@@ -0,0 +1,78 @@
+import {
+ customElement,
+ LitElement,
+ html,
+ property,
+ TemplateResult,
+ query,
+} from "lit-element";
+import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
+import { fireEvent } from "../../common/dom/fire_event";
+
+import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
+import "@polymer/paper-listbox/paper-listbox";
+import "@polymer/paper-item/paper-item";
+
+@customElement("ha-form-select")
+export class HaFormSelect extends LitElement implements HaFormElement {
+ @property() public schema!: HaFormSelectSchema;
+ @property() public data!: HaFormSelectData;
+ @property() public label!: string;
+ @property() public suffix!: string;
+ @query("paper-dropdown-menu") private _input?: HTMLElement;
+
+ public focus() {
+ if (this._input) {
+ this._input.focus();
+ }
+ }
+
+ protected render(): TemplateResult {
+ return html`
+
+
+ ${this.schema.options!.map(
+ (item) => html`
+
+ ${this._optionLabel(item)}
+
+ `
+ )}
+
+
+ `;
+ }
+
+ private _optionValue(item) {
+ return Array.isArray(item) ? item[0] : item;
+ }
+
+ private _optionLabel(item) {
+ return Array.isArray(item) ? item[1] : item;
+ }
+
+ private _valueChanged(ev: CustomEvent) {
+ if (!ev.detail.value) {
+ return;
+ }
+ fireEvent(
+ this,
+ "value-changed",
+ {
+ value: ev.detail.value.itemValue,
+ },
+ { bubbles: false }
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-form-select": HaFormSelect;
+ }
+}
diff --git a/src/components/ha-form/ha-form-string.ts b/src/components/ha-form/ha-form-string.ts
new file mode 100644
index 0000000000..82a3db8131
--- /dev/null
+++ b/src/components/ha-form/ha-form-string.ts
@@ -0,0 +1,93 @@
+import {
+ customElement,
+ LitElement,
+ html,
+ property,
+ TemplateResult,
+ query,
+} from "lit-element";
+
+import { HaFormElement, HaFormStringData, HaFormStringSchema } from "./ha-form";
+import { fireEvent } from "../../common/dom/fire_event";
+
+import "@polymer/paper-input/paper-input";
+import "@polymer/paper-icon-button/paper-icon-button";
+// Not duplicate, is for typing
+// tslint:disable-next-line
+import { PaperInputElement } from "@polymer/paper-input/paper-input";
+
+@customElement("ha-form-string")
+export class HaFormString extends LitElement implements HaFormElement {
+ @property() public schema!: HaFormStringSchema;
+ @property() public data!: HaFormStringData;
+ @property() public label!: string;
+ @property() public suffix!: string;
+ @property() private _unmaskedPassword = false;
+ @query("paper-input") private _input?: HTMLElement;
+
+ public focus() {
+ if (this._input) {
+ this._input.focus();
+ }
+ }
+
+ protected render(): TemplateResult {
+ return this.schema.name.includes("password")
+ ? html`
+
+
+
+
+ `
+ : html`
+
+ `;
+ }
+
+ private _toggleUnmaskedPassword(ev: Event) {
+ this._unmaskedPassword = (ev.target as any).active;
+ }
+
+ private _valueChanged(ev: Event) {
+ const value = (ev.target as PaperInputElement).value;
+ if (this.data === value) {
+ return;
+ }
+ fireEvent(
+ this,
+ "value-changed",
+ {
+ value,
+ },
+ { bubbles: false }
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-form-string": HaFormString;
+ }
+}
diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts
new file mode 100644
index 0000000000..48973c63b9
--- /dev/null
+++ b/src/components/ha-form/ha-form.ts
@@ -0,0 +1,237 @@
+import {
+ customElement,
+ LitElement,
+ html,
+ property,
+ query,
+ CSSResult,
+ css,
+ PropertyValues,
+} from "lit-element";
+
+import "./ha-form-string";
+import "./ha-form-integer";
+import "./ha-form-float";
+import "./ha-form-boolean";
+import "./ha-form-select";
+import "./ha-form-positive_time_period_dict";
+import { fireEvent } from "../../common/dom/fire_event";
+
+export type HaFormSchema =
+ | HaFormStringSchema
+ | HaFormIntegerSchema
+ | HaFormFloatSchema
+ | HaFormBooleanSchema
+ | HaFormSelectSchema
+ | HaFormTimeSchema;
+
+export interface HaFormBaseSchema {
+ name: string;
+ default?: HaFormData;
+ required?: boolean;
+ optional?: boolean;
+ description?: { suffix?: string };
+}
+
+export interface HaFormIntegerSchema extends HaFormBaseSchema {
+ type: "integer";
+ default?: HaFormIntegerData;
+ valueMin?: number;
+ valueMax?: number;
+}
+
+export interface HaFormSelectSchema extends HaFormBaseSchema {
+ type: "select";
+ options?: string[];
+}
+
+export interface HaFormFloatSchema extends HaFormBaseSchema {
+ type: "float";
+}
+
+export interface HaFormStringSchema extends HaFormBaseSchema {
+ type: "string";
+}
+
+export interface HaFormBooleanSchema extends HaFormBaseSchema {
+ type: "boolean";
+}
+
+export interface HaFormTimeSchema extends HaFormBaseSchema {
+ type: "time";
+}
+
+export interface HaFormDataContainer {
+ [key: string]: HaFormData;
+}
+
+export type HaFormData =
+ | HaFormStringData
+ | HaFormIntegerData
+ | HaFormFloatData
+ | HaFormBooleanData
+ | HaFormSelectData
+ | HaFormTimeData;
+
+export type HaFormStringData = string;
+export type HaFormIntegerData = number;
+export type HaFormFloatData = number;
+export type HaFormBooleanData = boolean;
+export type HaFormSelectData = string;
+export interface HaFormTimeData {
+ hours?: number;
+ minutes?: number;
+ seconds?: number;
+}
+
+export interface HaFormElement extends LitElement {
+ schema: HaFormSchema;
+ data: HaFormDataContainer | HaFormData;
+ label?: string;
+ suffix?: string;
+}
+
+@customElement("ha-form")
+export class HaForm extends LitElement implements HaFormElement {
+ @property() public data!: HaFormDataContainer | HaFormData;
+ @property() public schema!: HaFormSchema;
+ @property() public error;
+ @property() public computeError?: (schema: HaFormSchema, error) => string;
+ @property() public computeLabel?: (schema: HaFormSchema) => string;
+ @property() public computeSuffix?: (schema: HaFormSchema) => string;
+ @query("ha-form") private _childForm?: HaForm;
+ @query("#element") private _elementContainer?: HTMLDivElement;
+
+ public focus() {
+ const input = this._childForm
+ ? this._childForm
+ : this._elementContainer
+ ? this._elementContainer.lastChild
+ : undefined;
+
+ if (!input) {
+ return;
+ }
+
+ (input as HTMLElement).focus();
+ }
+
+ protected render() {
+ if (Array.isArray(this.schema)) {
+ return html`
+ ${this.error && this.error.base
+ ? html`
+
+ ${this._computeError(this.error.base, this.schema)}
+
+ `
+ : ""}
+ ${this.schema.map(
+ (item) => html`
+
+ `
+ )}
+ `;
+ }
+
+ return html`
+ ${this.error
+ ? html`
+
+ ${this._computeError(this.error, this.schema)}
+
+ `
+ : ""}
+
+ `;
+ }
+
+ protected updated(changedProperties: PropertyValues) {
+ const schemaChanged = changedProperties.has("schema");
+ const oldSchema = schemaChanged
+ ? changedProperties.get("schema")
+ : undefined;
+ if (
+ !Array.isArray(this.schema) &&
+ schemaChanged &&
+ (!oldSchema || (oldSchema as HaFormSchema).type !== this.schema.type)
+ ) {
+ const element = document.createElement(
+ `ha-form-${this.schema.type}`
+ ) as HaFormElement;
+ element.schema = this.schema;
+ element.data = this.data;
+ element.label = this._computeLabel(this.schema);
+ element.suffix = this._computeSuffix(this.schema);
+ if (this._elementContainer!.lastChild) {
+ this._elementContainer!.removeChild(this._elementContainer!.lastChild);
+ }
+ this._elementContainer!.append(element);
+ } else if (this._elementContainer && this._elementContainer.lastChild) {
+ const element = this._elementContainer!.lastChild as HaFormElement;
+ element.schema = this.schema;
+ element.data = this.data;
+ element.label = this._computeLabel(this.schema);
+ element.suffix = this._computeSuffix(this.schema);
+ }
+ }
+
+ private _computeLabel(schema: HaFormSchema) {
+ return this.computeLabel
+ ? this.computeLabel(schema)
+ : schema
+ ? schema.name
+ : "";
+ }
+
+ private _computeSuffix(schema: HaFormSchema) {
+ return this.computeSuffix
+ ? this.computeSuffix(schema)
+ : schema && schema.description
+ ? schema.description.suffix
+ : "";
+ }
+
+ private _computeError(error, schema: HaFormSchema) {
+ return this.computeError ? this.computeError(error, schema) : error;
+ }
+
+ private _getValue(obj, item) {
+ if (obj) {
+ return obj[item.name];
+ }
+ return null;
+ }
+
+ private _valueChanged(ev: CustomEvent) {
+ ev.stopPropagation();
+ const schema = (ev.target as HaFormElement).schema;
+ const data = this.data as HaFormDataContainer;
+ data[schema.name] = ev.detail.value;
+ fireEvent(this, "value-changed", {
+ value: { ...data },
+ });
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ .error {
+ color: var(--error-color);
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-form": HaForm;
+ }
+}
diff --git a/src/components/ha-icon.ts b/src/components/ha-icon.ts
index 2e2a6ba848..5d71f214ed 100644
--- a/src/components/ha-icon.ts
+++ b/src/components/ha-icon.ts
@@ -1,4 +1,5 @@
-import { Constructor } from "lit-element";
+import { Constructor } from "../types";
+
import "@polymer/iron-icon/iron-icon";
// Not duplicate, this is for typing.
// tslint:disable-next-line
diff --git a/src/components/ha-label-badge.ts b/src/components/ha-label-badge.ts
index d1561cb160..736d04b8ef 100644
--- a/src/components/ha-label-badge.ts
+++ b/src/components/ha-label-badge.ts
@@ -1,31 +1,21 @@
import {
html,
LitElement,
- PropertyDeclarations,
PropertyValues,
TemplateResult,
CSSResult,
css,
+ property,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "./ha-icon";
class HaLabelBadge extends LitElement {
- public value?: string;
- public icon?: string;
- public label?: string;
- public description?: string;
- public image?: string;
-
- static get properties(): PropertyDeclarations {
- return {
- value: {},
- icon: {},
- label: {},
- description: {},
- image: {},
- };
- }
+ @property() public value?: string;
+ @property() public icon?: string;
+ @property() public label?: string;
+ @property() public description?: string;
+ @property() public image?: string;
protected render(): TemplateResult | void {
return html`
diff --git a/src/components/ha-paper-dropdown-menu.ts b/src/components/ha-paper-dropdown-menu.ts
index bed3e0ea10..72f2f6b15c 100644
--- a/src/components/ha-paper-dropdown-menu.ts
+++ b/src/components/ha-paper-dropdown-menu.ts
@@ -1,6 +1,6 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
-import { Constructor } from "lit-element";
import { PolymerElement } from "@polymer/polymer";
+import { Constructor } from "../types";
const paperDropdownClass = customElements.get(
"paper-dropdown-menu"
diff --git a/src/components/ha-paper-icon-button-arrow-next.ts b/src/components/ha-paper-icon-button-arrow-next.ts
index 6bf526ec2e..f0d4730385 100644
--- a/src/components/ha-paper-icon-button-arrow-next.ts
+++ b/src/components/ha-paper-icon-button-arrow-next.ts
@@ -1,5 +1,5 @@
-import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
+import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
diff --git a/src/components/ha-paper-icon-button-arrow-prev.ts b/src/components/ha-paper-icon-button-arrow-prev.ts
index dd6378acad..d79c8c66f4 100644
--- a/src/components/ha-paper-icon-button-arrow-prev.ts
+++ b/src/components/ha-paper-icon-button-arrow-prev.ts
@@ -1,5 +1,5 @@
-import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
+import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
diff --git a/src/components/ha-paper-icon-button-next.ts b/src/components/ha-paper-icon-button-next.ts
index e3038e5a00..cca612de4f 100644
--- a/src/components/ha-paper-icon-button-next.ts
+++ b/src/components/ha-paper-icon-button-next.ts
@@ -1,5 +1,5 @@
-import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
+import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
diff --git a/src/components/ha-paper-icon-button-prev.ts b/src/components/ha-paper-icon-button-prev.ts
index 396482be4b..ca4c32ff1c 100644
--- a/src/components/ha-paper-icon-button-prev.ts
+++ b/src/components/ha-paper-icon-button-prev.ts
@@ -1,5 +1,5 @@
-import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
+import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
diff --git a/src/components/ha-switch.ts b/src/components/ha-switch.ts
index 4e6a57ac65..b31909c499 100644
--- a/src/components/ha-switch.ts
+++ b/src/components/ha-switch.ts
@@ -1,8 +1,9 @@
-import { Constructor, customElement, CSSResult, css, query } from "lit-element";
+import { customElement, CSSResult, css, query } from "lit-element";
import "@material/mwc-switch";
import { style } from "@material/mwc-switch/mwc-switch-css";
// tslint:disable-next-line
import { Switch } from "@material/mwc-switch";
+import { Constructor } from "../types";
// tslint:disable-next-line
const MwcSwitch = customElements.get("mwc-switch") as Constructor;
@@ -12,7 +13,10 @@ export class HaSwitch extends MwcSwitch {
protected firstUpdated() {
super.firstUpdated();
- this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
+ this.style.setProperty(
+ "--mdc-theme-secondary",
+ "var(--switch-checked-color)"
+ );
this.classList.toggle(
"slotted",
Boolean(this._slot.assignedNodes().length)
@@ -29,12 +33,12 @@ export class HaSwitch extends MwcSwitch {
align-items: center;
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb {
- background-color: var(--paper-toggle-button-unchecked-button-color);
- border-color: var(--paper-toggle-button-unchecked-button-color);
+ background-color: var(--switch-unchecked-button-color);
+ border-color: var(--switch-unchecked-button-color);
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track {
- background-color: var(--paper-toggle-button-unchecked-bar-color);
- border-color: var(--paper-toggle-button-unchecked-bar-color);
+ background-color: var(--switch-unchecked-track-color);
+ border-color: var(--switch-unchecked-track-color);
}
:host(.slotted) .mdc-switch {
margin-right: 24px;
diff --git a/src/components/paper-time-input.js b/src/components/paper-time-input.js
index 2a2d5bb52b..620da4d7b1 100644
--- a/src/components/paper-time-input.js
+++ b/src/components/paper-time-input.js
@@ -87,6 +87,10 @@ export class PaperTimeInput extends PolymerElement {
label {
@apply --paper-font-caption;
+ color: var(
+ --paper-input-container-color,
+ var(--secondary-text-color)
+ );
}
.time-input-wrap {
@@ -106,14 +110,17 @@ export class PaperTimeInput extends PolymerElement {
id="hour"
type="number"
value="{{hour}}"
+ label="[[hourLabel]]"
on-change="_shouldFormatHour"
- required=""
+ on-focus="_onFocus"
+ required
+ prevent-invalid-input
auto-validate="[[autoValidate]]"
- prevent-invalid-input=""
maxlength="2"
max="[[_computeHourMax(format)]]"
min="0"
- no-label-float=""
+ no-label-float$="[[!floatInputLabels]]"
+ always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
>
:
@@ -124,15 +131,40 @@ export class PaperTimeInput extends PolymerElement {
id="min"
type="number"
value="{{min}}"
+ label="[[minLabel]]"
on-change="_formatMin"
- required=""
+ on-focus="_onFocus"
+ required
auto-validate="[[autoValidate]]"
- prevent-invalid-input=""
+ prevent-invalid-input
maxlength="2"
max="59"
min="0"
- no-label-float=""
+ no-label-float$="[[!floatInputLabels]]"
+ always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
+ >
+ :
+
+
+
+
@@ -180,6 +212,20 @@ export class PaperTimeInput extends PolymerElement {
type: Boolean,
value: false,
},
+ /**
+ * float the input labels
+ */
+ floatInputLabels: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * always float the input labels
+ */
+ alwaysFloatInputLabels: {
+ type: Boolean,
+ value: false,
+ },
/**
* 12 or 24 hr format
*/
@@ -208,6 +254,48 @@ export class PaperTimeInput extends PolymerElement {
type: String,
notify: true,
},
+ /**
+ * second
+ */
+ sec: {
+ type: String,
+ notify: true,
+ },
+ /**
+ * Suffix for the hour input
+ */
+ hourLabel: {
+ type: String,
+ value: "",
+ },
+ /**
+ * Suffix for the min input
+ */
+ minLabel: {
+ type: String,
+ value: ":",
+ },
+ /**
+ * Suffix for the sec input
+ */
+ secLabel: {
+ type: String,
+ value: "",
+ },
+ /**
+ * show the sec field
+ */
+ enableSecond: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * limit hours input
+ */
+ noHoursLimit: {
+ type: Boolean,
+ value: false,
+ },
/**
* AM or PM
*/
@@ -223,7 +311,7 @@ export class PaperTimeInput extends PolymerElement {
type: String,
notify: true,
readOnly: true,
- computed: "_computeTime(min, hour, amPm)",
+ computed: "_computeTime(min, hour, sec, amPm)",
},
};
}
@@ -238,6 +326,10 @@ export class PaperTimeInput extends PolymerElement {
if (!this.$.hour.validate() | !this.$.min.validate()) {
valid = false;
}
+ // Validate second field
+ if (this.enableSecond && !this.$.sec.validate()) {
+ valid = false;
+ }
// Validate AM PM if 12 hour time
if (this.format === 12 && !this.$.dropdown.validate()) {
valid = false;
@@ -248,15 +340,37 @@ export class PaperTimeInput extends PolymerElement {
/**
* Create time string
*/
- _computeTime(min, hour, amPm) {
- if (hour && min) {
- // No ampm on 24 hr time
- if (this.format === 24) {
- amPm = "";
+ _computeTime(min, hour, sec, amPm) {
+ let str;
+ if (hour || min || (sec && this.enableSecond)) {
+ hour = hour || "00";
+ min = min || "00";
+ sec = sec || "00";
+ str = hour + ":" + min;
+ // add sec field
+ if (this.enableSecond && sec) {
+ str = str + ":" + sec;
+ }
+ // No ampm on 24 hr time
+ if (this.format === 12) {
+ str = str + " " + amPm;
}
- return hour + ":" + min + " " + amPm;
}
- return undefined;
+
+ return str;
+ }
+
+ _onFocus(ev) {
+ ev.target.inputElement.inputElement.select();
+ }
+
+ /**
+ * Format sec
+ */
+ _formatSec() {
+ if (this.sec.toString().length === 1) {
+ this.sec = this.sec.toString().padStart(2, "0");
+ }
}
/**
@@ -264,16 +378,16 @@ export class PaperTimeInput extends PolymerElement {
*/
_formatMin() {
if (this.min.toString().length === 1) {
- this.min = this.min < 10 ? "0" + this.min : this.min;
+ this.min = this.min.toString().padStart(2, "0");
}
}
/**
- * Hour needs a leading zero in 24hr format
+ * Format hour
*/
_shouldFormatHour() {
if (this.format === 24 && this.hour.toString().length === 1) {
- this.hour = this.hour < 10 ? "0" + this.hour : this.hour;
+ this.hour = this.hour.toString().padStart(2, "0");
}
}
@@ -281,6 +395,9 @@ export class PaperTimeInput extends PolymerElement {
* 24 hour format has a max hr of 23
*/
_computeHourMax(format) {
+ if (this.noHoursLimit) {
+ return null;
+ }
if (format === 12) {
return format;
}
diff --git a/src/components/state-history-chart-line.js b/src/components/state-history-chart-line.js
index f94507c2df..6100e4022c 100644
--- a/src/components/state-history-chart-line.js
+++ b/src/components/state-history-chart-line.js
@@ -181,23 +181,63 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
state.attributes.target_temp_low
);
- addColumn(name + " current temperature", true);
+ addColumn(
+ `${this.hass.localize(
+ "ui.card.climate.current_temperature",
+ "name",
+ name
+ )}`,
+ true
+ );
if (hasHeat) {
- addColumn(name + " heating", true, true);
+ addColumn(
+ `${this.hass.localize("ui.card.climate.heating", "name", name)}`,
+ true,
+ true
+ );
// The "heating" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasCool) {
- addColumn(name + " cooling", true, true);
+ addColumn(
+ `${this.hass.localize("ui.card.climate.cooling", "name", name)}`,
+ true,
+ true
+ );
// The "cooling" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasTargetRange) {
- addColumn(name + " target temperature high", true);
- addColumn(name + " target temperature low", true);
+ addColumn(
+ `${this.hass.localize(
+ "ui.card.climate.target_temperature_mode",
+ "name",
+ name,
+ "mode",
+ this.hass.localize("ui.card.climate.high")
+ )}`,
+ true
+ );
+ addColumn(
+ `${this.hass.localize(
+ "ui.card.climate.target_temperature_mode",
+ "name",
+ name,
+ "mode",
+ this.hass.localize("ui.card.climate.low")
+ )}`,
+ true
+ );
} else {
- addColumn(name + " target temperature", true);
+ addColumn(
+ `${this.hass.localize(
+ "ui.card.climate.target_temperature_entity",
+ "name",
+ name
+ )}`,
+ true
+ );
}
states.states.forEach((state) => {
diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts
index d162842cfb..817f1fa4c9 100644
--- a/src/components/user/ha-user-picker.ts
+++ b/src/components/user/ha-user-picker.ts
@@ -18,19 +18,20 @@ import { fireEvent } from "../../common/dom/fire_event";
import { User, fetchUsers } from "../../data/user";
import { compare } from "../../common/string/compare";
-class HaEntityPicker extends LitElement {
+class HaUserPicker extends LitElement {
public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public users?: User[];
private _sortedUsers = memoizeOne((users?: User[]) => {
- if (!users || users.length === 1) {
- return users || [];
+ if (!users) {
+ return [];
}
- const sorted = [...users];
- sorted.sort((a, b) => compare(a.name, b.name));
- return sorted;
+
+ return users
+ .filter((user) => !user.system_generated)
+ .sort((a, b) => compare(a.name, b.name));
});
protected render(): TemplateResult | void {
@@ -101,4 +102,4 @@ class HaEntityPicker extends LitElement {
}
}
-customElements.define("ha-user-picker", HaEntityPicker);
+customElements.define("ha-user-picker", HaUserPicker);
diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts
index 0a308fcdbc..58be904d62 100644
--- a/src/data/device_automation.ts
+++ b/src/data/device_automation.ts
@@ -39,6 +39,15 @@ export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
device_id: deviceId,
});
+export const fetchDeviceActionCapabilities = (
+ hass: HomeAssistant,
+ action: DeviceAction
+) =>
+ hass.callWS({
+ type: "device_automation/action/capabilities",
+ action,
+ });
+
export const fetchDeviceConditionCapabilities = (
hass: HomeAssistant,
condition: DeviceCondition
@@ -57,7 +66,7 @@ export const fetchDeviceTriggerCapabilities = (
trigger,
});
-const whitelist = ["above", "below", "for"];
+const whitelist = ["above", "below", "code", "for"];
export const deviceAutomationsEqual = (
a: DeviceAutomation,
diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts
index 5f32b70b4b..d5ba052dea 100644
--- a/src/data/lovelace.ts
+++ b/src/data/lovelace.ts
@@ -11,7 +11,7 @@ export interface LovelaceConfig {
export interface LovelaceViewConfig {
index?: number;
title?: string;
- badges?: string[];
+ badges?: Array;
cards?: LovelaceCardConfig[];
path?: string;
icon?: string;
@@ -25,6 +25,11 @@ export interface ShowViewConfig {
user?: string;
}
+export interface LovelaceBadgeConfig {
+ type?: string;
+ [key: string]: any;
+}
+
export interface LovelaceCardConfig {
index?: number;
view_index?: number;
@@ -32,11 +37,11 @@ export interface LovelaceCardConfig {
[key: string]: any;
}
-export interface ToggleActionConfig {
+export interface ToggleActionConfig extends BaseActionConfig {
action: "toggle";
}
-export interface CallServiceActionConfig {
+export interface CallServiceActionConfig extends BaseActionConfig {
action: "call-service";
service: string;
service_data?: {
@@ -45,24 +50,37 @@ export interface CallServiceActionConfig {
};
}
-export interface NavigateActionConfig {
+export interface NavigateActionConfig extends BaseActionConfig {
action: "navigate";
navigation_path: string;
}
-export interface UrlActionConfig {
+export interface UrlActionConfig extends BaseActionConfig {
action: "url";
url_path: string;
}
-export interface MoreInfoActionConfig {
+export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info";
}
-export interface NoActionConfig {
+export interface NoActionConfig extends BaseActionConfig {
action: "none";
}
+export interface BaseActionConfig {
+ confirmation?: ConfirmationRestrictionConfig;
+}
+
+export interface ConfirmationRestrictionConfig {
+ text?: string;
+ exemptions?: RestrictionConfig[];
+}
+
+export interface RestrictionConfig {
+ user: string;
+}
+
export type ActionConfig =
| ToggleActionConfig
| CallServiceActionConfig
@@ -108,3 +126,7 @@ export const getLovelaceCollection = (conn: Connection) =>
export interface WindowWithLovelaceProm extends Window {
llConfProm?: Promise;
}
+
+export interface LongPressOptions {
+ hasDoubleClick?: boolean;
+}
diff --git a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts
index 1064d41b36..2950d4c61a 100644
--- a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts
+++ b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts
@@ -60,7 +60,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
@opened-changed="${this._openedChanged}"
>
- ${this.hass.localize("ui.dialogs.config_entry_system_options.title")}
+ ${this.hass.localize(
+ "ui.dialogs.config_entry_system_options.title",
+ "integration",
+ this.hass.localize(
+ `component.${this._params.entry.domain}.config.title`
+ ) || this._params.entry.domain
+ )}
${this._loading
@@ -89,7 +95,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
${this.hass.localize(
- "ui.dialogs.config_entry_system_options.enable_new_entities_description"
+ "ui.dialogs.config_entry_system_options.enable_new_entities_description",
+ "integration",
+ this.hass.localize(
+ `component.${
+ this._params.entry.domain
+ }.config.title`
+ ) || this._params.entry.domain
)}
diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts
index c10900e340..d51838a682 100644
--- a/src/dialogs/config-flow/dialog-data-entry-flow.ts
+++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts
@@ -14,7 +14,7 @@ import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-spinner/paper-spinner";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
-import "../../components/ha-form";
+import "../../components/ha-form/ha-form";
import "../../components/ha-markdown";
import "../../resources/ha-style";
import "../../components/dialog/ha-paper-dialog";
@@ -141,6 +141,7 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.handlers=${this._handlers}
+ .showAdvanced=${this._params.showAdvanced}
>
`
: this._step.type === "form"
diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts
index a3609a733f..3d34834335 100644
--- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts
+++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts
@@ -75,6 +75,7 @@ export interface DataEntryFlowDialogParams {
continueFlowId?: string;
dialogClosedCallback?: (params: { flowFinished: boolean }) => void;
flowConfig: FlowConfig;
+ showAdvanced?: boolean;
}
export const loadDataEntryFlowDialog = () =>
diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts
index 4eb73f5c02..3b3d2fbad3 100644
--- a/src/dialogs/config-flow/step-flow-form.ts
+++ b/src/dialogs/config-flow/step-flow-form.ts
@@ -12,10 +12,9 @@ import "@material/mwc-button";
import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-spinner/paper-spinner";
-import "../../components/ha-form";
+import "../../components/ha-form/ha-form";
import "../../components/ha-markdown";
import "../../resources/ha-style";
-import { PolymerChangedEvent, applyPolymerEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { configFlowContentStyles } from "./styles";
@@ -69,7 +68,7 @@ class StepFlowForm extends LitElement {
${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)}
): void {
- this._stepData = applyPolymerEvent(ev, this._stepData);
+ private _stepDataChanged(ev: CustomEvent): void {
+ this._stepData = ev.detail.value;
}
private _labelCallback = (field: FieldSchema): string =>
diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts
index 6eaa5a5b01..ef410f7529 100644
--- a/src/dialogs/config-flow/step-flow-pick-handler.ts
+++ b/src/dialogs/config-flow/step-flow-pick-handler.ts
@@ -31,6 +31,7 @@ class StepFlowPickHandler extends LitElement {
@property() public hass!: HomeAssistant;
@property() public handlers!: string[];
+ @property() public showAdvanced?: boolean;
@property() private filter?: string;
private _width?: number;
@@ -79,18 +80,24 @@ class StepFlowPickHandler extends LitElement {
`
)}
-
- ${this.hass.localize(
- "ui.panel.config.integrations.note_about_integrations"
- )}
- ${this.hass.localize(
- "ui.panel.config.integrations.note_about_website_reference"
- )}${this.hass.localize(
- "ui.panel.config.integrations.home_assistant_website"
- )}.
-
+ ${this.showAdvanced
+ ? html`
+
+ ${this.hass.localize(
+ "ui.panel.config.integrations.note_about_integrations"
+ )}
+ ${this.hass.localize(
+ "ui.panel.config.integrations.note_about_website_reference"
+ )}${this.hass.localize(
+ "ui.panel.config.integrations.home_assistant_website"
+ )} .
+
+ `
+ : ""}
`;
}
@@ -133,6 +140,11 @@ class StepFlowPickHandler extends LitElement {
}
p {
text-align: center;
+ padding: 16px;
+ margin: 0;
+ }
+ p > a {
+ color: var(--primary-color);
}
`;
}
diff --git a/src/dialogs/confirmation/dialog-confirmation.ts b/src/dialogs/confirmation/dialog-confirmation.ts
new file mode 100644
index 0000000000..23bc7d312e
--- /dev/null
+++ b/src/dialogs/confirmation/dialog-confirmation.ts
@@ -0,0 +1,107 @@
+import {
+ LitElement,
+ html,
+ css,
+ CSSResult,
+ TemplateResult,
+ customElement,
+ property,
+} from "lit-element";
+import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
+import "@polymer/paper-input/paper-input";
+
+import "../../components/dialog/ha-paper-dialog";
+import "../../components/ha-switch";
+
+import { HomeAssistant } from "../../types";
+import { ConfirmationDialogParams } from "./show-dialog-confirmation";
+import { PolymerChangedEvent } from "../../polymer-types";
+import { haStyleDialog } from "../../resources/styles";
+
+@customElement("dialog-confirmation")
+class DialogConfirmation extends LitElement {
+ @property() public hass!: HomeAssistant;
+ @property() private _params?: ConfirmationDialogParams;
+
+ public async showDialog(params: ConfirmationDialogParams): Promise {
+ this._params = params;
+ }
+
+ protected render(): TemplateResult | void {
+ if (!this._params) {
+ return html``;
+ }
+
+ return html`
+
+
+ ${this._params.title
+ ? this._params.title
+ : this.hass.localize("ui.dialogs.confirmation.title")}
+
+
+ ${this._params.text}
+
+
+
+ ${this.hass.localize("ui.dialogs.confirmation.cancel")}
+
+
+ ${this.hass.localize("ui.dialogs.confirmation.ok")}
+
+
+
+ `;
+ }
+
+ private async _dismiss(): Promise {
+ this._params = undefined;
+ }
+
+ private async _confirm(): Promise {
+ this._params!.confirm();
+ this._dismiss();
+ }
+
+ private _openedChanged(ev: PolymerChangedEvent): void {
+ if (!(ev.detail as any).value) {
+ this._params = undefined;
+ }
+ }
+
+ static get styles(): CSSResult[] {
+ return [
+ haStyleDialog,
+ css`
+ ha-paper-dialog {
+ min-width: 400px;
+ max-width: 500px;
+ }
+ @media (max-width: 400px) {
+ ha-paper-dialog {
+ min-width: initial;
+ }
+ }
+ p {
+ margin: 0;
+ padding-top: 6px;
+ padding-bottom: 24px;
+ color: var(--primary-text-color);
+ }
+ .secondary {
+ color: var(--secondary-text-color);
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "dialog-confirmation": DialogConfirmation;
+ }
+}
diff --git a/src/dialogs/confirmation/show-dialog-confirmation.ts b/src/dialogs/confirmation/show-dialog-confirmation.ts
new file mode 100644
index 0000000000..47b3e556b8
--- /dev/null
+++ b/src/dialogs/confirmation/show-dialog-confirmation.ts
@@ -0,0 +1,21 @@
+import { fireEvent } from "../../common/dom/fire_event";
+
+export interface ConfirmationDialogParams {
+ title?: string;
+ text: string;
+ confirm: () => void;
+}
+
+export const loadConfirmationDialog = () =>
+ import(/* webpackChunkName: "confirmation" */ "./dialog-confirmation");
+
+export const showConfirmationDialog = (
+ element: HTMLElement,
+ systemLogDetailParams: ConfirmationDialogParams
+): void => {
+ fireEvent(element, "show-dialog", {
+ dialogTag: "dialog-confirmation",
+ dialogImport: loadConfirmationDialog,
+ dialogParams: systemLogDetailParams,
+ });
+};
diff --git a/src/dialogs/more-info/controls/more-info-alarm_control_panel.js b/src/dialogs/more-info/controls/more-info-alarm_control_panel.js
index 1a96927064..64ab1d8ba5 100644
--- a/src/dialogs/more-info/controls/more-info-alarm_control_panel.js
+++ b/src/dialogs/more-info/controls/more-info-alarm_control_panel.js
@@ -29,8 +29,9 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
width: 80px;
}
.actions mwc-button {
- min-width: 160px;
- margin-bottom: 16px;
+ flex: 1 0 50%;
+ margin: 0 4px 16px;
+ max-width: 200px;
}
mwc-button.disarm {
color: var(--google-red-500);
@@ -137,7 +138,7 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
+
+ ${this.hass!.localize("ui.card.counter.actions.increment")}
+
+
+ ${this.hass!.localize("ui.card.counter.actions.decrement")}
+
+
+ ${this.hass!.localize("ui.card.counter.actions.reset")}
+
+
+ `;
+ }
+
+ private _handleActionClick(e: MouseEvent): void {
+ const action = (e.currentTarget as any).action;
+ this.hass.callService("counter", action, {
+ entity_id: this.stateObj!.entity_id,
+ });
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ .actions {
+ margin: 0 8px;
+ padding-top: 20px;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "more-info-counter": MoreInfoCounter;
+ }
+}
diff --git a/src/dialogs/more-info/controls/more-info-sun.js b/src/dialogs/more-info/controls/more-info-sun.js
deleted file mode 100644
index d806826f23..0000000000
--- a/src/dialogs/more-info/controls/more-info-sun.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import "@polymer/iron-flex-layout/iron-flex-layout-classes";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import "../../../components/ha-relative-time";
-
-import LocalizeMixin from "../../../mixins/localize-mixin";
-import formatTime from "../../../common/datetime/format_time";
-
-class MoreInfoSun extends LocalizeMixin(PolymerElement) {
- static get template() {
- return html`
-
-
-
-
-
- [[itemCaption(item)]]
-
-
-
[[itemValue(item)]]
-
-
-
-
- [[localize('ui.dialogs.more_info_control.sun.elevation')]]
-
-
[[stateObj.attributes.elevation]]
-
- `;
- }
-
- static get properties() {
- return {
- hass: Object,
- stateObj: Object,
- risingDate: {
- type: Object,
- computed: "computeRising(stateObj)",
- },
-
- settingDate: {
- type: Object,
- computed: "computeSetting(stateObj)",
- },
- };
- }
-
- computeRising(stateObj) {
- return new Date(stateObj.attributes.next_rising);
- }
-
- computeSetting(stateObj) {
- return new Date(stateObj.attributes.next_setting);
- }
-
- computeOrder(risingDate, settingDate) {
- return risingDate > settingDate ? ["set", "ris"] : ["ris", "set"];
- }
-
- itemCaption(type) {
- if (type === "ris") {
- return this.localize("ui.dialogs.more_info_control.sun.rising");
- }
- return this.localize("ui.dialogs.more_info_control.sun.setting");
- }
-
- itemDate(type) {
- return type === "ris" ? this.risingDate : this.settingDate;
- }
-
- itemValue(type) {
- return formatTime(this.itemDate(type), this.hass.language);
- }
-}
-
-customElements.define("more-info-sun", MoreInfoSun);
diff --git a/src/dialogs/more-info/controls/more-info-sun.ts b/src/dialogs/more-info/controls/more-info-sun.ts
new file mode 100644
index 0000000000..088e686379
--- /dev/null
+++ b/src/dialogs/more-info/controls/more-info-sun.ts
@@ -0,0 +1,84 @@
+import {
+ property,
+ LitElement,
+ TemplateResult,
+ html,
+ customElement,
+ CSSResult,
+ css,
+} from "lit-element";
+import { HassEntity } from "home-assistant-js-websocket";
+
+import "../../../components/ha-relative-time";
+
+import formatTime from "../../../common/datetime/format_time";
+import { HomeAssistant } from "../../../types";
+
+@customElement("more-info-sun")
+class MoreInfoSun extends LitElement {
+ @property() public hass!: HomeAssistant;
+ @property() public stateObj?: HassEntity;
+
+ protected render(): TemplateResult | void {
+ if (!this.hass || !this.stateObj) {
+ return html``;
+ }
+
+ const risingDate = new Date(this.stateObj.attributes.next_rising);
+ const settingDate = new Date(this.stateObj.attributes.next_setting);
+ const order = risingDate > settingDate ? ["set", "ris"] : ["ris", "set"];
+
+ return html`
+ ${order.map((item) => {
+ return html`
+
+
+ ${item === "ris"
+ ? this.hass.localize(
+ "ui.dialogs.more_info_control.sun.rising"
+ )
+ : this.hass.localize(
+ "ui.dialogs.more_info_control.sun.setting"
+ )}
+
+
+
+ ${formatTime(
+ item === "ris" ? risingDate : settingDate,
+ this.hass.language
+ )}
+
+
+ `;
+ })}
+
+
+ ${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")}
+
+
${this.stateObj.attributes.elevation}
+
+ `;
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ .row {
+ margin: 0 8px;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "more-info-sun": MoreInfoSun;
+ }
+}
diff --git a/src/dialogs/more-info/controls/more-info-weather.js b/src/dialogs/more-info/controls/more-info-weather.js
deleted file mode 100644
index a9309905cd..0000000000
--- a/src/dialogs/more-info/controls/more-info-weather.js
+++ /dev/null
@@ -1,235 +0,0 @@
-import "@polymer/iron-icon/iron-icon";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import LocalizeMixin from "../../../mixins/localize-mixin";
-
-/*
- * @appliesMixin LocalizeMixin
- */
-class MoreInfoWeather extends LocalizeMixin(PolymerElement) {
- static get template() {
- return html`
-
-
-
-
-
- [[localize('ui.card.weather.attributes.temperature')]]
-
-
- [[stateObj.attributes.temperature]] [[getUnit('temperature')]]
-
-
-
-
-
-
- [[localize('ui.card.weather.attributes.air_pressure')]]
-
-
- [[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
-
-
-
-
-
-
-
- [[localize('ui.card.weather.attributes.humidity')]]
-
-
[[stateObj.attributes.humidity]] %
-
-
-
-
-
-
- [[localize('ui.card.weather.attributes.wind_speed')]]
-
-
- [[getWind(stateObj.attributes.wind_speed,
- stateObj.attributes.wind_bearing, localize)]]
-
-
-
-
-
-
-
- [[localize('ui.card.weather.attributes.visibility')]]
-
-
[[stateObj.attributes.visibility]] [[getUnit('length')]]
-
-
-
-
- [[localize('ui.card.weather.forecast')]]:
-
-
-
-
-
-
- [[computeDateTime(item.datetime)]]
-
-
- [[computeDate(item.datetime)]]
-
- [[item.templow]] [[getUnit('temperature')]]
-
-
-
- [[item.temperature]] [[getUnit('temperature')]]
-
-
-
-
-
-
- [[stateObj.attributes.attribution]]
-
- `;
- }
-
- static get properties() {
- return {
- hass: Object,
- stateObj: Object,
- };
- }
-
- constructor() {
- super();
- this.cardinalDirections = [
- "N",
- "NNE",
- "NE",
- "ENE",
- "E",
- "ESE",
- "SE",
- "SSE",
- "S",
- "SSW",
- "SW",
- "WSW",
- "W",
- "WNW",
- "NW",
- "NNW",
- "N",
- ];
- this.weatherIcons = {
- "clear-night": "hass:weather-night",
- cloudy: "hass:weather-cloudy",
- exceptional: "hass:alert-circle-outline",
- fog: "hass:weather-fog",
- hail: "hass:weather-hail",
- lightning: "hass:weather-lightning",
- "lightning-rainy": "hass:weather-lightning-rainy",
- partlycloudy: "hass:weather-partly-cloudy",
- pouring: "hass:weather-pouring",
- rainy: "hass:weather-rainy",
- snowy: "hass:weather-snowy",
- "snowy-rainy": "hass:weather-snowy-rainy",
- sunny: "hass:weather-sunny",
- windy: "hass:weather-windy",
- "windy-variant": "hass:weather-windy-variant",
- };
- }
-
- computeDate(data) {
- const date = new Date(data);
- return date.toLocaleDateString(this.hass.language, {
- weekday: "long",
- month: "short",
- day: "numeric",
- });
- }
-
- computeDateTime(data) {
- const date = new Date(data);
- return date.toLocaleDateString(this.hass.language, {
- weekday: "long",
- hour: "numeric",
- });
- }
-
- getUnit(measure) {
- const lengthUnit = this.hass.config.unit_system.length || "";
- switch (measure) {
- case "air_pressure":
- return lengthUnit === "km" ? "hPa" : "inHg";
- case "length":
- return lengthUnit;
- case "precipitation":
- return lengthUnit === "km" ? "mm" : "in";
- default:
- return this.hass.config.unit_system[measure] || "";
- }
- }
-
- windBearingToText(degree) {
- const degreenum = parseInt(degree);
- if (isFinite(degreenum)) {
- return this.cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
- }
- return degree;
- }
-
- getWind(speed, bearing, localize) {
- if (bearing != null) {
- const cardinalDirection = this.windBearingToText(bearing);
- return `${speed} ${this.getUnit("length")}/h (${localize(
- `ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
- ) || cardinalDirection})`;
- }
- return `${speed} ${this.getUnit("length")}/h`;
- }
-
- getWeatherIcon(condition) {
- return this.weatherIcons[condition];
- }
-
- _showValue(item) {
- return typeof item !== "undefined" && item !== null;
- }
-}
-
-customElements.define("more-info-weather", MoreInfoWeather);
diff --git a/src/dialogs/more-info/controls/more-info-weather.ts b/src/dialogs/more-info/controls/more-info-weather.ts
new file mode 100644
index 0000000000..8d14f80cac
--- /dev/null
+++ b/src/dialogs/more-info/controls/more-info-weather.ts
@@ -0,0 +1,288 @@
+import "@polymer/iron-icon/iron-icon";
+import {
+ LitElement,
+ property,
+ CSSResult,
+ css,
+ customElement,
+ PropertyValues,
+} from "lit-element";
+import { HassEntity } from "home-assistant-js-websocket";
+import { TemplateResult, html } from "lit-html";
+
+import { HomeAssistant } from "../../../types";
+
+const cardinalDirections = [
+ "N",
+ "NNE",
+ "NE",
+ "ENE",
+ "E",
+ "ESE",
+ "SE",
+ "SSE",
+ "S",
+ "SSW",
+ "SW",
+ "WSW",
+ "W",
+ "WNW",
+ "NW",
+ "NNW",
+ "N",
+];
+
+const weatherIcons = {
+ "clear-night": "hass:weather-night",
+ cloudy: "hass:weather-cloudy",
+ exceptional: "hass:alert-circle-outline",
+ fog: "hass:weather-fog",
+ hail: "hass:weather-hail",
+ lightning: "hass:weather-lightning",
+ "lightning-rainy": "hass:weather-lightning-rainy",
+ partlycloudy: "hass:weather-partly-cloudy",
+ pouring: "hass:weather-pouring",
+ rainy: "hass:weather-rainy",
+ snowy: "hass:weather-snowy",
+ "snowy-rainy": "hass:weather-snowy-rainy",
+ sunny: "hass:weather-sunny",
+ windy: "hass:weather-windy",
+ "windy-variant": "hass:weather-windy-variant",
+};
+
+@customElement("more-info-weather")
+class MoreInfoWeather extends LitElement {
+ @property() public hass!: HomeAssistant;
+ @property() public stateObj?: HassEntity;
+
+ protected shouldUpdate(changedProps: PropertyValues): boolean {
+ if (changedProps.has("stateObj")) {
+ return true;
+ }
+
+ const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
+ if (
+ !oldHass ||
+ oldHass.language !== this.hass.language ||
+ oldHass.config.unit_system !== this.hass.config.unit_system
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ protected render(): TemplateResult | void {
+ if (!this.hass || !this.stateObj) {
+ return html``;
+ }
+
+ return html`
+
+
+
+ ${this.hass.localize("ui.card.weather.attributes.temperature")}
+
+
+ ${this.stateObj.attributes.temperature} ${this.getUnit("temperature")}
+
+
+ ${this.stateObj.attributes.pressure
+ ? html`
+
+
+
+ ${this.hass.localize("ui.card.weather.attributes.air_pressure")}
+
+
+ ${this.stateObj.attributes.pressure}
+ ${this.getUnit("air_pressure")}
+
+
+ `
+ : ""}
+ ${this.stateObj.attributes.humidity
+ ? html`
+
+
+
+ ${this.hass.localize("ui.card.weather.attributes.humidity")}
+
+
${this.stateObj.attributes.humidity} %
+
+ `
+ : ""}
+ ${this.stateObj.attributes.wind_speed
+ ? html`
+
+
+
+ ${this.hass.localize("ui.card.weather.attributes.wind_speed")}
+
+
+ ${this.getWind(
+ this.stateObj.attributes.wind_speed,
+ this.stateObj.attributes.wind_bearing
+ )}
+
+
+ `
+ : ""}
+ ${this.stateObj.attributes.visibility
+ ? html`
+
+
+
+ ${this.hass.localize("ui.card.weather.attributes.visibility")}
+
+
+ ${this.stateObj.attributes.visibility} ${this.getUnit("length")}
+
+
+ `
+ : ""}
+ ${this.stateObj.attributes.forecast
+ ? html`
+
+ ${this.hass.localize("ui.card.weather.forecast")}:
+
+ ${this.stateObj.attributes.forecast.map((item) => {
+ return html`
+
+ ${item.condition
+ ? html`
+
+ `
+ : ""}
+ ${!item.templow
+ ? html`
+
+ ${this.computeDateTime(item.datetime)}
+
+ `
+ : ""}
+ ${item.templow
+ ? html`
+
+ ${this.computeDate(item.datetime)}
+
+
+ ${item.templow} ${this.getUnit("temperature")}
+
+ `
+ : ""};
+
+ ${item.temperature} ${this.getUnit("temperature")}
+
+
+ `;
+ })}
+ `
+ : ""}
+ ${this.stateObj.attributes.attribution
+ ? html`
+
+ ${this.stateObj.attributes.attribution}
+
+ `
+ : ""}
+ `;
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ iron-icon {
+ color: var(--paper-item-icon-color);
+ }
+ .section {
+ margin: 16px 0 8px 0;
+ font-size: 1.2em;
+ }
+
+ .flex {
+ display: flex;
+ height: 32px;
+ align-items: center;
+ }
+
+ .main {
+ flex: 1;
+ margin-left: 24px;
+ }
+
+ .temp,
+ .templow {
+ min-width: 48px;
+ text-align: right;
+ }
+
+ .templow {
+ margin: 0 16px;
+ color: var(--secondary-text-color);
+ }
+
+ .attribution {
+ color: var(--secondary-text-color);
+ text-align: center;
+ }
+ `;
+ }
+
+ private computeDate(data) {
+ const date = new Date(data);
+ return date.toLocaleDateString(this.hass.language, {
+ weekday: "long",
+ month: "short",
+ day: "numeric",
+ });
+ }
+
+ private computeDateTime(data) {
+ const date = new Date(data);
+ return date.toLocaleDateString(this.hass.language, {
+ weekday: "long",
+ hour: "numeric",
+ });
+ }
+
+ private getUnit(measure: string): string {
+ const lengthUnit = this.hass.config.unit_system.length || "";
+ switch (measure) {
+ case "air_pressure":
+ return lengthUnit === "km" ? "hPa" : "inHg";
+ case "length":
+ return lengthUnit;
+ case "precipitation":
+ return lengthUnit === "km" ? "mm" : "in";
+ default:
+ return this.hass.config.unit_system[measure] || "";
+ }
+ }
+
+ private windBearingToText(degree: string): string {
+ const degreenum = parseInt(degree, 10);
+ if (isFinite(degreenum)) {
+ // tslint:disable-next-line: no-bitwise
+ return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
+ }
+ return degree;
+ }
+
+ private getWind(speed: string, bearing: string) {
+ if (bearing != null) {
+ const cardinalDirection = this.windBearingToText(bearing);
+ return `${speed} ${this.getUnit("length")}/h (${this.hass.localize(
+ `ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
+ ) || cardinalDirection})`;
+ }
+ return `${speed} ${this.getUnit("length")}/h`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "more-info-weather": MoreInfoWeather;
+ }
+}
diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts
index bdb58eee00..f3101d5125 100644
--- a/src/fake_data/provide_hass.ts
+++ b/src/fake_data/provide_hass.ts
@@ -1,4 +1,4 @@
-import applyThemesOnElement from "../common/dom/apply_themes_on_element";
+import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { demoConfig } from "./demo_config";
import { demoServices } from "./demo_services";
diff --git a/src/mixins/lit-localize-lite-mixin.ts b/src/mixins/lit-localize-lite-mixin.ts
index afc30c074a..c99d32c5a8 100644
--- a/src/mixins/lit-localize-lite-mixin.ts
+++ b/src/mixins/lit-localize-lite-mixin.ts
@@ -1,45 +1,20 @@
-import {
- Constructor,
- LitElement,
- PropertyDeclarations,
- PropertyValues,
-} from "lit-element";
-import { getLocalLanguage } from "../util/hass-translation";
-import { localizeLiteBaseMixin } from "./localize-lite-base-mixin";
+import { LitElement, PropertyValues, property } from "lit-element";
+import { getLocalLanguage, getTranslation } from "../util/hass-translation";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
+import { Constructor, Resources } from "../types";
const empty = () => "";
-interface LitLocalizeLiteMixin {
- language: string;
- resources: {};
- translationFragment: string;
- localize: LocalizeFunc;
-}
-
-export const litLocalizeLiteMixin = (
- superClass: Constructor
-): Constructor =>
- // @ts-ignore
- class extends localizeLiteBaseMixin(superClass) {
- public localize: LocalizeFunc;
-
- static get properties(): PropertyDeclarations {
- return {
- localize: {},
- language: {},
- resources: {},
- translationFragment: {},
- };
- }
-
- constructor() {
- super();
- // This will prevent undefined errors if called before connected to DOM.
- this.localize = empty;
- // Use browser language setup before login.
- this.language = getLocalLanguage();
- }
+export const litLocalizeLiteMixin = >(
+ superClass: T
+) => {
+ class LitLocalizeLiteClass extends superClass {
+ // Initialized to empty will prevent undefined errors if called before connected to DOM.
+ @property() public localize: LocalizeFunc = empty;
+ @property() public resources?: Resources;
+ // Use browser language setup before login.
+ @property() public language?: string = getLocalLanguage();
+ @property() public translationFragment?: string;
public connectedCallback(): void {
super.connectedCallback();
@@ -51,7 +26,7 @@ export const litLocalizeLiteMixin = (
);
}
- public updated(changedProperties: PropertyValues) {
+ protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (
changedProperties.has("language") ||
@@ -64,4 +39,38 @@ export const litLocalizeLiteMixin = (
);
}
}
- };
+
+ protected async _initializeLocalizeLite() {
+ if (this.resources) {
+ return;
+ }
+
+ if (!this.translationFragment) {
+ // In dev mode, we will issue a warning if after a second we are still
+ // not configured correctly.
+ if (__DEV__) {
+ setTimeout(
+ () =>
+ !this.resources &&
+ // tslint:disable-next-line
+ console.error(
+ "Forgot to pass in resources or set translationFragment for",
+ this.nodeName
+ ),
+ 1000
+ );
+ }
+ return;
+ }
+
+ const { language, data } = await getTranslation(
+ this.translationFragment!,
+ this.language!
+ );
+ this.resources = {
+ [language]: data,
+ };
+ }
+ }
+ return LitLocalizeLiteClass;
+};
diff --git a/src/mixins/localize-lite-base-mixin.ts b/src/mixins/localize-lite-base-mixin.ts
deleted file mode 100644
index 83fbd612af..0000000000
--- a/src/mixins/localize-lite-base-mixin.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Lite base mixin to add localization without depending on the Hass object.
- */
-import { getTranslation } from "../util/hass-translation";
-import { Resources } from "../types";
-
-/**
- * @polymerMixin
- */
-export const localizeLiteBaseMixin = (superClass) =>
- class extends superClass {
- public resources?: Resources;
- public language?: string;
- public translationFragment?: string;
-
- protected _initializeLocalizeLite() {
- if (this.resources) {
- return;
- }
-
- if (!this.translationFragment) {
- // In dev mode, we will issue a warning if after a second we are still
- // not configured correctly.
- if (__DEV__) {
- setTimeout(
- () =>
- !this.resources &&
- // tslint:disable-next-line
- console.error(
- "Forgot to pass in resources or set translationFragment for",
- this.nodeName
- ),
- 1000
- );
- }
- return;
- }
-
- this._downloadResources();
- }
-
- private async _downloadResources() {
- const { language, data } = await getTranslation(
- this.translationFragment!,
- this.language!
- );
- this.resources = {
- [language]: data,
- };
- }
- };
diff --git a/src/mixins/localize-lite-mixin.ts b/src/mixins/localize-lite-mixin.ts
deleted file mode 100644
index 8fcf112972..0000000000
--- a/src/mixins/localize-lite-mixin.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Lite mixin to add localization without depending on the Hass object.
- */
-import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin";
-import { getLocalLanguage } from "../util/hass-translation";
-import { localizeLiteBaseMixin } from "./localize-lite-base-mixin";
-import { computeLocalize } from "../common/translations/localize";
-
-/**
- * @polymerMixin
- */
-export const localizeLiteMixin = dedupingMixin(
- (superClass) =>
- class extends localizeLiteBaseMixin(superClass) {
- static get properties() {
- return {
- language: {
- type: String,
- // Use browser language setup before login.
- value: getLocalLanguage(),
- },
- resources: Object,
- // The fragment to load.
- translationFragment: String,
- /**
- * Translates a string to the current `language`. Any parameters to the
- * string should be passed in order, as follows:
- * `localize(stringKey, param1Name, param1Value, param2Name, param2Value)`
- */
- localize: {
- type: Function,
- computed: "__computeLocalize(language, resources, formats)",
- },
- };
- }
-
- public ready() {
- super.ready();
- this._initializeLocalizeLite();
- }
-
- protected __computeLocalize(language, resources, formats?) {
- return computeLocalize(
- this.constructor.prototype,
- language,
- resources,
- formats
- );
- }
- }
-);
diff --git a/src/mixins/provide-hass-lit-mixin.ts b/src/mixins/provide-hass-lit-mixin.ts
index 3b45ecedbe..fb9d1b3e93 100644
--- a/src/mixins/provide-hass-lit-mixin.ts
+++ b/src/mixins/provide-hass-lit-mixin.ts
@@ -1,5 +1,5 @@
-import { UpdatingElement, Constructor, PropertyValues } from "lit-element";
-import { HomeAssistant } from "../types";
+import { UpdatingElement, PropertyValues } from "lit-element";
+import { HomeAssistant, Constructor } from "../types";
export interface ProvideHassElement {
provideHass(element: HTMLElement);
@@ -7,9 +7,9 @@ export interface ProvideHassElement {
/* tslint:disable */
-export const ProvideHassLitMixin = (
- superClass: Constructor
-): Constructor =>
+export const ProvideHassLitMixin = >(
+ superClass: T
+) =>
// @ts-ignore
class extends superClass {
protected hass!: HomeAssistant;
diff --git a/src/mixins/subscribe-mixin.ts b/src/mixins/subscribe-mixin.ts
index 63e9f1436b..350055d232 100644
--- a/src/mixins/subscribe-mixin.ts
+++ b/src/mixins/subscribe-mixin.ts
@@ -1,32 +1,21 @@
-import {
- LitElement,
- Constructor,
- PropertyValues,
- PropertyDeclarations,
-} from "lit-element";
+import { LitElement, PropertyValues, property } from "lit-element";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
-import { HomeAssistant } from "../types";
+import { HomeAssistant, Constructor } from "../types";
export interface HassSubscribeElement {
hassSubscribe(): UnsubscribeFunc[];
}
/* tslint:disable-next-line */
-export const SubscribeMixin = (
- superClass: Constructor
-): Constructor =>
- // @ts-ignore
- class extends superClass {
- private hass?: HomeAssistant;
+export const SubscribeMixin = >(
+ superClass: T
+) => {
+ class SubscribeClass extends superClass {
+ @property() public hass?: HomeAssistant;
+
/* tslint:disable-next-line */
private __unsubs?: UnsubscribeFunc[];
- static get properties(): PropertyDeclarations {
- return {
- hass: {},
- };
- }
-
public connectedCallback() {
super.connectedCallback();
this.__checkSubscribed();
@@ -50,7 +39,6 @@ export const SubscribeMixin = (
}
protected hassSubscribe(): UnsubscribeFunc[] {
- super.hassSubscribe();
return [];
}
@@ -64,4 +52,6 @@ export const SubscribeMixin = (
}
this.__unsubs = this.hassSubscribe();
}
- };
+ }
+ return SubscribeClass;
+};
diff --git a/src/onboarding/integration-badge.ts b/src/onboarding/integration-badge.ts
index 3035c8b893..df2adb3c18 100644
--- a/src/onboarding/integration-badge.ts
+++ b/src/onboarding/integration-badge.ts
@@ -74,6 +74,7 @@ class IntegrationBadge extends LitElement {
.title {
min-height: 2.3em;
+ word-break: break-word;
}
`;
}
diff --git a/src/panels/config/area_registry/dialog-area-registry-detail.ts b/src/panels/config/area_registry/dialog-area-registry-detail.ts
index f2703018c9..ca92374853 100644
--- a/src/panels/config/area_registry/dialog-area-registry-detail.ts
+++ b/src/panels/config/area_registry/dialog-area-registry-detail.ts
@@ -2,9 +2,9 @@ import {
LitElement,
html,
css,
- PropertyDeclarations,
CSSResult,
TemplateResult,
+ property,
} from "lit-element";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
@@ -17,19 +17,11 @@ import { HomeAssistant } from "../../../types";
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
class DialogAreaDetail extends LitElement {
- public hass!: HomeAssistant;
- private _name!: string;
- private _error?: string;
- private _params?: AreaRegistryDetailDialogParams;
- private _submitting?: boolean;
-
- static get properties(): PropertyDeclarations {
- return {
- _error: {},
- _name: {},
- _params: {},
- };
- }
+ @property() public hass!: HomeAssistant;
+ @property() private _name!: string;
+ @property() private _error?: string;
+ @property() private _params?: AreaRegistryDetailDialogParams;
+ @property() private _submitting?: boolean;
public async showDialog(
params: AreaRegistryDetailDialogParams
diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts
index 7054ad5365..5cc7b8abc7 100644
--- a/src/panels/config/automation/ha-automation-editor.ts
+++ b/src/panels/config/automation/ha-automation-editor.ts
@@ -4,8 +4,8 @@ import {
html,
CSSResult,
css,
- PropertyDeclarations,
PropertyValues,
+ property,
} from "lit-element";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
@@ -38,26 +38,14 @@ function AutomationEditor(mountEl, props, mergeEl) {
}
export class HaAutomationEditor extends LitElement {
- public hass!: HomeAssistant;
- public automation!: AutomationEntity;
- public isWide?: boolean;
- public creatingNew?: boolean;
- private _config?: AutomationConfig;
- private _dirty?: boolean;
+ @property() public hass!: HomeAssistant;
+ @property() public automation!: AutomationEntity;
+ @property() public isWide?: boolean;
+ @property() public creatingNew?: boolean;
+ @property() private _config?: AutomationConfig;
+ @property() private _dirty?: boolean;
private _rendered?: unknown;
- private _errors?: string;
-
- static get properties(): PropertyDeclarations {
- return {
- hass: {},
- automation: {},
- creatingNew: {},
- isWide: {},
- _errors: {},
- _dirty: {},
- _config: {},
- };
- }
+ @property() private _errors?: string;
constructor() {
super();
diff --git a/src/panels/config/cloud/account/cloud-google-pref.ts b/src/panels/config/cloud/account/cloud-google-pref.ts
index 6c9ece442d..dd25e9efe9 100644
--- a/src/panels/config/cloud/account/cloud-google-pref.ts
+++ b/src/panels/config/cloud/account/cloud-google-pref.ts
@@ -1,10 +1,10 @@
import {
html,
LitElement,
- PropertyDeclarations,
TemplateResult,
CSSResult,
css,
+ property,
} from "lit-element";
import "@material/mwc-button";
import "../../../../components/buttons/ha-call-api-button";
@@ -21,15 +21,8 @@ import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
export class CloudGooglePref extends LitElement {
- public hass?: HomeAssistant;
- public cloudStatus?: CloudStatusLoggedIn;
-
- static get properties(): PropertyDeclarations {
- return {
- hass: {},
- cloudStatus: {},
- };
- }
+ @property() public hass?: HomeAssistant;
+ @property() public cloudStatus?: CloudStatusLoggedIn;
protected render(): TemplateResult | void {
if (!this.cloudStatus) {
diff --git a/src/panels/config/cloud/account/cloud-remote-pref.ts b/src/panels/config/cloud/account/cloud-remote-pref.ts
index 35f309c016..1c701487ff 100644
--- a/src/panels/config/cloud/account/cloud-remote-pref.ts
+++ b/src/panels/config/cloud/account/cloud-remote-pref.ts
@@ -1,11 +1,11 @@
import {
html,
LitElement,
- PropertyDeclarations,
TemplateResult,
customElement,
CSSResult,
css,
+ property,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-item/paper-item-body";
@@ -26,15 +26,8 @@ import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dia
@customElement("cloud-remote-pref")
export class CloudRemotePref extends LitElement {
- public hass?: HomeAssistant;
- public cloudStatus?: CloudStatusLoggedIn;
-
- static get properties(): PropertyDeclarations {
- return {
- hass: {},
- cloudStatus: {},
- };
- }
+ @property() public hass?: HomeAssistant;
+ @property() public cloudStatus?: CloudStatusLoggedIn;
protected render(): TemplateResult | void {
if (!this.cloudStatus) {
diff --git a/src/panels/config/cloud/account/cloud-webhooks.ts b/src/panels/config/cloud/account/cloud-webhooks.ts
index 9f2b912283..af8a63d794 100644
--- a/src/panels/config/cloud/account/cloud-webhooks.ts
+++ b/src/panels/config/cloud/account/cloud-webhooks.ts
@@ -1,9 +1,4 @@
-import {
- html,
- LitElement,
- PropertyDeclarations,
- PropertyValues,
-} from "lit-element";
+import { html, LitElement, PropertyValues, property } from "lit-element";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-spinner/paper-spinner";
@@ -22,21 +17,11 @@ import {
import { showManageCloudhookDialog } from "../dialog-manage-cloudhook/show-dialog-manage-cloudhook";
export class CloudWebhooks extends LitElement {
- public hass?: HomeAssistant;
- public cloudStatus?: CloudStatusLoggedIn;
- private _cloudHooks?: { [webhookId: string]: CloudWebhook };
- private _localHooks?: Webhook[];
- private _progress: string[];
-
- static get properties(): PropertyDeclarations {
- return {
- hass: {},
- cloudStatus: {},
- _cloudHooks: {},
- _localHooks: {},
- _progress: {},
- };
- }
+ @property() public hass?: HomeAssistant;
+ @property() public cloudStatus?: CloudStatusLoggedIn;
+ @property() private _cloudHooks?: { [webhookId: string]: CloudWebhook };
+ @property() private _localHooks?: Webhook[];
+ @property() private _progress: string[];
constructor() {
super();
diff --git a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts
index 3ecdb04683..39628e44e9 100644
--- a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts
+++ b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts
@@ -1,10 +1,4 @@
-import {
- html,
- LitElement,
- PropertyDeclarations,
- css,
- CSSResult,
-} from "lit-element";
+import { html, LitElement, css, CSSResult, property } from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
@@ -24,13 +18,7 @@ const inputLabel = "Public URL – Click to copy to clipboard";
export class DialogManageCloudhook extends LitElement {
protected hass?: HomeAssistant;
- private _params?: WebhookDialogParams;
-
- static get properties(): PropertyDeclarations {
- return {
- _params: {},
- };
- }
+ @property() private _params?: WebhookDialogParams;
public async showDialog(params: WebhookDialogParams) {
this._params = params;
diff --git a/src/panels/config/customize/ha-form-customize.js b/src/panels/config/customize/ha-form-customize.js
index 17799f8415..7b1bca7a49 100644
--- a/src/panels/config/customize/ha-form-customize.js
+++ b/src/panels/config/customize/ha-form-customize.js
@@ -26,9 +26,14 @@ class HaFormCustomize extends PolymerElement {
if="[[computeShowWarning(localConfig, globalConfig)]]"
>
- It seems that your configuration.yaml doesn't properly include
- customize.yaml
- Changes made here won't affect your configuration.
+ It seems that your configuration.yaml doesn't properly
+
include customize.yaml .
+ Changes made here are written in it, but will not be applied after a
+ configuration reload unless the include is in place.
diff --git a/src/panels/config/devices/device-detail/ha-device-card.js b/src/panels/config/devices/device-detail/ha-device-card.js
deleted file mode 100644
index 94ee330e7e..0000000000
--- a/src/panels/config/devices/device-detail/ha-device-card.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import "../../../../components/ha-card";
-
-import { EventsMixin } from "../../../../mixins/events-mixin";
-import LocalizeMixin from "../../../../mixins/localize-mixin";
-import { compare } from "../../../../common/string/compare";
-import { updateDeviceRegistryEntry } from "../../../../data/device_registry";
-import {
- loadDeviceRegistryDetailDialog,
- showDeviceRegistryDetailDialog,
-} from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
-
-/*
- * @appliesMixin EventsMixin
- */
-class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
- static get template() {
- return html`
-
-
-
-
-
[[device.model]]
-
- [[localize('ui.panel.config.integrations.config_entry.manuf',
- 'manufacturer', device.manufacturer)]]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- device: Object,
- devices: Array,
- areas: Array,
- hass: Object,
- narrow: {
- type: Boolean,
- reflectToAttribute: true,
- },
- _childDevices: {
- type: Array,
- computed: "_computeChildDevices(device, devices)",
- },
- };
- }
-
- firstUpdated(changedProps) {
- super.firstUpdated(changedProps);
- loadDeviceRegistryDetailDialog();
- }
-
- _computeArea(areas, device) {
- if (!areas || !device || !device.area_id) {
- return "No Area";
- }
- // +1 because of "No Area" entry
- return areas.find((area) => area.area_id === device.area_id).name;
- }
-
- _computeChildDevices(device, devices) {
- return devices
- .filter((dev) => dev.via_device_id === device.id)
- .sort((dev1, dev2) => compare(dev1.name, dev2.name));
- }
-
- _deviceName(device) {
- return device.name_by_user || device.name;
- }
-
- _computeDeviceName(devices, deviceId) {
- const device = devices.find((dev) => dev.id === deviceId);
- return device
- ? this._deviceName(device)
- : `(${this.localize(
- "ui.panel.config.integrations.config_entry.device_unavailable"
- )})`;
- }
-
- _gotoSettings() {
- const device = this.device;
- showDeviceRegistryDetailDialog(this, {
- device,
- updateEntry: async (updates) => {
- await updateDeviceRegistryEntry(this.hass, device.id, updates);
- },
- });
- }
-
- _openMoreInfo(ev) {
- this.fire("hass-more-info", { entityId: ev.model.entity.entity_id });
- }
-}
-
-customElements.define("ha-device-card", HaDeviceCard);
diff --git a/src/panels/config/devices/device-detail/ha-device-card.ts b/src/panels/config/devices/device-detail/ha-device-card.ts
new file mode 100644
index 0000000000..86e3109793
--- /dev/null
+++ b/src/panels/config/devices/device-detail/ha-device-card.ts
@@ -0,0 +1,132 @@
+import "../../../../components/ha-card";
+
+import { DeviceRegistryEntry } from "../../../../data/device_registry";
+import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
+import {
+ LitElement,
+ html,
+ customElement,
+ property,
+ TemplateResult,
+ CSSResult,
+ css,
+} from "lit-element";
+import { HomeAssistant } from "../../../../types";
+import { AreaRegistryEntry } from "../../../../data/area_registry";
+
+@customElement("ha-device-card")
+export class HaDeviceCard extends LitElement {
+ @property() public hass!: HomeAssistant;
+ @property() public device!: DeviceRegistryEntry;
+ @property() public devices!: DeviceRegistryEntry[];
+ @property() public areas!: AreaRegistryEntry[];
+ @property() public narrow!: boolean;
+
+ protected render(): TemplateResult {
+ return html`
+
+
+
+
${this.device.model}
+
+ ${this.hass.localize(
+ "ui.panel.config.integrations.config_entry.manuf",
+ "manufacturer",
+ this.device.manufacturer
+ )}
+
+ ${this.device.area_id
+ ? html`
+
+
+
+ `
+ : ""}
+
+ ${this.device.via_device_id
+ ? html`
+
+ `
+ : ""}
+ ${this.device.sw_version
+ ? html`
+
+ `
+ : ""}
+
+
+ `;
+ }
+
+ protected firstUpdated(changedProps) {
+ super.firstUpdated(changedProps);
+ loadDeviceRegistryDetailDialog();
+ }
+
+ private _computeArea(areas, device) {
+ if (!areas || !device || !device.area_id) {
+ return "No Area";
+ }
+ // +1 because of "No Area" entry
+ return areas.find((area) => area.area_id === device.area_id).name;
+ }
+
+ private _deviceName(device) {
+ return device.name_by_user || device.name;
+ }
+
+ private _computeDeviceName(devices, deviceId) {
+ const device = devices.find((dev) => dev.id === deviceId);
+ return device
+ ? this._deviceName(device)
+ : `(${this.hass.localize(
+ "ui.panel.config.integrations.config_entry.device_unavailable"
+ )})`;
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ ha-card {
+ flex: 1 0 100%;
+ padding-bottom: 10px;
+ min-width: 0;
+ }
+ .device {
+ width: 30%;
+ }
+ .area {
+ color: var(--primary-text-color);
+ }
+ .extra-info {
+ margin-top: 8px;
+ }
+ .manuf,
+ .entity-id,
+ .model {
+ color: var(--secondary-text-color);
+ }
+ `;
+ }
+}
diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts
index 17f260064b..3b19563b86 100644
--- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts
+++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts
@@ -62,6 +62,7 @@ export class HaDeviceEntitiesCard extends LitElement {
${stateObj
? html`
@@ -72,7 +73,7 @@ export class HaDeviceEntitiesCard extends LitElement {
.icon=${domainIcon(computeDomain(entry.entity_id))}
>
`}
-
+
${entry.stateName}
${entry.entity_id}
@@ -81,7 +82,7 @@ export class HaDeviceEntitiesCard extends LitElement {
? html`
`
: ""}
@@ -139,6 +140,12 @@ export class HaDeviceEntitiesCard extends LitElement {
.disabled-entry {
color: var(--secondary-text-color);
}
+ state-badge {
+ cursor: pointer;
+ }
+ paper-icon-item:not(.disabled-entry) paper-item-body {
+ cursor: pointer;
+ }
`;
}
}
diff --git a/src/panels/config/entity_registry/dialog-entity-registry-detail.ts b/src/panels/config/entity_registry/dialog-entity-registry-detail.ts
index f29d629909..29d8d9d78a 100644
--- a/src/panels/config/entity_registry/dialog-entity-registry-detail.ts
+++ b/src/panels/config/entity_registry/dialog-entity-registry-detail.ts
@@ -26,6 +26,7 @@ import {
updateEntityRegistryEntry,
removeEntityRegistryEntry,
} from "../../../data/entity_registry";
+import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
class DialogEntityRegistryDetail extends LitElement {
@property() public hass!: HomeAssistant;
@@ -139,7 +140,7 @@ class DialogEntityRegistryDetail extends LitElement {
@@ -84,7 +84,7 @@ export default class DeviceTrigger extends Component {
}
public componentDidUpdate(prevProps) {
- if (prevProps.trigger !== this.props.trigger) {
+ if (!deviceAutomationsEqual(prevProps.trigger, this.props.trigger)) {
this._getCapabilities();
}
}
@@ -99,15 +99,9 @@ export default class DeviceTrigger extends Component {
}
private _extraFieldsChanged(ev) {
- if (!ev.detail.path) {
- return;
- }
- const item = ev.detail.path.replace("data.", "");
- const value = ev.detail.value || undefined;
-
this.props.onChange(this.props.index, {
...this.props.trigger,
- [item]: value,
+ ...ev.detail.value,
});
}
diff --git a/src/panels/config/js/yaml_textarea.tsx b/src/panels/config/js/yaml_textarea.tsx
index 5050576db2..0b8115e7d3 100644
--- a/src/panels/config/js/yaml_textarea.tsx
+++ b/src/panels/config/js/yaml_textarea.tsx
@@ -1,5 +1,5 @@
import { h, Component } from "preact";
-import yaml from "js-yaml";
+import { safeDump, safeLoad } from "js-yaml";
import "../../../components/ha-code-editor";
const isEmpty = (obj: object) => {
@@ -19,7 +19,7 @@ export default class YAMLTextArea extends Component {
try {
value =
props.value && !isEmpty(props.value)
- ? yaml.safeDump(props.value)
+ ? safeDump(props.value)
: undefined;
} catch (err) {
alert(`There was an error converting to YAML: ${err}`);
@@ -40,7 +40,7 @@ export default class YAMLTextArea extends Component {
if (value) {
try {
- parsed = yaml.safeLoad(value);
+ parsed = safeLoad(value);
isValid = true;
} catch (err) {
// Invalid YAML
diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts
index 46958c4fd6..fbdcc0fcd3 100644
--- a/src/panels/config/person/ha-config-person.ts
+++ b/src/panels/config/person/ha-config-person.ts
@@ -4,7 +4,7 @@ import {
html,
css,
CSSResult,
- PropertyDeclarations,
+ property,
} from "lit-element";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
@@ -30,21 +30,12 @@ import {
import { User, fetchUsers } from "../../../data/user";
class HaConfigPerson extends LitElement {
- public hass?: HomeAssistant;
- public isWide?: boolean;
- private _storageItems?: Person[];
- private _configItems?: Person[];
+ @property() public hass?: HomeAssistant;
+ @property() public isWide?: boolean;
+ @property() private _storageItems?: Person[];
+ @property() private _configItems?: Person[];
private _usersLoad?: Promise;
- static get properties(): PropertyDeclarations {
- return {
- hass: {},
- isWide: {},
- _storageItems: {},
- _configItems: {},
- };
- }
-
protected render(): TemplateResult | void {
if (
!this.hass ||
diff --git a/src/panels/config/zha/zha-cluster-attributes.ts b/src/panels/config/zha/zha-cluster-attributes.ts
index c7b310eb1f..c438ca8200 100644
--- a/src/panels/config/zha/zha-cluster-attributes.ts
+++ b/src/panels/config/zha/zha-cluster-attributes.ts
@@ -14,9 +14,9 @@ import {
CSSResult,
html,
LitElement,
- PropertyDeclarations,
PropertyValues,
TemplateResult,
+ property,
} from "lit-element";
import {
@@ -37,39 +37,16 @@ import {
} from "./types";
export class ZHAClusterAttributes extends LitElement {
- public hass?: HomeAssistant;
- public isWide?: boolean;
- public showHelp: boolean;
- public selectedNode?: ZHADevice;
- public selectedCluster?: Cluster;
- private _attributes: Attribute[];
- private _selectedAttributeIndex: number;
- private _attributeValue?: any;
- private _manufacturerCodeOverride?: string | number;
- private _setAttributeServiceData?: SetAttributeServiceData;
-
- constructor() {
- super();
- this.showHelp = false;
- this._selectedAttributeIndex = -1;
- this._attributes = [];
- this._attributeValue = "";
- }
-
- static get properties(): PropertyDeclarations {
- return {
- hass: {},
- isWide: {},
- showHelp: {},
- selectedNode: {},
- selectedCluster: {},
- _attributes: {},
- _selectedAttributeIndex: {},
- _attributeValue: {},
- _manufacturerCodeOverride: {},
- _setAttributeServiceData: {},
- };
- }
+ @property() public hass?: HomeAssistant;
+ @property() public isWide?: boolean;
+ @property() public showHelp = false;
+ @property() public selectedNode?: ZHADevice;
+ @property() public selectedCluster?: Cluster;
+ @property() private _attributes: Attribute[] = [];
+ @property() private _selectedAttributeIndex = -1;
+ @property() private _attributeValue?: any = "";
+ @property() private _manufacturerCodeOverride?: string | number;
+ @property() private _setAttributeServiceData?: SetAttributeServiceData;
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedCluster")) {
diff --git a/src/panels/config/zha/zha-cluster-commands.ts b/src/panels/config/zha/zha-cluster-commands.ts
index fd2d4d2041..881ee6681c 100644
--- a/src/panels/config/zha/zha-cluster-commands.ts
+++ b/src/panels/config/zha/zha-cluster-commands.ts
@@ -13,9 +13,9 @@ import {
CSSResult,
html,
LitElement,
- PropertyDeclarations,
PropertyValues,
TemplateResult,
+ property,
} from "lit-element";
import {
@@ -34,36 +34,15 @@ import {
} from "./types";
export class ZHAClusterCommands extends LitElement {
- public hass?: HomeAssistant;
- public isWide?: boolean;
- public selectedNode?: ZHADevice;
- public selectedCluster?: Cluster;
- private _showHelp: boolean;
- private _commands: Command[];
- private _selectedCommandIndex: number;
- private _manufacturerCodeOverride?: number;
- private _issueClusterCommandServiceData?: IssueCommandServiceData;
-
- constructor() {
- super();
- this._showHelp = false;
- this._selectedCommandIndex = -1;
- this._commands = [];
- }
-
- static get properties(): PropertyDeclarations {
- return {
- hass: {},
- isWide: {},
- selectedNode: {},
- selectedCluster: {},
- _showHelp: {},
- _commands: {},
- _selectedCommandIndex: {},
- _manufacturerCodeOverride: {},
- _issueClusterCommandServiceData: {},
- };
- }
+ @property() public hass?: HomeAssistant;
+ @property() public isWide?: boolean;
+ @property() public selectedNode?: ZHADevice;
+ @property() public selectedCluster?: Cluster;
+ @property() private _showHelp = false;
+ @property() private _commands: Command[] = [];
+ @property() private _selectedCommandIndex = -1;
+ @property() private _manufacturerCodeOverride?: number;
+ @property() private _issueClusterCommandServiceData?: IssueCommandServiceData;
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedCluster")) {
diff --git a/src/panels/config/zha/zha-clusters.ts b/src/panels/config/zha/zha-clusters.ts
index d1e0ec8c2d..b5baba730f 100644
--- a/src/panels/config/zha/zha-clusters.ts
+++ b/src/panels/config/zha/zha-clusters.ts
@@ -11,9 +11,9 @@ import {
CSSResult,
html,
LitElement,
- PropertyDeclarations,
PropertyValues,
TemplateResult,
+ property,
} from "lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -39,30 +39,12 @@ const computeClusterKey = (cluster: Cluster): string => {
};
export class ZHAClusters extends LitElement {
- public hass?: HomeAssistant;
- public isWide?: boolean;
- public showHelp: boolean;
- public selectedDevice?: ZHADevice;
- private _selectedClusterIndex: number;
- private _clusters: Cluster[];
-
- constructor() {
- super();
- this.showHelp = false;
- this._selectedClusterIndex = -1;
- this._clusters = [];
- }
-
- static get properties(): PropertyDeclarations {
- return {
- hass: {},
- isWide: {},
- showHelp: {},
- selectedDevice: {},
- _selectedClusterIndex: {},
- _clusters: {},
- };
- }
+ @property() public hass?: HomeAssistant;
+ @property() public isWide?: boolean;
+ @property() public showHelp = false;
+ @property() public selectedDevice?: ZHADevice;
+ @property() private _selectedClusterIndex = -1;
+ @property() private _clusters: Cluster[] = [];
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedDevice")) {
diff --git a/src/panels/config/zha/zha-network.ts b/src/panels/config/zha/zha-network.ts
index a8c5761aef..78ee69857b 100644
--- a/src/panels/config/zha/zha-network.ts
+++ b/src/panels/config/zha/zha-network.ts
@@ -10,8 +10,8 @@ import {
CSSResult,
html,
LitElement,
- PropertyDeclarations,
TemplateResult,
+ property,
} from "lit-element";
import { navigate } from "../../../common/navigate";
@@ -19,23 +19,9 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
export class ZHANetwork extends LitElement {
- public hass?: HomeAssistant;
- public isWide?: boolean;
- private _showHelp: boolean;
-
- constructor() {
- super();
- this._showHelp = false;
- }
-
- static get properties(): PropertyDeclarations {
- return {
- hass: {},
- isWide: {},
- _showHelp: {},
- _joinParams: {},
- };
- }
+ @property() public hass?: HomeAssistant;
+ @property() public isWide?: boolean;
+ @property() private _showHelp = false;
protected render(): TemplateResult | void {
return html`
diff --git a/src/panels/developer-tools/event/developer-tools-event.js b/src/panels/developer-tools/event/developer-tools-event.js
index c1b50bd068..074fcb6851 100644
--- a/src/panels/developer-tools/event/developer-tools-event.js
+++ b/src/panels/developer-tools/event/developer-tools-event.js
@@ -4,19 +4,21 @@ import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
-import yaml from "js-yaml";
+import { safeLoad } from "js-yaml";
import "../../../components/ha-code-editor";
import "../../../resources/ha-style";
import "./events-list";
import "./event-subscribe-card";
import { EventsMixin } from "../../../mixins/events-mixin";
+import LocalizeMixin from "../../../mixins/localize-mixin";
const ERROR_SENTINEL = {};
/*
* @appliesMixin EventsMixin
+ * @appliesMixin LocalizeMixin
*/
-class HaPanelDevEvent extends EventsMixin(PolymerElement) {
+class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
@@ -54,21 +56,26 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
- Fire an event on the event bus.
+ [[localize( 'ui.panel.developer-tools.tabs.events.description' )]]
Events Documentation. [[localize( 'ui.panel.developer-tools.tabs.events.documentation'
+ )]]
-
+
+
${this._events.map(
(ev) => html`
- Event ${ev.id} fired
+ ${this.hass!.localize(
+ "ui.panel.developer-tools.tabs.events.event_fired",
+ "name",
+ ev.id
+ )}
${format_time(
new Date(ev.event.time_fired),
this.hass!.language
diff --git a/src/panels/developer-tools/event/events-list.js b/src/panels/developer-tools/event/events-list.js
index 3ffba2e5f5..e2435c0dab 100644
--- a/src/panels/developer-tools/event/events-list.js
+++ b/src/panels/developer-tools/event/events-list.js
@@ -2,11 +2,13 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../../../mixins/events-mixin";
+import LocalizeMixin from "../../../mixins/localize-mixin";
/*
* @appliesMixin EventsMixin
+ * @appliesMixin LocalizeMixin
*/
-class EventsList extends EventsMixin(PolymerElement) {
+class EventsList extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
@@ -60,26 +61,25 @@ class HaPanelDevTemplate extends PolymerElement {
- Templates are rendered using the Jinja2 template engine with some
- Home Assistant specific extensions.
+ [[localize('ui.panel.developer-tools.tabs.templates.description')]]
-
Template editor
+
[[localize('ui.panel.developer-tools.tabs.templates.editor')]]
+ typeof entity === "object" &&
+ entity.state_filter &&
+ Array.isArray(entity.state_filter)
+ )
+ ) {
+ throw new Error("Incorrect filter config.");
+ }
+
+ this._config = config;
+ this._configEntities = undefined;
+
+ if (this.lastChild) {
+ this.removeChild(this.lastChild);
+ this._elements = undefined;
+ }
+ }
+
+ set hass(hass: HomeAssistant) {
+ if (!hass || !this._config) {
+ return;
+ }
+
+ if (this._elements) {
+ for (const element of this._elements) {
+ element.hass = hass;
+ }
+ }
+
+ if (!this.haveEntitiesChanged(hass)) {
+ this._hass = hass;
+ return;
+ }
+
+ this._hass = hass;
+
+ if (!this._configEntities) {
+ this._configEntities = processConfigEntities(this._config.entities);
+ }
+
+ const entitiesList = this._configEntities.filter((entityConf) => {
+ const stateObj = hass.states[entityConf.entity];
+
+ if (!stateObj) {
+ return false;
+ }
+
+ if (entityConf.state_filter) {
+ for (const filter of entityConf.state_filter) {
+ if (evaluateFilter(stateObj, filter)) {
+ return true;
+ }
+ }
+ } else {
+ for (const filter of this._config!.state_filter) {
+ if (evaluateFilter(stateObj, filter)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ });
+
+ if (entitiesList.length === 0) {
+ this.style.display = "none";
+ return;
+ }
+
+ const isSame =
+ this._oldEntities &&
+ entitiesList.length === this._oldEntities.length &&
+ entitiesList.every((entity, idx) => entity === this._oldEntities![idx]);
+
+ if (!isSame) {
+ this._elements = [];
+ for (const badgeConfig of entitiesList) {
+ const element = createBadgeElement(badgeConfig);
+ element.hass = hass;
+ this._elements.push(element);
+ }
+ this._oldEntities = entitiesList;
+ }
+
+ if (!this._elements) {
+ return;
+ }
+
+ // Attach element if it has never been attached.
+ if (!this.lastChild) {
+ for (const element of this._elements) {
+ this.appendChild(element);
+ }
+ }
+
+ this.style.display = "inline";
+ }
+
+ private haveEntitiesChanged(hass: HomeAssistant): boolean {
+ if (!this._hass) {
+ return true;
+ }
+
+ if (!this._configEntities || this._hass.localize !== hass.localize) {
+ return true;
+ }
+
+ for (const config of this._configEntities) {
+ if (this._hass.states[config.entity] !== hass.states[config.entity]) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+customElements.define("hui-entity-filter-badge", EntityFilterBadge);
diff --git a/src/panels/lovelace/badges/hui-error-badge.ts b/src/panels/lovelace/badges/hui-error-badge.ts
new file mode 100644
index 0000000000..9c20ceff39
--- /dev/null
+++ b/src/panels/lovelace/badges/hui-error-badge.ts
@@ -0,0 +1,65 @@
+import {
+ html,
+ LitElement,
+ TemplateResult,
+ customElement,
+ property,
+ css,
+ CSSResult,
+} from "lit-element";
+
+import { LovelaceBadge } from "../types";
+import { HomeAssistant } from "../../../types";
+import { ErrorBadgeConfig } from "./types";
+
+import "../../../components/ha-label-badge";
+
+export const createErrorBadgeElement = (config) => {
+ const el = document.createElement("hui-error-badge");
+ el.setConfig(config);
+ return el;
+};
+
+export const createErrorBadgeConfig = (error) => ({
+ type: "error",
+ error,
+});
+
+@customElement("hui-error-badge")
+export class HuiErrorBadge extends LitElement implements LovelaceBadge {
+ public hass?: HomeAssistant;
+
+ @property() private _config?: ErrorBadgeConfig;
+
+ public setConfig(config: ErrorBadgeConfig): void {
+ this._config = config;
+ }
+
+ protected render(): TemplateResult | void {
+ if (!this._config) {
+ return html``;
+ }
+
+ return html`
+
+ `;
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ :host {
+ --ha-label-badge-color: var(--label-badge-red, #fce588);
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-error-badge": HuiErrorBadge;
+ }
+}
diff --git a/src/panels/lovelace/badges/hui-state-label-badge.ts b/src/panels/lovelace/badges/hui-state-label-badge.ts
new file mode 100644
index 0000000000..d0523880a0
--- /dev/null
+++ b/src/panels/lovelace/badges/hui-state-label-badge.ts
@@ -0,0 +1,74 @@
+import {
+ html,
+ LitElement,
+ TemplateResult,
+ customElement,
+ property,
+} from "lit-element";
+
+import "../../../components/entity/ha-state-label-badge";
+import "../components/hui-warning-element";
+
+import { LovelaceBadge } from "../types";
+import { HomeAssistant } from "../../../types";
+import { computeStateName } from "../../../common/entity/compute_state_name";
+import { StateLabelBadgeConfig } from "./types";
+import { longPress } from "../common/directives/long-press-directive";
+import { hasDoubleClick } from "../common/has-double-click";
+import { handleClick } from "../common/handle-click";
+
+@customElement("hui-state-label-badge")
+export class HuiStateLabelBadge extends LitElement implements LovelaceBadge {
+ @property() public hass?: HomeAssistant;
+ @property() protected _config?: StateLabelBadgeConfig;
+
+ public setConfig(config: StateLabelBadgeConfig): void {
+ this._config = config;
+ }
+
+ protected render(): TemplateResult | void {
+ if (!this._config || !this.hass) {
+ return html``;
+ }
+
+ const stateObj = this.hass.states[this._config.entity!];
+
+ return html`
+
+ `;
+ }
+
+ private _handleClick() {
+ handleClick(this, this.hass!, this._config!, false, false);
+ }
+
+ private _handleHold() {
+ handleClick(this, this.hass!, this._config!, true, false);
+ }
+
+ private _handleDblClick() {
+ handleClick(this, this.hass!, this._config!, false, true);
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-state-label-badge": HuiStateLabelBadge;
+ }
+}
diff --git a/src/panels/lovelace/badges/types.ts b/src/panels/lovelace/badges/types.ts
new file mode 100644
index 0000000000..7b28dae4fb
--- /dev/null
+++ b/src/panels/lovelace/badges/types.ts
@@ -0,0 +1,22 @@
+import { LovelaceBadgeConfig, ActionConfig } from "../../../data/lovelace";
+import { EntityFilterEntityConfig } from "../entity-rows/types";
+
+export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig {
+ type: "entity-filter";
+ entities: Array;
+ state_filter: Array<{ key: string } | string>;
+}
+
+export interface ErrorBadgeConfig extends LovelaceBadgeConfig {
+ error: string;
+}
+
+export interface StateLabelBadgeConfig extends LovelaceBadgeConfig {
+ entity: string;
+ name?: string;
+ icon?: string;
+ image?: string;
+ tap_action?: ActionConfig;
+ hold_action?: ActionConfig;
+ double_tap_action?: ActionConfig;
+}
diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts
index e8a9fccc80..c9f85e20ec 100644
--- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts
+++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts
@@ -22,6 +22,7 @@ import {
} from "../../../data/alarm_control_panel";
import { AlarmPanelCardConfig } from "./types";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
+import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
const ICONS = {
armed_away: "hass:shield-lock",
@@ -81,19 +82,44 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
this._code = "";
}
+ protected updated(changedProps: PropertyValues): void {
+ super.updated(changedProps);
+ if (!this._config || !this.hass) {
+ return;
+ }
+ const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
+ const oldConfig = changedProps.get("_config") as
+ | AlarmPanelCardConfig
+ | undefined;
+
+ if (
+ !oldHass ||
+ !oldConfig ||
+ oldHass.themes !== this.hass.themes ||
+ oldConfig.theme !== this._config.theme
+ ) {
+ applyThemesOnElement(this, this.hass.themes, this._config.theme);
+ }
+ }
+
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_config") || changedProps.has("_code")) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
- if (oldHass) {
- return (
- oldHass.states[this._config!.entity] !==
- this.hass!.states[this._config!.entity]
- );
+
+ if (
+ !oldHass ||
+ oldHass.themes !== this.hass!.themes ||
+ oldHass.language !== this.hass!.language
+ ) {
+ return true;
}
- return true;
+ return (
+ oldHass.states[this._config!.entity] !==
+ this.hass!.states[this._config!.entity]
+ );
}
protected render(): TemplateResult | void {
@@ -164,7 +190,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
${value === "clear"
? this._label("clear_code")
@@ -226,8 +252,6 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
--alarm-color-armed: var(--label-badge-red);
--alarm-color-autoarm: rgba(0, 153, 255, 0.1);
--alarm-state-color: var(--alarm-color-armed);
- --base-unit: 15px;
- font-size: calc(var(--base-unit));
}
ha-label-badge {
@@ -271,13 +295,11 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
paper-input {
margin: 0 auto 8px;
max-width: 150px;
- font-size: calc(var(--base-unit));
text-align: center;
}
.state {
margin-left: 16px;
- font-size: calc(var(--base-unit) * 0.9);
position: relative;
bottom: 16px;
color: var(--alarm-state-color);
@@ -289,14 +311,14 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
justify-content: center;
flex-wrap: wrap;
margin: auto;
- width: 300px;
+ width: 100%;
+ max-width: 300px;
}
#keypad mwc-button {
- margin-bottom: 5%;
+ text-size: 20px;
+ padding: 8px;
width: 30%;
- padding: calc(var(--base-unit));
- font-size: calc(var(--base-unit) * 1.1);
box-sizing: border-box;
}
@@ -306,11 +328,9 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
display: flex;
flex-wrap: wrap;
justify-content: center;
- font-size: calc(var(--base-unit) * 1);
}
.actions mwc-button {
- min-width: calc(var(--base-unit) * 9);
margin: 0 4px 4px;
}
diff --git a/src/panels/lovelace/cards/hui-entities-card.ts b/src/panels/lovelace/cards/hui-entities-card.ts
index 15f5762029..3b7d1de39e 100644
--- a/src/panels/lovelace/cards/hui-entities-card.ts
+++ b/src/panels/lovelace/cards/hui-entities-card.ts
@@ -12,17 +12,13 @@ import {
import "../../../components/ha-card";
import "../components/hui-entities-toggle";
-import { fireEvent } from "../../../common/dom/fire_event";
-import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { HomeAssistant } from "../../../types";
import { EntityRow } from "../entity-rows/types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { processConfigEntities } from "../common/process-config-entities";
import { createRowElement } from "../common/create-row-element";
import { EntitiesCardConfig, EntitiesCardEntityConfig } from "./types";
-
-import { computeDomain } from "../../../common/entity/compute_domain";
-import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
+import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
@customElement("hui-entities-card")
class HuiEntitiesCard extends LitElement implements LovelaceCard {
@@ -71,9 +67,22 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
this._configEntities = entities;
}
- protected updated(changedProperties: PropertyValues): void {
- super.updated(changedProperties);
- if (this._hass && this._config) {
+ protected updated(changedProps: PropertyValues): void {
+ super.updated(changedProps);
+ if (!this._config || !this._hass) {
+ return;
+ }
+ const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
+ const oldConfig = changedProps.get("_config") as
+ | EntitiesCardConfig
+ | undefined;
+
+ if (
+ !oldHass ||
+ !oldConfig ||
+ oldHass.themes !== this.hass.themes ||
+ oldConfig.theme !== this._config.theme
+ ) {
applyThemesOnElement(this, this._hass.themes, this._config.theme);
}
}
@@ -82,16 +91,27 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
if (!this._config || !this._hass) {
return html``;
}
- const { show_header_toggle, title } = this._config;
return html`
- ${!title && !show_header_toggle
+ ${!this._config.title &&
+ !this._config.show_header_toggle &&
+ !this._config.icon
? html``
: html`