Merge pull request #4117 from home-assistant/dev

20191023.0
This commit is contained in:
Bram Kragten 2019-10-23 21:36:21 +02:00 committed by GitHub
commit ad8f049570
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
246 changed files with 12813 additions and 3746 deletions

View File

@ -1,10 +1,24 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: ""
---
<!-- READ THIS FIRST: <!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/ - If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases - Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- Provide as many details as possible. Do not delete any text from this template! - Provide as many details as possible. Do not delete any text from this template!
--> -->
**Checklist:**
- [ ] I updated to the latest version available
- [ ] I cleared the cache of my browser
**Home Assistant release with the issue:** **Home Assistant release with the issue:**
<!-- <!--
- Frontend -> Developer tools -> Info - Frontend -> Developer tools -> Info
- Or use this command: hass --version - Or use this command: hass --version
@ -13,22 +27,25 @@
**Last working Home Assistant release (if known):** **Last working Home Assistant release (if known):**
**UI (States or Lovelace UI?):** **UI (States or Lovelace UI?):**
<!-- <!--
- Frontend -> Developer tools -> Info - Frontend -> Developer tools -> Info
--> -->
**Browser and Operating System:** **Browser and Operating System:**
<!-- <!--
Provide details about what browser (and version) you are seeing the issue in. And also which operating system this is on. If possible try to replicate the issue in other browsers and include your findings here. Provide details about what browser (and version) you are seeing the issue in. And also which operating system this is on. If possible try to replicate the issue in other browsers and include your findings here.
--> -->
**Description of problem:** **Description of problem:**
<!-- <!--
Explain what the issue is, and how things should look/behave. If possible provide a screenshot with a description. Explain what the issue is, and how things should look/behave. If possible provide a screenshot with a description.
--> -->
**Javascript errors shown in the web inspector (if applicable):** **Javascript errors shown in the web inspector (if applicable):**
``` ```
``` ```

View File

@ -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.

View File

@ -8,19 +8,20 @@ install: yarn install
script: script:
- npm run build - npm run build
- hassio/script/build_hassio - hassio/script/build_hassio
# Because else eslint fails because hassio has cleaned that build
- ./node_modules/.bin/gulp gen-icons-app
- npm run test - npm run test
# - xvfb-run wct --module-resolution=node --npm # - xvfb-run wct --module-resolution=node --npm
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi' # - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
services: services:
- docker - docker
before_deploy: before_deploy:
- 'docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21' - "docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21"
deploy: deploy:
provider: script provider: script
script: script/travis_deploy script: script/travis_deploy
'on': "on":
branch: master branch: master
dist: trusty dist: trusty
addons: addons:
sauce_connect: true sauce_connect: true

View File

@ -3,7 +3,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
throw Error("latestBuild not defined for babel loader config"); throw Error("latestBuild not defined for babel loader config");
} }
return { return {
test: /\.m?js$/, test: /\.m?js$|\.tsx?$/,
use: { use: {
loader: "babel-loader", loader: "babel-loader",
options: { options: {
@ -12,6 +12,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
require("@babel/preset-env").default, require("@babel/preset-env").default,
{ modules: false }, { modules: false },
], ],
[
require("@babel/preset-typescript").default,
{
jsxPragma: "h",
},
],
].filter(Boolean), ].filter(Boolean),
plugins: [ plugins: [
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2}) // Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
@ -21,6 +27,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
], ],
// Only support the syntax, Webpack will handle it. // Only support the syntax, Webpack will handle it.
"@babel/syntax-dynamic-import", "@babel/syntax-dynamic-import",
[
"@babel/transform-react-jsx",
{
pragma: "h",
},
],
[ [
require("@babel/plugin-proposal-decorators").default, require("@babel/plugin-proposal-decorators").default,
{ decoratorsBeforeExport: true }, { decoratorsBeforeExport: true },

6
build-scripts/env.js Normal file
View File

@ -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",
};

View File

@ -1,10 +1,13 @@
// Run HA develop mode // Run HA develop mode
const gulp = require("gulp"); const gulp = require("gulp");
const envVars = require("../env");
require("./clean.js"); require("./clean.js");
require("./translations.js"); require("./translations.js");
require("./gen-icons.js"); require("./gen-icons.js");
require("./gather-static.js"); require("./gather-static.js");
require("./compress.js");
require("./webpack.js"); require("./webpack.js");
require("./service-worker.js"); require("./service-worker.js");
require("./entry-html.js"); require("./entry-html.js");
@ -18,7 +21,7 @@ gulp.task(
"clean", "clean",
gulp.parallel( gulp.parallel(
"gen-service-worker-dev", "gen-service-worker-dev",
"gen-icons", gulp.parallel("gen-icons-app", "gen-icons-mdi"),
"gen-pages-dev", "gen-pages-dev",
"gen-index-app-dev", "gen-index-app-dev",
gulp.series("create-test-translation", "build-translations") gulp.series("create-test-translation", "build-translations")
@ -35,13 +38,11 @@ gulp.task(
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
}, },
"clean", "clean",
gulp.parallel("gen-icons", "build-translations"), gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
"copy-static", "copy-static",
gulp.parallel( "webpack-prod-app",
"webpack-prod-app", ...// Don't compress running tests
// Do not compress static files in CI, it's SLOW. (envVars.isTravis ? [] : ["compress-app"]),
...(process.env.CI === "true" ? [] : ["compress-static"])
),
gulp.parallel( gulp.parallel(
"gen-pages-prod", "gen-pages-prod",
"gen-index-app-prod", "gen-index-app-prod",

View File

@ -1,4 +1,3 @@
// Run cast develop mode
const gulp = require("gulp"); const gulp = require("gulp");
require("./clean.js"); require("./clean.js");
@ -16,7 +15,12 @@ gulp.task(
process.env.NODE_ENV = "development"; process.env.NODE_ENV = "development";
}, },
"clean-cast", "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", "copy-static-cast",
"webpack-dev-server-cast" "webpack-dev-server-cast"
) )
@ -29,7 +33,7 @@ gulp.task(
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
}, },
"clean-cast", "clean-cast",
gulp.parallel("gen-icons", "build-translations"), gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
"copy-static-cast", "copy-static-cast",
"webpack-prod-cast", "webpack-prod-cast",
"gen-index-cast-prod" "gen-index-cast-prod"

View File

@ -9,15 +9,31 @@ gulp.task(
return del([config.root, config.build_dir]); return del([config.root, config.build_dir]);
}) })
); );
gulp.task( gulp.task(
"clean-demo", "clean-demo",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() { gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.demo_root, config.build_dir]); return del([config.demo_root, config.build_dir]);
}) })
); );
gulp.task( gulp.task(
"clean-cast", "clean-cast",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() { gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.cast_root, config.build_dir]); 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]);
})
);

View File

@ -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));
});

View File

@ -17,7 +17,8 @@ gulp.task(
}, },
"clean-demo", "clean-demo",
gulp.parallel( gulp.parallel(
"gen-icons", "gen-icons-app",
"gen-icons-mdi",
"gen-icons-demo", "gen-icons-demo",
"gen-index-demo-dev", "gen-index-demo-dev",
"build-translations" "build-translations"
@ -34,7 +35,12 @@ gulp.task(
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
}, },
"clean-demo", "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", "copy-static-demo",
"webpack-prod-demo", "webpack-prod-demo",
"gen-index-demo-prod" "gen-index-demo-prod"

View File

@ -11,12 +11,6 @@ const config = require("../paths.js");
const templatePath = (tpl) => const templatePath = (tpl) =>
path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`); 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 readFile = (pth) => fs.readFileSync(pth).toString();
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => { const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
@ -25,10 +19,19 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
}; };
const renderDemoTemplate = (pth, data = {}) => 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 = {}) => 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) => const minifyHtml = (content) =>
minify(content, { minify(content, {
@ -209,8 +212,33 @@ gulp.task("gen-index-demo-prod", (done) => {
es5Compatibility: es5Manifest["compatibility.js"], es5Compatibility: es5Manifest["compatibility.js"],
es5DemoJS: es5Manifest["main.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); fs.outputFileSync(path.resolve(config.demo_root, "index.html"), minified);
done(); 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();
});

View File

@ -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"
)
);

View File

@ -4,8 +4,6 @@ const gulp = require("gulp");
const path = require("path"); const path = require("path");
const cpx = require("cpx"); const cpx = require("cpx");
const fs = require("fs-extra"); const fs = require("fs-extra");
const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream");
const paths = require("../paths"); const paths = require("../paths");
const npmPath = (...parts) => 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) => { gulp.task("copy-static", (done) => {
const staticDir = paths.static; const staticDir = paths.static;
const staticPath = genStaticPath(paths.static); const staticPath = genStaticPath(paths.static);
@ -100,8 +84,6 @@ gulp.task("copy-static", (done) => {
done(); done();
}); });
gulp.task("compress-static", () => compressStatic(paths.static));
gulp.task("copy-static-demo", (done) => { gulp.task("copy-static-demo", (done) => {
// Copy app static files // Copy app static files
fs.copySync( fs.copySync(
@ -129,3 +111,15 @@ gulp.task("copy-static-cast", (done) => {
copyTranslations(paths.cast_static); copyTranslations(paths.cast_static);
done(); 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();
});

View File

@ -57,18 +57,6 @@ function generateIconset(iconsetName, iconNames) {
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`; return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
} }
// 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 // Helper function to map recursively over files in a folder and it's subfolders
function mapFiles(startPath, filter, mapFunc) { function mapFiles(startPath, filter, mapFunc) {
const files = fs.readdirSync(startPath); const files = fs.readdirSync(startPath);
@ -101,24 +89,27 @@ function findIcons(searchPath, iconsetName) {
return icons; 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"); const iconNames = findIcons("./src", "hass");
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name)); BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
if (!fs.existsSync(OUTPUT_DIR)) { if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR); fs.mkdirSync(OUTPUT_DIR);
} }
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames)); fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
}
gulp.task("gen-icons-mdi", (done) => {
genMDIIcons();
done(); 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) => { gulp.task("gen-icons-demo", (done) => {
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo"); const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
@ -129,8 +120,21 @@ gulp.task("gen-icons-demo", (done) => {
done(); done();
}); });
module.exports = { gulp.task("gen-icons-hassio", (done) => {
findIcons, const iconNames = findIcons(
generateIconset, path.resolve(paths.hassio_dir, "./src"),
genMDIIcons, "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();
});

View File

@ -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"])
)
);

View File

@ -1,6 +1,5 @@
// Tasks to run webpack. // Tasks to run webpack.
const gulp = require("gulp"); const gulp = require("gulp");
const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server"); const WebpackDevServer = require("webpack-dev-server");
const log = require("fancy-log"); const log = require("fancy-log");
@ -9,8 +8,33 @@ const {
createAppConfig, createAppConfig,
createDemoConfig, createDemoConfig,
createCastConfig, createCastConfig,
createHassioConfig,
createGalleryConfig,
} = require("../webpack"); } = 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) => { const handler = (done) => (err, stats) => {
if (err) { if (err) {
console.log(err.stack || err); console.log(err.stack || err);
@ -32,20 +56,11 @@ const handler = (done) => (err, stats) => {
}; };
gulp.task("webpack-watch-app", () => { 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 // we are not calling done, so this command will run forever
webpack(bothBuilds(createAppConfig, { isProdBuild: false })).watch(
{},
handler()
);
}); });
gulp.task( gulp.task(
@ -53,47 +68,17 @@ gulp.task(
() => () =>
new Promise((resolve) => new Promise((resolve) =>
webpack( webpack(
[ bothBuilds(createAppConfig, { isProdBuild: true }),
createAppConfig({
isProdBuild: true,
latestBuild: true,
isStatsBuild: false,
}),
createAppConfig({
isProdBuild: true,
latestBuild: false,
isStatsBuild: false,
}),
],
handler(resolve) handler(resolve)
) )
) )
); );
gulp.task("webpack-dev-server-demo", () => { gulp.task("webpack-dev-server-demo", () => {
const compiler = webpack([ runDevServer({
createDemoConfig({ compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
isProdBuild: false, contentBase: paths.demo_root,
latestBuild: false, port: 8090,
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");
}); });
}); });
@ -102,51 +87,22 @@ gulp.task(
() => () =>
new Promise((resolve) => new Promise((resolve) =>
webpack( webpack(
[ bothBuilds(createDemoConfig, {
createDemoConfig({ isProdBuild: true,
isProdBuild: true, }),
latestBuild: false,
isStatsBuild: false,
}),
createDemoConfig({
isProdBuild: true,
latestBuild: true,
isStatsBuild: false,
}),
],
handler(resolve) handler(resolve)
) )
) )
); );
gulp.task("webpack-dev-server-cast", () => { gulp.task("webpack-dev-server-cast", () => {
const compiler = webpack([ runDevServer({
createCastConfig({ compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
isProdBuild: false, contentBase: paths.cast_root,
latestBuild: false, port: 8080,
}),
createCastConfig({
isProdBuild: false,
latestBuild: true,
}),
]);
new WebpackDevServer(compiler, {
open: true,
watchContentBase: true,
contentBase: path.resolve(paths.cast_dir, "dist"),
}).listen(
8080,
// Accessible from the network, because that's how Cast hits it. // Accessible from the network, because that's how Cast hits it.
"0.0.0.0", listenHost: "0.0.0.0",
function(err) { });
if (err) {
throw err;
}
// Server listening
log("[webpack-dev-server]", "http://localhost:8080");
}
);
}); });
gulp.task( gulp.task(
@ -154,16 +110,59 @@ gulp.task(
() => () =>
new Promise((resolve) => new Promise((resolve) =>
webpack( webpack(
[ bothBuilds(createCastConfig, {
createCastConfig({ isProdBuild: true,
isProdBuild: true, }),
latestBuild: false,
}), handler(resolve)
createCastConfig({ )
isProdBuild: true, )
latestBuild: true, );
}),
], 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) handler(resolve)
) )
) )

View File

@ -20,4 +20,13 @@ module.exports = {
cast_static: path.resolve(__dirname, "../cast/dist/static"), cast_static: path.resolve(__dirname, "../cast/dist/static"),
cast_output: path.resolve(__dirname, "../cast/dist/frontend_latest"), cast_output: path.resolve(__dirname, "../cast/dist/frontend_latest"),
cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"), 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",
}; };

View File

@ -3,8 +3,6 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
const WorkboxPlugin = require("workbox-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 ManifestPlugin = require("webpack-manifest-plugin");
const paths = require("./paths.js"); const paths = require("./paths.js");
const { babelLoaderConfig } = require("./babel.js"); const { babelLoaderConfig } = require("./babel.js");
@ -17,288 +15,246 @@ if (!version) {
} }
version = version[0]; version = version[0];
const genMode = (isProdBuild) => (isProdBuild ? "production" : "development"); const createWebpackConfig = ({
const genDevTool = (isProdBuild) => entry,
isProdBuild ? "source-map" : "inline-cheap-module-source-map"; outputRoot,
const genFilename = (isProdBuild, dontHash = new Set()) => ({ chunk }) => { defineOverlay,
if (!isProdBuild || dontHash.has(chunk.name)) { isProdBuild,
return `${chunk.name}.js`; latestBuild,
} isStatsBuild,
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 }));
}
return { return {
mode: genMode(isProdBuild), mode: isProdBuild ? "production" : "development",
devtool: genDevTool(isProdBuild), devtool: isProdBuild ? "source-map" : "inline-cheap-module-source-map",
entry, entry,
module: { 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: [ plugins: [
new ManifestPlugin(), new ManifestPlugin(),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
__DEV__: JSON.stringify(!isProdBuild), __DEV__: !isProdBuild,
__DEMO__: false,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"), __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(version), __VERSION__: JSON.stringify(version),
__DEMO__: false,
__STATIC_PATH__: "/static/", __STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify( "process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development" isProdBuild ? "production" : "development"
), ),
...defineOverlay,
}), }),
...plugins, // Ignore moment.js locales
isProdBuild && new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
!isCI && // Color.js is bloated, it contains all color definitions for all material color sets.
!isStatsBuild && new webpack.NormalModuleReplacementPlugin(
new CompressionPlugin({ /@polymer\/paper-styles\/color\.js$/,
cache: true, path.resolve(paths.polymer_dir, "src/util/empty.js")
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/], ),
algorithm(input, compressionOptions, callback) { // Ignore roboto pointing at CDN. We use local font-roboto-local.
return zopfli.gzip(input, compressionOptions, callback); new webpack.NormalModuleReplacementPlugin(
}, /@polymer\/font-roboto\/roboto\.js$/,
}), path.resolve(paths.polymer_dir, "src/util/empty.js")
latestBuild && ),
new WorkboxPlugin.InjectManifest({ // Ignore mwc icons pointing at CDN.
swSrc: "./src/entrypoints/service-worker-hass.js", new webpack.NormalModuleReplacementPlugin(
swDest: "service_worker.js", /@material\/mwc-icon\/mwc-icon-font\.js$/,
importWorkboxFrom: "local", path.resolve(paths.polymer_dir, "src/util/empty.js")
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",
},
}),
].filter(Boolean), ].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: { output: {
filename: genFilename(isProdBuild), filename: ({ chunk }) => {
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild), const dontHash = new Set();
path: latestBuild ? paths.output : paths.output_es5,
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/", publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
// For workerize loader // For workerize loader
globalObject: "self", 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 createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
const rules = [tsLoader(latestBuild), cssLoader, htmlLoader]; return createWebpackConfig({
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
entry: { entry: {
main: "./demo/src/entrypoint.ts", main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
compatibility: "./src/entrypoints/compatibility.ts", compatibility: path.resolve(
}, paths.polymer_dir,
module: { "src/entrypoints/compatibility.ts"
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"
), ),
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 createCastConfig = ({ isProdBuild, latestBuild }) => {
const isStatsBuild = false;
const entry = { const entry = {
launcher: "./cast/src/launcher/entrypoint.ts", launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
}; };
if (latestBuild) { 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]; return createWebpackConfig({
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
entry, entry,
module: { outputRoot: paths.cast_root,
rules, 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), outputRoot: "",
plugins: [ isProdBuild,
new ManifestPlugin(), latestBuild,
new webpack.DefinePlugin({ });
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"), config.output.path = paths.hassio_root;
__VERSION__: JSON.stringify(version), config.output.publicPath = paths.hassio_publicPath;
__DEMO__: false,
__STATIC_PATH__: "/static/", return config;
"process.env.NODE_ENV": JSON.stringify( };
isProdBuild ? "production" : "development"
), const createGalleryConfig = ({ isProdBuild, latestBuild }) => {
}), if (!latestBuild) {
...plugins, throw new Error("Gallery only supports latest build!");
].filter(Boolean), }
resolve, const config = createWebpackConfig({
output: { entry: {
filename: genFilename(isProdBuild), entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"),
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: paths.gallery_root,
isProdBuild,
latestBuild,
});
return config;
}; };
module.exports = { module.exports = {
resolve,
plugins,
optimization,
createAppConfig, createAppConfig,
createDemoConfig, createDemoConfig,
createCastConfig, createCastConfig,
createHassioConfig,
createGalleryConfig,
}; };

11
cast/webpack.config.js Normal file
View File

@ -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,
});

View File

@ -217,6 +217,18 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
icon: "hademo:currency-usd", 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": { "cover.garagedoor": {
entity_id: "cover.garagedoor", entity_id: "cover.garagedoor",
state: "closed", state: "closed",

View File

@ -446,6 +446,11 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
"script.tv_off", "script.tv_off",
], ],
}, },
{
type: "sensor",
entity: "sensor.study_temp",
graph: "line",
},
{ {
type: "entities", type: "entities",
title: "Doorbell", title: "Doorbell",

View File

@ -23,27 +23,24 @@ export const demoThemeJimpower = () => ({
"paper-listbox-background-color": "#2E333A", "paper-listbox-background-color": "#2E333A",
"table-row-background-color": "#353840", "table-row-background-color": "#353840",
"paper-grey-50": "var(--primary-text-color)", "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", "paper-dialog-background-color": "#434954",
"secondary-text-color": "#5294E2", "secondary-text-color": "#5294E2",
"google-red-500": "#E45E65", "google-red-500": "#E45E65",
"divider-color": "rgba(0, 0, 0, .12)", "divider-color": "rgba(0, 0, 0, .12)",
"paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
"google-green-500": "#39E949", "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", "label-badge-border-color": "green",
"paper-listbox-color": "var(--primary-color)", "paper-listbox-color": "var(--primary-color)",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)", "paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
"paper-toggle-button-checked-ink-color": "var(--accent-color)",
"paper-card-background-color": "#434954", "paper-card-background-color": "#434954",
"label-badge-text-color": "var(--primary-text-color)", "label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-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)", "dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)", "paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)", "paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#F9C536", "paper-item-icon-active-color": "#F9C536",
"accent-color": "#E45E65", "accent-color": "#E45E65",
"paper-toggle-button-checked-bar-color": "var(--accent-color)",
"table-row-alternative-background-color": "#3E424B", "table-row-alternative-background-color": "#3E424B",
}); });

View File

@ -24,27 +24,24 @@ export const demoThemeKernehed = () => ({
"paper-listbox-background-color": "#141414", "paper-listbox-background-color": "#141414",
"table-row-background-color": "#292929", "table-row-background-color": "#292929",
"paper-grey-50": "var(--primary-text-color)", "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", "paper-dialog-background-color": "#292929",
"secondary-text-color": "#b58e31", "secondary-text-color": "#b58e31",
"google-red-500": "#b58e31", "google-red-500": "#b58e31",
"divider-color": "rgba(0, 0, 0, .12)", "divider-color": "rgba(0, 0, 0, .12)",
"paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
"google-green-500": "#2980b9", "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", "label-badge-border-color": "green",
"paper-listbox-color": "#777777", "paper-listbox-color": "#777777",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)", "paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
"paper-toggle-button-checked-ink-color": "var(--accent-color)",
"paper-card-background-color": "#292929", "paper-card-background-color": "#292929",
"label-badge-text-color": "var(--primary-text-color)", "label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-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)", "dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)", "paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)", "paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#b58e31", "paper-item-icon-active-color": "#b58e31",
"accent-color": "#2980b9", "accent-color": "#2980b9",
"paper-toggle-button-checked-bar-color": "var(--accent-color)",
"table-row-alternative-background-color": "#292929", "table-row-alternative-background-color": "#292929",
}); });

View File

@ -12,8 +12,7 @@ export const demoThemeTeachingbirds = () => ({
"paper-slider-knob-color": "var(--primary-color)", "paper-slider-knob-color": "var(--primary-color)",
"paper-listbox-color": "#FFFFFF", "paper-listbox-color": "#FFFFFF",
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)", "paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
"paper-toggle-button-checked-ink-color": "var(--dark-primary-color)", "switch-unchecked-track-color": "var(--primary-text-color)",
"paper-toggle-button-unchecked-bar-color": "var(--primary-text-color)",
"paper-card-background-color": "#4e4e4e", "paper-card-background-color": "#4e4e4e",
"label-badge-text-color": "var(--text-primary-color)", "label-badge-text-color": "var(--text-primary-color)",
"primary-background-color": "#303030", "primary-background-color": "#303030",
@ -22,7 +21,7 @@ export const demoThemeTeachingbirds = () => ({
"secondary-background-color": "#2b2b2b", "secondary-background-color": "#2b2b2b",
"paper-slider-knob-start-color": "var(--primary-color)", "paper-slider-knob-start-color": "var(--primary-color)",
"paper-item-icon-active-color": "#d8bf50", "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", "secondary-text-color": "#389638",
"disabled-text-color": "#545454", "disabled-text-color": "#545454",
"paper-item-icon_-_color": "var(--primary-text-color)", "paper-item-icon_-_color": "var(--primary-text-color)",

View File

@ -1,10 +1,4 @@
import { import { LitElement, html, CSSResult, css, property } from "lit-element";
LitElement,
html,
CSSResult,
css,
PropertyDeclarations,
} from "lit-element";
import { until } from "lit-html/directives/until"; import { until } from "lit-html/directives/until";
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-spinner/paper-spinner-lite"; import "@polymer/paper-spinner/paper-spinner-lite";
@ -20,19 +14,11 @@ import {
} from "../configs/demo-configs"; } from "../configs/demo-configs";
export class HADemoCard extends LitElement implements LovelaceCard { export class HADemoCard extends LitElement implements LovelaceCard {
public lovelace?: Lovelace; @property() public lovelace?: Lovelace;
public hass!: MockHomeAssistant; @property() public hass!: MockHomeAssistant;
private _switching?: boolean; @property() private _switching?: boolean;
private _hidden = localStorage.hide_demo_card; private _hidden = localStorage.hide_demo_card;
static get properties(): PropertyDeclarations {
return {
lovelace: {},
hass: {},
_switching: {},
};
}
public getCardSize() { public getCardSize() {
return this._hidden ? 0 : 2; return this._hidden ? 0 : 2;
} }

View File

@ -1,10 +1,9 @@
const { createDemoConfig } = require("../build-scripts/webpack.js"); 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 latestBuild = true;
const isStatsBuild = process.env.STATS === "1";
const latestBuild = false;
module.exports = createDemoConfig({ module.exports = createDemoConfig({
isProdBuild, isProdBuild,

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#2157BC">
<title>HAGallery</title>
<script src='./main.js' async></script>
<style>
body {
font-family: Roboto, Noto, sans-serif;
margin: 0;
padding: 0;
}
</style>
</head>
<body></body>
</html>

View File

@ -4,14 +4,6 @@
# Stop on errors # Stop on errors
set -e set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/../.."
OUTPUT_DIR=dist ./node_modules/.bin/gulp build-gallery
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

View File

@ -4,10 +4,6 @@
# Stop on errors # Stop on errors
set -e set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/../.."
cd .. ./node_modules/.bin/gulp develop-gallery
./node_modules/.bin/gulp build-translations gen-icons
cd gallery
../node_modules/.bin/webpack-dev-server

View File

@ -1,6 +1,6 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; 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"; import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";
@ -62,7 +62,7 @@ class DemoCard extends PolymerElement {
card.removeChild(card.lastChild); card.removeChild(card.lastChild);
} }
const el = createCardElement(JsYaml.safeLoad(config.config)[0]); const el = createCardElement(safeLoad(config.config)[0]);
el.hass = this.hass; el.hass = this.hass;
card.appendChild(el); card.appendChild(el);
} }

View File

@ -26,7 +26,9 @@ class DemoCards extends PolymerElement {
</style> </style>
<app-toolbar> <app-toolbar>
<div class="filters"> <div class="filters">
<ha-switch checked="{{_showConfig}}">Show config</ha-switch> <ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
Show config
</ha-switch>
</div> </div>
</app-toolbar> </app-toolbar>
<div class="cards"> <div class="cards">
@ -51,6 +53,10 @@ class DemoCards extends PolymerElement {
}, },
}; };
} }
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
} }
customElements.define("demo-cards", DemoCards); customElements.define("demo-cards", DemoCards);

View File

@ -12,7 +12,7 @@ export class DemoUtilLongPress extends LitElement {
() => html` () => html`
<ha-card> <ha-card>
<mwc-button <mwc-button
@ha-click="${this._handleTap}" @ha-click="${this._handleClick}"
@ha-hold="${this._handleHold}" @ha-hold="${this._handleHold}"
.longPress="${longPress()}" .longPress="${longPress()}"
> >
@ -28,7 +28,7 @@ export class DemoUtilLongPress extends LitElement {
`; `;
} }
private _handleTap(ev: Event) { private _handleClick(ev: Event) {
this._addValue(ev, "tap"); this._addValue(ev, "tap");
} }

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#2157BC" />
<title>HAGallery</title>
<script type="module" src="<%= latestGalleryJS %>"></script>
<style>
body {
font-family: Roboto, Noto, sans-serif;
margin: 0;
padding: 0;
}
</style>
</head>
<body></body>
</html>

View File

@ -1,6 +1,6 @@
const path = require("path"); const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin"); 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 { babelLoaderConfig } = require("../build-scripts/babel.js");
const isProd = process.env.NODE_ENV === "production"; const isProd = process.env.NODE_ENV === "production";
@ -9,80 +9,64 @@ const buildPath = path.resolve(__dirname, "dist");
const publicPath = isProd ? "./" : "http://localhost:8080/"; const publicPath = isProd ? "./" : "http://localhost:8080/";
const latestBuild = true; const latestBuild = true;
const rules = [ module.exports = createGalleryConfig({
{ latestBuild: true,
exclude: [path.resolve(__dirname, "../node_modules")], });
test: /\.ts$/,
use: [ const bla = () => {
{ const oldExports = {
loader: "ts-loader", mode: isProd ? "production" : "development",
options: { // Disabled in prod while we make Home Assistant able to serve the right files.
compilerOptions: latestBuild // Was source-map
? { noEmit: false } devtool: isProd ? "none" : "inline-source-map",
: { entry: "./src/entrypoint.js",
target: "es5", module: {
noEmit: false, rules: [
}, babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
}, },
}, {
], test: /\.(html)$/,
}, use: {
{ loader: "html-loader",
test: /\.css$/, options: {
use: "raw-loader", exportAsEs6Default: true,
}, },
{ },
test: /\.(html)$/, },
use: { ],
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
}, },
}, optimization: webpackBase.optimization(latestBuild),
]; plugins: [
new CopyWebpackPlugin([
if (!latestBuild) { "public",
rules.push(babelLoaderConfig({ latestBuild })); { from: "../public", to: "static" },
} { from: "../build-translations/output", to: "static/translations" },
{
module.exports = { from: "../node_modules/leaflet/dist/leaflet.css",
mode: isProd ? "production" : "development", to: "static/images/leaflet/",
// Disabled in prod while we make Home Assistant able to serve the right files. },
// Was source-map {
devtool: isProd ? "none" : "inline-source-map", from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
entry: "./src/entrypoint.js", to: "static/fonts/roboto/",
module: { },
rules, {
}, from: "../node_modules/leaflet/dist/images",
optimization: webpackBase.optimization(latestBuild), to: "static/images/leaflet/",
plugins: [ },
new CopyWebpackPlugin([ ]),
"public", ].filter(Boolean),
{ from: "../public", to: "static" }, resolve: webpackBase.resolve,
{ from: "../build-translations/output", to: "static/translations" }, output: {
{ filename: "[name].js",
from: "../node_modules/leaflet/dist/leaflet.css", chunkFilename: chunkFilename,
to: "static/images/leaflet/", path: buildPath,
}, publicPath,
{ },
from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2", devServer: {
to: "static/fonts/roboto/", contentBase: "./public",
}, },
{ };
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",
},
}; };

View File

@ -4,11 +4,6 @@
# Stop on errors # Stop on errors
set -e set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/../.."
OUTPUT_DIR=build ./node_modules/.bin/gulp build-hassio
rm -rf $OUTPUT_DIR
node script/gen-icons.js
NODE_ENV=production CI=false ../node_modules/.bin/webpack -p --config webpack.config.js

View File

@ -4,11 +4,6 @@
# Stop on errors # Stop on errors
set -e set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/../.."
OUTPUT_DIR=build ./node_modules/.bin/gulp develop-hassio
rm -rf $OUTPUT_DIR
mkdir $OUTPUT_DIR
node script/gen-icons.js
../node_modules/.bin/webpack --watch --progress

View File

@ -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();

View File

@ -7,6 +7,7 @@ import {
property, property,
customElement, customElement,
} from "lit-element"; } from "lit-element";
import "@polymer/iron-icon/iron-icon";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { import {
@ -33,12 +34,15 @@ export class HassioUpdate extends LitElement {
@property() public error?: string; @property() public error?: string;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if ( const updatesAvailable: number = [
this.hassInfo.version === this.hassInfo.last_version && this.hassInfo,
this.supervisorInfo.version === this.supervisorInfo.last_version && this.supervisorInfo,
(!this.hassOsInfo || this.hassOsInfo,
this.hassOsInfo.version === this.hassOsInfo.version_latest) ].filter((value) => {
) { return !!value && value.version !== value.last_version;
}).length;
if (!updatesAvailable) {
return html``; return html``;
} }
@ -50,6 +54,11 @@ export class HassioUpdate extends LitElement {
` `
: ""} : ""}
<div class="card-group"> <div class="card-group">
<div class="title">
${updatesAvailable > 1
? "Updates Available 🎉"
: "Update Available 🎉"}
</div>
${this._renderUpdateCard( ${this._renderUpdateCard(
"Home Assistant", "Home Assistant",
this.hassInfo.version, this.hassInfo.version,
@ -57,7 +66,8 @@ export class HassioUpdate extends LitElement {
"hassio/homeassistant/update", "hassio/homeassistant/update",
`https://${ `https://${
this.hassInfo.last_version.includes("b") ? "rc" : "www" 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( ${this._renderUpdateCard(
"Hass.io Supervisor", "Hass.io Supervisor",
@ -89,18 +99,31 @@ export class HassioUpdate extends LitElement {
curVersion: string, curVersion: string,
lastVersion: string, lastVersion: string,
apiPath: string, apiPath: string,
releaseNotesUrl: string releaseNotesUrl: string,
icon?: string
): TemplateResult { ): TemplateResult {
if (lastVersion === curVersion) { if (lastVersion === curVersion) {
return html``; return html``;
} }
return html` return html`
<paper-card heading="${name} update available! 🎉"> <paper-card>
<div class="card-content"> <div class="card-content">
${name} ${lastVersion} is available and you are currently running ${icon
${name} ${curVersion}. ? html`
<div class="icon">
<iron-icon .icon="${icon}" />
</div>
`
: ""}
<div class="update-heading">${name} ${lastVersion}</div>
<div class="warning">
You are currently running version ${curVersion}
</div>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a href="${releaseNotesUrl}" target="_blank">
<mwc-button>Release notes</mwc-button>
</a>
<ha-call-api-button <ha-call-api-button
.hass=${this.hass} .hass=${this.hass}
.path=${apiPath} .path=${apiPath}
@ -108,9 +131,6 @@ export class HassioUpdate extends LitElement {
> >
Update Update
</ha-call-api-button> </ha-call-api-button>
<a href="${releaseNotesUrl}" target="_blank">
<mwc-button>Release notes</mwc-button>
</a>
</div> </div>
</paper-card> </paper-card>
`; `;
@ -140,6 +160,23 @@ export class HassioUpdate extends LitElement {
display: inline-block; display: inline-block;
margin-bottom: 32px; 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 { .errors {
color: var(--google-red-500); color: var(--google-red-500);
padding: 16px; padding: 16px;

66
hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts Normal file → Executable file
View File

@ -3,6 +3,7 @@ import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -94,13 +95,23 @@ class HassioSnapshotDialog extends PolymerElement {
.details { .details {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.download {
color: var(--primary-color);
}
.warning, .warning,
.error { .error {
color: var(--google-red-500); 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;
}
</style> </style>
<ha-paper-dialog <ha-paper-dialog
id="dialog" id="dialog"
@ -132,7 +143,7 @@ class HassioSnapshotDialog extends PolymerElement {
</template> </template>
<template is="dom-if" if="[[_addons.length]]"> <template is="dom-if" if="[[_addons.length]]">
<div>Add-ons:</div> <div>Add-ons:</div>
<paper-dialog-scrollable> <paper-dialog-scrollable class="no-margin-top">
<template is="dom-repeat" items="[[_addons]]" sort="_sortAddons"> <template is="dom-repeat" items="[[_addons]]" sort="_sortAddons">
<paper-checkbox checked="{{item.checked}}"> <paper-checkbox checked="{{item.checked}}">
[[item.name]] <span class="details">([[item.version]])</span> [[item.name]] <span class="details">([[item.version]])</span>
@ -151,28 +162,35 @@ class HassioSnapshotDialog extends PolymerElement {
<template is="dom-if" if="[[error]]"> <template is="dom-if" if="[[error]]">
<p class="error">Error: [[error]]</p> <p class="error">Error: [[error]]</p>
</template> </template>
<div class="buttons"> <div>Actions:</div>
<paper-icon-button <ul class="buttons">
icon="hassio:delete" <li>
on-click="_deleteClicked" <mwc-button on-click="_downloadClicked">
class="warning" <iron-icon icon="hassio:download" class="icon"></iron-icon>
title="Delete snapshot" Download Snapshot
></paper-icon-button> </mwc-button>
<paper-icon-button </li>
on-click="_downloadClicked" <li>
icon="hassio:download" <mwc-button on-click="_partialRestoreClicked">
class="download" <iron-icon icon="hassio:history" class="icon"> </iron-icon>
title="Download snapshot" Restore Selected
></paper-icon-button> </mwc-button>
<mwc-button on-click="_partialRestoreClicked" </li>
>Restore selected</mwc-button
>
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]"> <template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
<mwc-button on-click="_fullRestoreClicked" <li>
>Wipe &amp; restore</mwc-button <mwc-button on-click="_fullRestoreClicked">
> <iron-icon icon="hassio:history" class="icon"> </iron-icon>
Wipe &amp; restore
</mwc-button>
</li>
</template> </template>
</div> <li>
<mwc-button on-click="_deleteClicked">
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>
</li>
</ul>
</ha-paper-dialog> </ha-paper-dialog>
`; `;
} }

View File

@ -3,7 +3,7 @@ import { PolymerElement } from "@polymer/polymer";
import "@polymer/paper-icon-button"; import "@polymer/paper-icon-button";
import "../../src/resources/ha-style"; 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 { fireEvent } from "../../src/common/dom/fire_event";
import { import {
HassRouterPage, HassRouterPage,

View File

@ -1,85 +1,11 @@
const webpack = require("webpack"); const { createHassioConfig } = require("../build-scripts/webpack.js");
const CompressionPlugin = require("compression-webpack-plugin"); const { isProdBuild } = require("../build-scripts/env.js");
const zopfli = require("@gfx/zopfli");
const config = require("./config.js"); // File just used for stats builds
const webpackBase = require("../build-scripts/webpack.js");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
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 latestBuild = false;
const rules = [ module.exports = createHassioConfig({
{ isProdBuild,
exclude: [config.nodeDir], latestBuild,
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}/`,
},
};

View File

@ -84,7 +84,6 @@
"hls.js": "^0.12.4", "hls.js": "^0.12.4",
"home-assistant-js-websocket": "^4.4.0", "home-assistant-js-websocket": "^4.4.0",
"intl-messageformat": "^2.2.0", "intl-messageformat": "^2.2.0",
"jquery": "^3.4.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"leaflet": "^1.4.0", "leaflet": "^1.4.0",
"lit-element": "^2.2.1", "lit-element": "^2.2.1",
@ -98,7 +97,6 @@
"react-big-calendar": "^0.20.4", "react-big-calendar": "^0.20.4",
"regenerator-runtime": "^0.13.2", "regenerator-runtime": "^0.13.2",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"round-slider": "^1.3.3",
"superstruct": "^0.6.1", "superstruct": "^0.6.1",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
@ -112,19 +110,20 @@
"@babel/plugin-proposal-decorators": "^7.4.0", "@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-proposal-object-rest-spread": "^7.4.0", "@babel/plugin-proposal-object-rest-spread": "^7.4.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.4.0", "@babel/plugin-transform-react-jsx": "^7.3.0",
"@gfx/zopfli": "^1.0.11", "@babel/preset-env": "^7.4.2",
"@babel/preset-typescript": "^7.4.0",
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12", "@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1", "@types/chromecast-caf-sender": "^1.0.1",
"@types/codemirror": "^0.0.78", "@types/codemirror": "^0.0.78",
"@types/hls.js": "^0.12.3", "@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3", "@types/leaflet": "^1.4.3",
"@types/memoize-one": "4.1.0", "@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6", "@types/mocha": "^5.2.6",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"chai": "^4.2.0", "chai": "^4.2.0",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^5.0.2", "copy-webpack-plugin": "^5.0.2",
"del": "^4.0.0", "del": "^4.0.0",
"eslint": "^6.3.0", "eslint": "^6.3.0",
@ -160,7 +159,6 @@
"require-dir": "^1.2.0", "require-dir": "^1.2.0",
"sinon": "^7.3.1", "sinon": "^7.3.1",
"terser-webpack-plugin": "^1.2.3", "terser-webpack-plugin": "^1.2.3",
"ts-loader": "^6.1.1",
"ts-mocha": "^6.0.0", "ts-mocha": "^6.0.0",
"tslint": "^5.14.0", "tslint": "^5.14.0",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",

View File

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

View File

@ -7,7 +7,7 @@ import {
css, css,
} from "lit-element"; } from "lit-element";
import "@material/mwc-button"; import "@material/mwc-button";
import "../components/ha-form"; import "../components/ha-form/ha-form";
import "../components/ha-markdown"; import "../components/ha-markdown";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { AuthProvider } from "../data/auth"; import { AuthProvider } from "../data/auth";

View File

@ -2,10 +2,10 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { import {
LitElement, LitElement,
html, html,
PropertyDeclarations,
PropertyValues, PropertyValues,
CSSResult, CSSResult,
css, css,
property,
} from "lit-element"; } from "lit-element";
import "./ha-auth-flow"; import "./ha-auth-flow";
import { AuthProvider, fetchAuthProviders } from "../data/auth"; import { AuthProvider, fetchAuthProviders } from "../data/auth";
@ -20,11 +20,11 @@ interface QueryParams {
} }
class HaAuthorize extends litLocalizeLiteMixin(LitElement) { class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
public clientId?: string; @property() public clientId?: string;
public redirectUri?: string; @property() public redirectUri?: string;
public oauth2State?: string; @property() public oauth2State?: string;
private _authProvider?: AuthProvider; @property() private _authProvider?: AuthProvider;
private _authProviders?: AuthProvider[]; @property() private _authProviders?: AuthProvider[];
constructor() { constructor() {
super(); super();
@ -48,16 +48,6 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
} }
} }
static get properties(): PropertyDeclarations {
return {
_authProvider: {},
_authProviders: {},
clientId: {},
redirectUri: {},
oauth2State: {},
};
}
protected render() { protected render() {
if (!this._authProviders) { if (!this._authProviders) {
return html` return html`

View File

@ -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`
<style>
ha-state-label-badge {
display: inline-block;
margin-bottom: var(--ha-state-label-badge-margin-bottom, 16px);
}
</style>
<template is="dom-repeat" items="[[states]]">
<ha-state-label-badge
hass="[[hass]]"
state="[[item]]"
></ha-state-label-badge>
</template>
`;
}
static get properties() {
return {
hass: Object,
states: Array,
};
}
}
customElements.define("ha-badges-card", HaBadgesCard);

View File

@ -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`
<ha-state-label-badge
.hass=${this.hass}
.state=${state}
@click=${this._handleClick}
></ha-state-label-badge>
`
)}
`;
}
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;
}
}

View File

@ -35,6 +35,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"camera", "camera",
"climate", "climate",
"configurator", "configurator",
"counter",
"cover", "cover",
"fan", "fan",
"group", "group",

View File

@ -21,12 +21,12 @@ const hexToRgb = (hex: string): string | null => {
* localTheme: selected theme. * localTheme: selected theme.
* updateMeta: boolean if we should update the theme-color meta element. * updateMeta: boolean if we should update the theme-color meta element.
*/ */
export default function applyThemesOnElement( export const applyThemesOnElement = (
element, element,
themes, themes,
localTheme, localTheme,
updateMeta = false updateMeta = false
) { ) => {
if (!element._themes) { if (!element._themes) {
element._themes = {}; element._themes = {};
} }
@ -76,4 +76,4 @@ export default function applyThemesOnElement(
styles["--primary-color"] || meta.getAttribute("default-content"); styles["--primary-color"] || meta.getAttribute("default-content");
meta.setAttribute("content", themeColor); meta.setAttribute("content", themeColor);
} }
} };

View File

@ -14,6 +14,7 @@ const fixedIcons = {
climate: "hass:thermostat", climate: "hass:thermostat",
configurator: "hass:settings", configurator: "hass:settings",
conversation: "hass:text-to-speech", conversation: "hass:text-to-speech",
counter: "hass:counter",
device_tracker: "hass:account", device_tracker: "hass:account",
fan: "hass:fan", fan: "hass:fan",
google_assistant: "hass:google-assistant", google_assistant: "hass:google-assistant",

View File

@ -3,6 +3,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-progress-button"; import "./ha-progress-button";
import { EventsMixin } from "../../mixins/events-mixin"; import { EventsMixin } from "../../mixins/events-mixin";
import { showConfirmationDialog } from "../../dialogs/confirmation/show-dialog-confirmation";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
@ -49,10 +50,7 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
}; };
} }
buttonTapped() { callService() {
if (this.confirmation && !window.confirm(this.confirmation)) {
return;
}
this.progress = true; this.progress = true;
var el = this; var el = this;
var eventData = { var eventData = {
@ -79,6 +77,17 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
el.fire("hass-service-called", eventData); 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); customElements.define("ha-call-service-button", HaCallServiceButton);

View File

@ -427,7 +427,7 @@ export class HaDataTable extends BaseElement {
} }
.mdc-data-table { .mdc-data-table {
background-color: var(--card-background-color); background-color: var(--data-table-background-color);
border-radius: 4px; border-radius: 4px;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;

View File

@ -1,7 +1,7 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; 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 "@polymer/paper-listbox/paper-listbox";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { import {
@ -23,52 +23,165 @@ import {
subscribeDeviceRegistry, subscribeDeviceRegistry,
} from "../../data/device_registry"; } from "../../data/device_registry";
import { compare } from "../../common/string/compare"; 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 = `
<style>
paper-item {
margin: -10px 0;
padding: 0;
}
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div secondary>[[item.area]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name!;
root.querySelector("[secondary]")!.textContent = model.item.area!;
};
@customElement("ha-device-picker") @customElement("ha-device-picker")
class HaDevicePicker extends SubscribeMixin(LitElement) { class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public hass?: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@property() public value?: string; @property() public value?: string;
@property() public devices?: DeviceRegistryEntry[]; @property() public devices?: DeviceRegistryEntry[];
@property() public areas?: AreaRegistryEntry[];
@property() public entities?: EntityRegistryEntry[];
@property({ type: Boolean }) private _opened?: boolean;
private _sortedDevices = memoizeOne((devices?: DeviceRegistryEntry[]) => { private _getDevices = memoizeOne(
if (!devices || devices.length === 1) { (
return devices || []; 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[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeDeviceRegistry(this.hass!.connection!, (devices) => { subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this.devices = devices; this.devices = devices;
}), }),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this.areas = areas;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this.entities = entities;
}),
]; ];
} }
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (!this.devices || !this.areas || !this.entities) {
return;
}
const devices = this._getDevices(this.devices, this.areas, this.entities);
return html` return html`
<paper-dropdown-menu-light .label=${this.label}> <vaadin-combo-box-light
<paper-listbox item-value-path="id"
slot="dropdown-content" item-id-path="id"
.selected=${this._value} item-label-path="name"
attr-for-selected="data-device-id" .items=${devices}
@iron-select=${this._deviceChanged} .value=${this._value}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._deviceChanged}
>
<paper-input
.label=${this.label}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
> >
<paper-item data-device-id=""> ${this.value
No device ? html`
</paper-item> <paper-icon-button
${this._sortedDevices(this.devices).map( aria-label="Clear"
(device) => html` slot="suffix"
<paper-item data-device-id=${device.id}> class="clear-button"
${device.name_by_user || device.name} icon="hass:close"
</paper-item> no-ripple
` >
)} Clear
</paper-listbox> </paper-icon-button>
</paper-dropdown-menu-light> `
: ""}
${devices.length > 0
? html`
<paper-icon-button
aria-label="Show devices"
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</paper-icon-button>
`
: ""}
</paper-input>
</vaadin-combo-box-light>
`; `;
} }
@ -76,8 +189,12 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
return this.value || ""; return this.value || "";
} }
private _deviceChanged(ev) { private _openedChanged(ev: PolymerChangedEvent<boolean>) {
const newValue = ev.detail.item.dataset.deviceId; this._opened = ev.detail.value;
}
private _deviceChanged(ev: PolymerChangedEvent<string>) {
const newValue = ev.detail.value;
if (newValue !== this._value) { if (newValue !== this._value) {
this.value = newValue; 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 { static get styles(): CSSResult {
return css` return css`
paper-dropdown-menu-light { paper-input > paper-icon-button {
width: 100%; width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
} }
paper-listbox { [hidden] {
min-width: 200px; display: none;
}
paper-item {
cursor: pointer;
} }
`; `;
} }

View File

@ -11,7 +11,6 @@ import {
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; 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 { protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties); super.updated(changedProperties);

View File

@ -1,8 +1,9 @@
import { Constructor, customElement, CSSResult, css } from "lit-element"; import { customElement, CSSResult, css } from "lit-element";
import "@material/mwc-checkbox"; import "@material/mwc-checkbox";
// tslint:disable-next-line // tslint:disable-next-line
import { Checkbox } from "@material/mwc-checkbox"; import { Checkbox } from "@material/mwc-checkbox";
import { style } from "@material/mwc-checkbox/mwc-checkbox-css"; import { style } from "@material/mwc-checkbox/mwc-checkbox-css";
import { Constructor } from "../types";
// tslint:disable-next-line // tslint:disable-next-line
const MwcCheckbox = customElements.get("mwc-checkbox") as Constructor<Checkbox>; const MwcCheckbox = customElements.get("mwc-checkbox") as Constructor<Checkbox>;

View File

@ -1,12 +1,8 @@
import { import { classMap, html, customElement } from "@material/mwc-base/base-element";
classMap,
html,
customElement,
Constructor,
} from "@material/mwc-base/base-element";
import { ripple } from "@material/mwc-ripple/ripple-directive.js"; import { ripple } from "@material/mwc-ripple/ripple-directive.js";
import "@material/mwc-fab"; import "@material/mwc-fab";
import { Constructor } from "../types";
// tslint:disable-next-line // tslint:disable-next-line
import { Fab } from "@material/mwc-fab"; import { Fab } from "@material/mwc-fab";
// tslint:disable-next-line // tslint:disable-next-line

View File

@ -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`
<style>
.error {
color: red;
}
paper-checkbox {
display: inline-block;
padding: 22px 0;
}
</style>
<template is="dom-if" if="[[_isArray(schema)]]" restamp="">
<template is="dom-if" if="[[error.base]]">
<div class="error">[[computeError(error.base, schema)]]</div>
</template>
<template is="dom-repeat" items="[[schema]]">
<ha-form
data="[[_getValue(data, item)]]"
schema="[[item]]"
error="[[_getValue(error, item)]]"
on-data-changed="_valueChanged"
compute-error="[[computeError]]"
compute-label="[[computeLabel]]"
compute-suffix="[[computeSuffix]]"
></ha-form>
</template>
</template>
<template is="dom-if" if="[[!_isArray(schema)]]" restamp="">
<template is="dom-if" if="[[error]]">
<div class="error">[[computeError(error, schema)]]</div>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "string")]]'
restamp=""
>
<template
is="dom-if"
if='[[_includes(schema.name, "password")]]'
restamp=""
>
<paper-input
type="[[_passwordFieldType(unmaskedPassword)]]"
label="[[computeLabel(schema)]]"
value="{{data}}"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
>
<paper-icon-button
toggles
active="{{unmaskedPassword}}"
slot="suffix"
icon="[[_passwordFieldIcon(unmaskedPassword)]]"
id="iconButton"
title="Click to toggle between masked and clear password"
>
</paper-icon-button>
</paper-input>
</template>
<template
is="dom-if"
if='[[!_includes(schema.name, "password")]]'
restamp=""
>
<paper-input
label="[[computeLabel(schema)]]"
value="{{data}}"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
></paper-input>
</template>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "integer")]]'
restamp=""
>
<template is="dom-if" if="[[_isRange(schema)]]" restamp="">
<div>
[[computeLabel(schema)]]
<ha-paper-slider
pin=""
value="{{data}}"
min="[[schema.valueMin]]"
max="[[schema.valueMax]]"
></ha-paper-slider>
</div>
</template>
<template is="dom-if" if="[[!_isRange(schema)]]" restamp="">
<paper-input
label="[[computeLabel(schema)]]"
value="{{data}}"
type="number"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
></paper-input>
</template>
</template>
<template is="dom-if" if='[[_equals(schema.type, "float")]]' restamp="">
<!-- TODO -->
<paper-input
label="[[computeLabel(schema)]]"
value="{{data}}"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
>
<span suffix="" slot="suffix">[[computeSuffix(schema)]]</span>
</paper-input>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "boolean")]]'
restamp=""
>
<div>
<paper-checkbox checked="{{data}}"
>[[computeLabel(schema)]]</paper-checkbox
>
</div>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "select")]]'
restamp=""
>
<paper-dropdown-menu label="[[computeLabel(schema)]]">
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
selected="{{data}}"
>
<template is="dom-repeat" items="[[schema.options]]">
<paper-item item-name$="[[_optionValue(item)]]"
>[[_optionLabel(item)]]</paper-item
>
</template>
</paper-listbox>
</paper-dropdown-menu>
</template>
</template>
`;
}
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);

View File

@ -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`
<paper-checkbox .checked=${this.data} @change=${this._valueChanged}>
${this.label}
</paper-checkbox>
`;
}
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;
}
}

View File

@ -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`
<paper-input
.label=${this.label}
.value=${this._value}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
>
<span suffix="" slot="suffix">${this.suffix}</span>
</paper-input>
`;
}
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;
}
}

View File

@ -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`
<div>
${this.label}
<ha-paper-slider
pin=""
.value=${this._value}
.min=${this.schema.valueMin}
.max=${this.schema.valueMax}
@value-changed=${this._valueChanged}
></ha-paper-slider>
</div>
`
: html`
<paper-input
type="number"
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
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;
}
}

View File

@ -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`
<paper-time-input
.label=${this.label}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
error-message="Required"
enable-second
format="24"
.hour=${this._parseDuration(this._hours)}
.min=${this._parseDuration(this._minutes)}
.sec=${this._parseDuration(this._seconds)}
@hour-changed=${this._hourChanged}
@min-changed=${this._minChanged}
@sec-changed=${this._secChanged}
float-input-labels
no-hours-limit
always-float-input-labels
hour-label="hh"
min-label="mm"
sec-label="ss"
></paper-time-input>
`;
}
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;
}
}

View File

@ -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`
<paper-dropdown-menu .label=${this.label}>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
.selected=${this.data}
@selected-item-changed=${this._valueChanged}
>
${this.schema.options!.map(
(item) => html`
<paper-item .itemValue=${this._optionValue(item)}>
${this._optionLabel(item)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
`;
}
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;
}
}

View File

@ -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`
<paper-input
.type=${this._unmaskedPassword ? "text" : "password"}
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
>
<paper-icon-button
toggles
.active=${this._unmaskedPassword}
slot="suffix"
.icon=${this._unmaskedPassword ? "hass:eye-off" : "hass:eye"}
id="iconButton"
title="Click to toggle between masked and clear password"
@click=${this._toggleUnmaskedPassword}
>
</paper-icon-button>
</paper-input>
`
: html`
<paper-input
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
error-message="Required"
@value-changed=${this._valueChanged}
></paper-input>
`;
}
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;
}
}

View File

@ -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`
<div class="error">
${this._computeError(this.error.base, this.schema)}
</div>
`
: ""}
${this.schema.map(
(item) => html`
<ha-form
.data=${this._getValue(this.data, item)}
.schema=${item}
.error=${this._getValue(this.error, item)}
@value-changed=${this._valueChanged}
.computeError=${this.computeError}
.computeLabel=${this.computeLabel}
.computeSuffix=${this.computeSuffix}
></ha-form>
`
)}
`;
}
return html`
${this.error
? html`
<div class="error">
${this._computeError(this.error, this.schema)}
</div>
`
: ""}
<div id="element" @value-changed=${this._valueChanged}></div>
`;
}
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;
}
}

View File

@ -1,4 +1,5 @@
import { Constructor } from "lit-element"; import { Constructor } from "../types";
import "@polymer/iron-icon/iron-icon"; import "@polymer/iron-icon/iron-icon";
// Not duplicate, this is for typing. // Not duplicate, this is for typing.
// tslint:disable-next-line // tslint:disable-next-line

View File

@ -1,31 +1,21 @@
import { import {
html, html,
LitElement, LitElement,
PropertyDeclarations,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
CSSResult, CSSResult,
css, css,
property,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import "./ha-icon"; import "./ha-icon";
class HaLabelBadge extends LitElement { class HaLabelBadge extends LitElement {
public value?: string; @property() public value?: string;
public icon?: string; @property() public icon?: string;
public label?: string; @property() public label?: string;
public description?: string; @property() public description?: string;
public image?: string; @property() public image?: string;
static get properties(): PropertyDeclarations {
return {
value: {},
icon: {},
label: {},
description: {},
image: {},
};
}
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`

View File

@ -1,6 +1,6 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import { Constructor } from "lit-element";
import { PolymerElement } from "@polymer/polymer"; import { PolymerElement } from "@polymer/polymer";
import { Constructor } from "../types";
const paperDropdownClass = customElements.get( const paperDropdownClass = customElements.get(
"paper-dropdown-menu" "paper-dropdown-menu"

View File

@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing. // Not duplicate, this is for typing.
// tslint:disable-next-line // tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button"; import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing. // Not duplicate, this is for typing.
// tslint:disable-next-line // tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button"; import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing. // Not duplicate, this is for typing.
// tslint:disable-next-line // tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button"; import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing. // Not duplicate, this is for typing.
// tslint:disable-next-line // tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button"; import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@ -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 "@material/mwc-switch";
import { style } from "@material/mwc-switch/mwc-switch-css"; import { style } from "@material/mwc-switch/mwc-switch-css";
// tslint:disable-next-line // tslint:disable-next-line
import { Switch } from "@material/mwc-switch"; import { Switch } from "@material/mwc-switch";
import { Constructor } from "../types";
// tslint:disable-next-line // tslint:disable-next-line
const MwcSwitch = customElements.get("mwc-switch") as Constructor<Switch>; const MwcSwitch = customElements.get("mwc-switch") as Constructor<Switch>;
@ -12,7 +13,10 @@ export class HaSwitch extends MwcSwitch {
protected firstUpdated() { protected firstUpdated() {
super.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( this.classList.toggle(
"slotted", "slotted",
Boolean(this._slot.assignedNodes().length) Boolean(this._slot.assignedNodes().length)
@ -29,12 +33,12 @@ export class HaSwitch extends MwcSwitch {
align-items: center; align-items: center;
} }
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb { .mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb {
background-color: var(--paper-toggle-button-unchecked-button-color); background-color: var(--switch-unchecked-button-color);
border-color: var(--paper-toggle-button-unchecked-button-color); border-color: var(--switch-unchecked-button-color);
} }
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track { .mdc-switch:not(.mdc-switch--checked) .mdc-switch__track {
background-color: var(--paper-toggle-button-unchecked-bar-color); background-color: var(--switch-unchecked-track-color);
border-color: var(--paper-toggle-button-unchecked-bar-color); border-color: var(--switch-unchecked-track-color);
} }
:host(.slotted) .mdc-switch { :host(.slotted) .mdc-switch {
margin-right: 24px; margin-right: 24px;

View File

@ -87,6 +87,10 @@ export class PaperTimeInput extends PolymerElement {
label { label {
@apply --paper-font-caption; @apply --paper-font-caption;
color: var(
--paper-input-container-color,
var(--secondary-text-color)
);
} }
.time-input-wrap { .time-input-wrap {
@ -106,14 +110,17 @@ export class PaperTimeInput extends PolymerElement {
id="hour" id="hour"
type="number" type="number"
value="{{hour}}" value="{{hour}}"
label="[[hourLabel]]"
on-change="_shouldFormatHour" on-change="_shouldFormatHour"
required="" on-focus="_onFocus"
required
prevent-invalid-input
auto-validate="[[autoValidate]]" auto-validate="[[autoValidate]]"
prevent-invalid-input=""
maxlength="2" maxlength="2"
max="[[_computeHourMax(format)]]" max="[[_computeHourMax(format)]]"
min="0" min="0"
no-label-float="" no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]" disabled="[[disabled]]"
> >
<span suffix="" slot="suffix">:</span> <span suffix="" slot="suffix">:</span>
@ -124,15 +131,40 @@ export class PaperTimeInput extends PolymerElement {
id="min" id="min"
type="number" type="number"
value="{{min}}" value="{{min}}"
label="[[minLabel]]"
on-change="_formatMin" on-change="_formatMin"
required="" on-focus="_onFocus"
required
auto-validate="[[autoValidate]]" auto-validate="[[autoValidate]]"
prevent-invalid-input="" prevent-invalid-input
maxlength="2" maxlength="2"
max="59" max="59"
min="0" min="0"
no-label-float="" no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]" disabled="[[disabled]]"
>
<span hidden$="[[!enableSecond]]" suffix slot="suffix">:</span>
</paper-input>
<!-- Sec Input -->
<paper-input
id="sec"
type="number"
value="{{sec}}"
label="[[secLabel]]"
on-change="_formatSec"
on-focus="_onFocus"
required
auto-validate="[[autoValidate]]"
prevent-invalid-input
maxlength="2"
max="59"
min="0"
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
hidden$="[[!enableSecond]]"
> >
</paper-input> </paper-input>
@ -180,6 +212,20 @@ export class PaperTimeInput extends PolymerElement {
type: Boolean, type: Boolean,
value: false, 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 * 12 or 24 hr format
*/ */
@ -208,6 +254,48 @@ export class PaperTimeInput extends PolymerElement {
type: String, type: String,
notify: true, 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 * AM or PM
*/ */
@ -223,7 +311,7 @@ export class PaperTimeInput extends PolymerElement {
type: String, type: String,
notify: true, notify: true,
readOnly: 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()) { if (!this.$.hour.validate() | !this.$.min.validate()) {
valid = false; valid = false;
} }
// Validate second field
if (this.enableSecond && !this.$.sec.validate()) {
valid = false;
}
// Validate AM PM if 12 hour time // Validate AM PM if 12 hour time
if (this.format === 12 && !this.$.dropdown.validate()) { if (this.format === 12 && !this.$.dropdown.validate()) {
valid = false; valid = false;
@ -248,15 +340,37 @@ export class PaperTimeInput extends PolymerElement {
/** /**
* Create time string * Create time string
*/ */
_computeTime(min, hour, amPm) { _computeTime(min, hour, sec, amPm) {
if (hour && min) { let str;
// No ampm on 24 hr time if (hour || min || (sec && this.enableSecond)) {
if (this.format === 24) { hour = hour || "00";
amPm = ""; 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() { _formatMin() {
if (this.min.toString().length === 1) { 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() { _shouldFormatHour() {
if (this.format === 24 && this.hour.toString().length === 1) { 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 * 24 hour format has a max hr of 23
*/ */
_computeHourMax(format) { _computeHourMax(format) {
if (this.noHoursLimit) {
return null;
}
if (format === 12) { if (format === 12) {
return format; return format;
} }

View File

@ -181,23 +181,63 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
state.attributes.target_temp_low state.attributes.target_temp_low
); );
addColumn(name + " current temperature", true); addColumn(
`${this.hass.localize(
"ui.card.climate.current_temperature",
"name",
name
)}`,
true
);
if (hasHeat) { 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 // The "heating" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat. // temperature when the thermostat is calling for heat.
} }
if (hasCool) { 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 // The "cooling" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat. // temperature when the thermostat is calling for heat.
} }
if (hasTargetRange) { if (hasTargetRange) {
addColumn(name + " target temperature high", true); addColumn(
addColumn(name + " target temperature low", true); `${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 { } else {
addColumn(name + " target temperature", true); addColumn(
`${this.hass.localize(
"ui.card.climate.target_temperature_entity",
"name",
name
)}`,
true
);
} }
states.states.forEach((state) => { states.states.forEach((state) => {

View File

@ -18,19 +18,20 @@ import { fireEvent } from "../../common/dom/fire_event";
import { User, fetchUsers } from "../../data/user"; import { User, fetchUsers } from "../../data/user";
import { compare } from "../../common/string/compare"; import { compare } from "../../common/string/compare";
class HaEntityPicker extends LitElement { class HaUserPicker extends LitElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@property() public value?: string; @property() public value?: string;
@property() public users?: User[]; @property() public users?: User[];
private _sortedUsers = memoizeOne((users?: User[]) => { private _sortedUsers = memoizeOne((users?: User[]) => {
if (!users || users.length === 1) { if (!users) {
return users || []; return [];
} }
const sorted = [...users];
sorted.sort((a, b) => compare(a.name, b.name)); return users
return sorted; .filter((user) => !user.system_generated)
.sort((a, b) => compare(a.name, b.name));
}); });
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
@ -101,4 +102,4 @@ class HaEntityPicker extends LitElement {
} }
} }
customElements.define("ha-user-picker", HaEntityPicker); customElements.define("ha-user-picker", HaUserPicker);

View File

@ -39,6 +39,15 @@ export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
device_id: deviceId, device_id: deviceId,
}); });
export const fetchDeviceActionCapabilities = (
hass: HomeAssistant,
action: DeviceAction
) =>
hass.callWS<DeviceAction[]>({
type: "device_automation/action/capabilities",
action,
});
export const fetchDeviceConditionCapabilities = ( export const fetchDeviceConditionCapabilities = (
hass: HomeAssistant, hass: HomeAssistant,
condition: DeviceCondition condition: DeviceCondition
@ -57,7 +66,7 @@ export const fetchDeviceTriggerCapabilities = (
trigger, trigger,
}); });
const whitelist = ["above", "below", "for"]; const whitelist = ["above", "below", "code", "for"];
export const deviceAutomationsEqual = ( export const deviceAutomationsEqual = (
a: DeviceAutomation, a: DeviceAutomation,

View File

@ -11,7 +11,7 @@ export interface LovelaceConfig {
export interface LovelaceViewConfig { export interface LovelaceViewConfig {
index?: number; index?: number;
title?: string; title?: string;
badges?: string[]; badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[]; cards?: LovelaceCardConfig[];
path?: string; path?: string;
icon?: string; icon?: string;
@ -25,6 +25,11 @@ export interface ShowViewConfig {
user?: string; user?: string;
} }
export interface LovelaceBadgeConfig {
type?: string;
[key: string]: any;
}
export interface LovelaceCardConfig { export interface LovelaceCardConfig {
index?: number; index?: number;
view_index?: number; view_index?: number;
@ -32,11 +37,11 @@ export interface LovelaceCardConfig {
[key: string]: any; [key: string]: any;
} }
export interface ToggleActionConfig { export interface ToggleActionConfig extends BaseActionConfig {
action: "toggle"; action: "toggle";
} }
export interface CallServiceActionConfig { export interface CallServiceActionConfig extends BaseActionConfig {
action: "call-service"; action: "call-service";
service: string; service: string;
service_data?: { service_data?: {
@ -45,24 +50,37 @@ export interface CallServiceActionConfig {
}; };
} }
export interface NavigateActionConfig { export interface NavigateActionConfig extends BaseActionConfig {
action: "navigate"; action: "navigate";
navigation_path: string; navigation_path: string;
} }
export interface UrlActionConfig { export interface UrlActionConfig extends BaseActionConfig {
action: "url"; action: "url";
url_path: string; url_path: string;
} }
export interface MoreInfoActionConfig { export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info"; action: "more-info";
} }
export interface NoActionConfig { export interface NoActionConfig extends BaseActionConfig {
action: "none"; action: "none";
} }
export interface BaseActionConfig {
confirmation?: ConfirmationRestrictionConfig;
}
export interface ConfirmationRestrictionConfig {
text?: string;
exemptions?: RestrictionConfig[];
}
export interface RestrictionConfig {
user: string;
}
export type ActionConfig = export type ActionConfig =
| ToggleActionConfig | ToggleActionConfig
| CallServiceActionConfig | CallServiceActionConfig
@ -108,3 +126,7 @@ export const getLovelaceCollection = (conn: Connection) =>
export interface WindowWithLovelaceProm extends Window { export interface WindowWithLovelaceProm extends Window {
llConfProm?: Promise<LovelaceConfig>; llConfProm?: Promise<LovelaceConfig>;
} }
export interface LongPressOptions {
hasDoubleClick?: boolean;
}

View File

@ -60,7 +60,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
@opened-changed="${this._openedChanged}" @opened-changed="${this._openedChanged}"
> >
<h2> <h2>
${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
)}
</h2> </h2>
<paper-dialog-scrollable> <paper-dialog-scrollable>
${this._loading ${this._loading
@ -89,7 +95,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
</p> </p>
<p class="secondary"> <p class="secondary">
${this.hass.localize( ${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
)} )}
</p> </p>
</div> </div>

View File

@ -14,7 +14,7 @@ import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import "../../components/ha-form"; import "../../components/ha-form/ha-form";
import "../../components/ha-markdown"; import "../../components/ha-markdown";
import "../../resources/ha-style"; import "../../resources/ha-style";
import "../../components/dialog/ha-paper-dialog"; import "../../components/dialog/ha-paper-dialog";
@ -141,6 +141,7 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig} .flowConfig=${this._params.flowConfig}
.hass=${this.hass} .hass=${this.hass}
.handlers=${this._handlers} .handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
></step-flow-pick-handler> ></step-flow-pick-handler>
` `
: this._step.type === "form" : this._step.type === "form"

View File

@ -75,6 +75,7 @@ export interface DataEntryFlowDialogParams {
continueFlowId?: string; continueFlowId?: string;
dialogClosedCallback?: (params: { flowFinished: boolean }) => void; dialogClosedCallback?: (params: { flowFinished: boolean }) => void;
flowConfig: FlowConfig; flowConfig: FlowConfig;
showAdvanced?: boolean;
} }
export const loadDataEntryFlowDialog = () => export const loadDataEntryFlowDialog = () =>

View File

@ -12,10 +12,9 @@ import "@material/mwc-button";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import "../../components/ha-form"; import "../../components/ha-form/ha-form";
import "../../components/ha-markdown"; import "../../components/ha-markdown";
import "../../resources/ha-style"; import "../../resources/ha-style";
import { PolymerChangedEvent, applyPolymerEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { configFlowContentStyles } from "./styles"; import { configFlowContentStyles } from "./styles";
@ -69,7 +68,7 @@ class StepFlowForm extends LitElement {
${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)} ${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)}
<ha-form <ha-form
.data=${stepData} .data=${stepData}
@data-changed=${this._stepDataChanged} @value-changed=${this._stepDataChanged}
.schema=${step.data_schema} .schema=${step.data_schema}
.error=${step.errors} .error=${step.errors}
.computeLabel=${this._labelCallback} .computeLabel=${this._labelCallback}
@ -169,8 +168,8 @@ class StepFlowForm extends LitElement {
} }
} }
private _stepDataChanged(ev: PolymerChangedEvent<any>): void { private _stepDataChanged(ev: CustomEvent): void {
this._stepData = applyPolymerEvent(ev, this._stepData); this._stepData = ev.detail.value;
} }
private _labelCallback = (field: FieldSchema): string => private _labelCallback = (field: FieldSchema): string =>

View File

@ -31,6 +31,7 @@ class StepFlowPickHandler extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public handlers!: string[]; @property() public handlers!: string[];
@property() public showAdvanced?: boolean;
@property() private filter?: string; @property() private filter?: string;
private _width?: number; private _width?: number;
@ -79,18 +80,24 @@ class StepFlowPickHandler extends LitElement {
` `
)} )}
</div> </div>
<p> ${this.showAdvanced
${this.hass.localize( ? html`
"ui.panel.config.integrations.note_about_integrations" <p>
)}<br /> ${this.hass.localize(
${this.hass.localize( "ui.panel.config.integrations.note_about_integrations"
"ui.panel.config.integrations.note_about_website_reference" )}<br />
)}<a href="https://www.home-assistant.io/integrations/" ${this.hass.localize(
>${this.hass.localize( "ui.panel.config.integrations.note_about_website_reference"
"ui.panel.config.integrations.home_assistant_website" )}<a
)}.</a href="https://www.home-assistant.io/integrations/"
> target="_blank"
</p> >${this.hass.localize(
"ui.panel.config.integrations.home_assistant_website"
)}</a
>.
</p>
`
: ""}
`; `;
} }
@ -133,6 +140,11 @@ class StepFlowPickHandler extends LitElement {
} }
p { p {
text-align: center; text-align: center;
padding: 16px;
margin: 0;
}
p > a {
color: var(--primary-color);
} }
`; `;
} }

View File

@ -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<void> {
this._params = params;
}
protected render(): TemplateResult | void {
if (!this._params) {
return html``;
}
return html`
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
>
<h2>
${this._params.title
? this._params.title
: this.hass.localize("ui.dialogs.confirmation.title")}
</h2>
<paper-dialog-scrollable>
<p>${this._params.text}</p>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<mwc-button @click="${this._dismiss}">
${this.hass.localize("ui.dialogs.confirmation.cancel")}
</mwc-button>
<mwc-button @click="${this._confirm}">
${this.hass.localize("ui.dialogs.confirmation.ok")}
</mwc-button>
</div>
</ha-paper-dialog>
`;
}
private async _dismiss(): Promise<void> {
this._params = undefined;
}
private async _confirm(): Promise<void> {
this._params!.confirm();
this._dismiss();
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): 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;
}
}

View File

@ -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,
});
};

View File

@ -29,8 +29,9 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
width: 80px; width: 80px;
} }
.actions mwc-button { .actions mwc-button {
min-width: 160px; flex: 1 0 50%;
margin-bottom: 16px; margin: 0 4px 16px;
max-width: 200px;
} }
mwc-button.disarm { mwc-button.disarm {
color: var(--google-red-500); color: var(--google-red-500);
@ -137,7 +138,7 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
<div class="layout horizontal center-justified actions"> <div class="layout horizontal center-justified actions">
<template is="dom-if" if="[[_disarmVisible]]"> <template is="dom-if" if="[[_disarmVisible]]">
<mwc-button <mwc-button
raised outlined
class="disarm" class="disarm"
on-click="_callService" on-click="_callService"
data-service="alarm_disarm" data-service="alarm_disarm"
@ -148,7 +149,7 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
</template> </template>
<template is="dom-if" if="[[_armVisible]]"> <template is="dom-if" if="[[_armVisible]]">
<mwc-button <mwc-button
raised outlined
on-click="_callService" on-click="_callService"
data-service="alarm_arm_home" data-service="alarm_arm_home"
disabled="[[!_codeValid]]" disabled="[[!_codeValid]]"
@ -156,7 +157,7 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
[[localize('ui.card.alarm_control_panel.arm_home')]] [[localize('ui.card.alarm_control_panel.arm_home')]]
</mwc-button> </mwc-button>
<mwc-button <mwc-button
raised outlined
on-click="_callService" on-click="_callService"
data-service="alarm_arm_away" data-service="alarm_arm_away"
disabled="[[!_codeValid]]" disabled="[[!_codeValid]]"

View File

@ -1,8 +1,4 @@
import { import { PropertyValues, UpdatingElement, property } from "lit-element";
PropertyDeclarations,
PropertyValues,
UpdatingElement,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import "./more-info-alarm_control_panel"; import "./more-info-alarm_control_panel";
@ -10,6 +6,7 @@ import "./more-info-automation";
import "./more-info-camera"; import "./more-info-camera";
import "./more-info-climate"; import "./more-info-climate";
import "./more-info-configurator"; import "./more-info-configurator";
import "./more-info-counter";
import "./more-info-cover"; import "./more-info-cover";
import "./more-info-default"; import "./more-info-default";
import "./more-info-fan"; import "./more-info-fan";
@ -32,17 +29,10 @@ import dynamicContentUpdater from "../../../common/dom/dynamic_content_updater";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
class MoreInfoContent extends UpdatingElement { class MoreInfoContent extends UpdatingElement {
public hass?: HomeAssistant; @property() public hass?: HomeAssistant;
public stateObj?: HassEntity; @property() public stateObj?: HassEntity;
private _detachedChild?: ChildNode; private _detachedChild?: ChildNode;
static get properties(): PropertyDeclarations {
return {
hass: {},
stateObj: {},
};
}
protected firstUpdated(): void { protected firstUpdated(): void {
this.style.position = "relative"; this.style.position = "relative";
this.style.display = "block"; this.style.display = "block";

View File

@ -0,0 +1,70 @@
import {
LitElement,
html,
TemplateResult,
CSSResult,
css,
property,
customElement,
} from "lit-element";
import "@material/mwc-button";
import { HassEntity } from "home-assistant-js-websocket";
import { HomeAssistant } from "../../../types";
@customElement("more-info-counter")
class MoreInfoCounter extends LitElement {
@property() public hass!: HomeAssistant;
@property() public stateObj?: HassEntity;
protected render(): TemplateResult | void {
if (!this.hass || !this.stateObj) {
return html``;
}
return html`
<div class="actions">
<mwc-button
.action="${"increment"}"
@click="${this._handleActionClick}"
>
${this.hass!.localize("ui.card.counter.actions.increment")}
</mwc-button>
<mwc-button
.action="${"decrement"}"
@click="${this._handleActionClick}"
>
${this.hass!.localize("ui.card.counter.actions.decrement")}
</mwc-button>
<mwc-button .action="${"reset"}" @click="${this._handleActionClick}">
${this.hass!.localize("ui.card.counter.actions.reset")}
</mwc-button>
</div>
`;
}
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;
}
}

View File

@ -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`
<style include="iron-flex iron-flex-alignment"></style>
<template
is="dom-repeat"
items="[[computeOrder(risingDate, settingDate)]]"
>
<div class="data-entry layout justified horizontal">
<div class="key">
<span>[[itemCaption(item)]]</span>
<ha-relative-time
hass="[[hass]]"
datetime-obj="[[itemDate(item)]]"
></ha-relative-time>
</div>
<div class="value">[[itemValue(item)]]</div>
</div>
</template>
<div class="data-entry layout justified horizontal">
<div class="key">
[[localize('ui.dialogs.more_info_control.sun.elevation')]]
</div>
<div class="value">[[stateObj.attributes.elevation]]</div>
</div>
`;
}
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);

View File

@ -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`
<div class="row">
<div class="key">
<span
>${item === "ris"
? this.hass.localize(
"ui.dialogs.more_info_control.sun.rising"
)
: this.hass.localize(
"ui.dialogs.more_info_control.sun.setting"
)}</span
>
<ha-relative-time
.hass=${this.hass}
.datetimeObj=${item === "ris" ? risingDate : settingDate}
></ha-relative-time>
</div>
<div class="value">
${formatTime(
item === "ris" ? risingDate : settingDate,
this.hass.language
)}
</div>
</div>
`;
})}
<div class="row">
<div class="key">
${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")}
</div>
<div class="value">${this.stateObj.attributes.elevation}</div>
</div>
`;
}
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;
}
}

View File

@ -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`
<style>
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;
}
</style>
<div class="flex">
<iron-icon icon="hass:thermometer"></iron-icon>
<div class="main">
[[localize('ui.card.weather.attributes.temperature')]]
</div>
<div>
[[stateObj.attributes.temperature]] [[getUnit('temperature')]]
</div>
</div>
<template is="dom-if" if="[[_showValue(stateObj.attributes.pressure)]]">
<div class="flex">
<iron-icon icon="hass:gauge"></iron-icon>
<div class="main">
[[localize('ui.card.weather.attributes.air_pressure')]]
</div>
<div>
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
</div>
</div>
</template>
<template is="dom-if" if="[[_showValue(stateObj.attributes.humidity)]]">
<div class="flex">
<iron-icon icon="hass:water-percent"></iron-icon>
<div class="main">
[[localize('ui.card.weather.attributes.humidity')]]
</div>
<div>[[stateObj.attributes.humidity]] %</div>
</div>
</template>
<template is="dom-if" if="[[_showValue(stateObj.attributes.wind_speed)]]">
<div class="flex">
<iron-icon icon="hass:weather-windy"></iron-icon>
<div class="main">
[[localize('ui.card.weather.attributes.wind_speed')]]
</div>
<div>
[[getWind(stateObj.attributes.wind_speed,
stateObj.attributes.wind_bearing, localize)]]
</div>
</div>
</template>
<template is="dom-if" if="[[_showValue(stateObj.attributes.visibility)]]">
<div class="flex">
<iron-icon icon="hass:eye"></iron-icon>
<div class="main">
[[localize('ui.card.weather.attributes.visibility')]]
</div>
<div>[[stateObj.attributes.visibility]] [[getUnit('length')]]</div>
</div>
</template>
<template is="dom-if" if="[[stateObj.attributes.forecast]]">
<div class="section">[[localize('ui.card.weather.forecast')]]:</div>
<template is="dom-repeat" items="[[stateObj.attributes.forecast]]">
<div class="flex">
<template is="dom-if" if="[[_showValue(item.condition)]]">
<iron-icon icon="[[getWeatherIcon(item.condition)]]"></iron-icon>
</template>
<template is="dom-if" if="[[!_showValue(item.templow)]]">
<div class="main">[[computeDateTime(item.datetime)]]</div>
</template>
<template is="dom-if" if="[[_showValue(item.templow)]]">
<div class="main">[[computeDate(item.datetime)]]</div>
<div class="templow">
[[item.templow]] [[getUnit('temperature')]]
</div>
</template>
<div class="temp">
[[item.temperature]] [[getUnit('temperature')]]
</div>
</div>
</template>
</template>
<template is="dom-if" if="stateObj.attributes.attribution">
<div class="attribution">[[stateObj.attributes.attribution]]</div>
</template>
`;
}
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);

View File

@ -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`
<div class="flex">
<iron-icon icon="hass:thermometer"></iron-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.temperature")}
</div>
<div>
${this.stateObj.attributes.temperature} ${this.getUnit("temperature")}
</div>
</div>
${this.stateObj.attributes.pressure
? html`
<div class="flex">
<iron-icon icon="hass:gauge"></iron-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.air_pressure")}
</div>
<div>
${this.stateObj.attributes.pressure}
${this.getUnit("air_pressure")}
</div>
</div>
`
: ""}
${this.stateObj.attributes.humidity
? html`
<div class="flex">
<iron-icon icon="hass:water-percent"></iron-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.humidity")}
</div>
<div>${this.stateObj.attributes.humidity} %</div>
</div>
`
: ""}
${this.stateObj.attributes.wind_speed
? html`
<div class="flex">
<iron-icon icon="hass:weather-windy"></iron-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.wind_speed")}
</div>
<div>
${this.getWind(
this.stateObj.attributes.wind_speed,
this.stateObj.attributes.wind_bearing
)}
</div>
</div>
`
: ""}
${this.stateObj.attributes.visibility
? html`
<div class="flex">
<iron-icon icon="hass:eye"></iron-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.visibility")}
</div>
<div>
${this.stateObj.attributes.visibility} ${this.getUnit("length")}
</div>
</div>
`
: ""}
${this.stateObj.attributes.forecast
? html`
<div class="section">
${this.hass.localize("ui.card.weather.forecast")}:
</div>
${this.stateObj.attributes.forecast.map((item) => {
return html`
<div class="flex">
${item.condition
? html`
<iron-icon
.icon="${weatherIcons[item.condition]}"
></iron-icon>
`
: ""}
${!item.templow
? html`
<div class="main">
${this.computeDateTime(item.datetime)}
</div>
`
: ""}
${item.templow
? html`
<div class="main">
${this.computeDate(item.datetime)}
</div>
<div class="templow">
${item.templow} ${this.getUnit("temperature")}
</div>
`
: ""};
<div class="temp">
${item.temperature} ${this.getUnit("temperature")}
</div>
</div>
`;
})}
`
: ""}
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
${this.stateObj.attributes.attribution}
</div>
`
: ""}
`;
}
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;
}
}

View File

@ -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 { demoConfig } from "./demo_config";
import { demoServices } from "./demo_services"; import { demoServices } from "./demo_services";

View File

@ -1,45 +1,20 @@
import { import { LitElement, PropertyValues, property } from "lit-element";
Constructor, import { getLocalLanguage, getTranslation } from "../util/hass-translation";
LitElement,
PropertyDeclarations,
PropertyValues,
} from "lit-element";
import { getLocalLanguage } from "../util/hass-translation";
import { localizeLiteBaseMixin } from "./localize-lite-base-mixin";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize"; import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
import { Constructor, Resources } from "../types";
const empty = () => ""; const empty = () => "";
interface LitLocalizeLiteMixin { export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
language: string; superClass: T
resources: {}; ) => {
translationFragment: string; class LitLocalizeLiteClass extends superClass {
localize: LocalizeFunc; // Initialized to empty will prevent undefined errors if called before connected to DOM.
} @property() public localize: LocalizeFunc = empty;
@property() public resources?: Resources;
export const litLocalizeLiteMixin = <T extends LitElement>( // Use browser language setup before login.
superClass: Constructor<T> @property() public language?: string = getLocalLanguage();
): Constructor<T & LitLocalizeLiteMixin> => @property() public translationFragment?: string;
// @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();
}
public connectedCallback(): void { public connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
@ -51,7 +26,7 @@ export const litLocalizeLiteMixin = <T extends LitElement>(
); );
} }
public updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties); super.updated(changedProperties);
if ( if (
changedProperties.has("language") || changedProperties.has("language") ||
@ -64,4 +39,38 @@ export const litLocalizeLiteMixin = <T extends LitElement>(
); );
} }
} }
};
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;
};

View File

@ -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,
};
}
};

View File

@ -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
);
}
}
);

View File

@ -1,5 +1,5 @@
import { UpdatingElement, Constructor, PropertyValues } from "lit-element"; import { UpdatingElement, PropertyValues } from "lit-element";
import { HomeAssistant } from "../types"; import { HomeAssistant, Constructor } from "../types";
export interface ProvideHassElement { export interface ProvideHassElement {
provideHass(element: HTMLElement); provideHass(element: HTMLElement);
@ -7,9 +7,9 @@ export interface ProvideHassElement {
/* tslint:disable */ /* tslint:disable */
export const ProvideHassLitMixin = <T extends UpdatingElement>( export const ProvideHassLitMixin = <T extends Constructor<UpdatingElement>>(
superClass: Constructor<T> superClass: T
): Constructor<T & ProvideHassElement> => ) =>
// @ts-ignore // @ts-ignore
class extends superClass { class extends superClass {
protected hass!: HomeAssistant; protected hass!: HomeAssistant;

View File

@ -1,32 +1,21 @@
import { import { LitElement, PropertyValues, property } from "lit-element";
LitElement,
Constructor,
PropertyValues,
PropertyDeclarations,
} from "lit-element";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types"; import { HomeAssistant, Constructor } from "../types";
export interface HassSubscribeElement { export interface HassSubscribeElement {
hassSubscribe(): UnsubscribeFunc[]; hassSubscribe(): UnsubscribeFunc[];
} }
/* tslint:disable-next-line */ /* tslint:disable-next-line */
export const SubscribeMixin = <T extends LitElement>( export const SubscribeMixin = <T extends Constructor<LitElement>>(
superClass: Constructor<T> superClass: T
): Constructor<T & HassSubscribeElement> => ) => {
// @ts-ignore class SubscribeClass extends superClass {
class extends superClass { @property() public hass?: HomeAssistant;
private hass?: HomeAssistant;
/* tslint:disable-next-line */ /* tslint:disable-next-line */
private __unsubs?: UnsubscribeFunc[]; private __unsubs?: UnsubscribeFunc[];
static get properties(): PropertyDeclarations {
return {
hass: {},
};
}
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.__checkSubscribed(); this.__checkSubscribed();
@ -50,7 +39,6 @@ export const SubscribeMixin = <T extends LitElement>(
} }
protected hassSubscribe(): UnsubscribeFunc[] { protected hassSubscribe(): UnsubscribeFunc[] {
super.hassSubscribe();
return []; return [];
} }
@ -64,4 +52,6 @@ export const SubscribeMixin = <T extends LitElement>(
} }
this.__unsubs = this.hassSubscribe(); this.__unsubs = this.hassSubscribe();
} }
}; }
return SubscribeClass;
};

View File

@ -74,6 +74,7 @@ class IntegrationBadge extends LitElement {
.title { .title {
min-height: 2.3em; min-height: 2.3em;
word-break: break-word;
} }
`; `;
} }

View File

@ -2,9 +2,9 @@ import {
LitElement, LitElement,
html, html,
css, css,
PropertyDeclarations,
CSSResult, CSSResult,
TemplateResult, TemplateResult,
property,
} from "lit-element"; } from "lit-element";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
@ -17,19 +17,11 @@ import { HomeAssistant } from "../../../types";
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry"; import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
class DialogAreaDetail extends LitElement { class DialogAreaDetail extends LitElement {
public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
private _name!: string; @property() private _name!: string;
private _error?: string; @property() private _error?: string;
private _params?: AreaRegistryDetailDialogParams; @property() private _params?: AreaRegistryDetailDialogParams;
private _submitting?: boolean; @property() private _submitting?: boolean;
static get properties(): PropertyDeclarations {
return {
_error: {},
_name: {},
_params: {},
};
}
public async showDialog( public async showDialog(
params: AreaRegistryDetailDialogParams params: AreaRegistryDetailDialogParams

View File

@ -4,8 +4,8 @@ import {
html, html,
CSSResult, CSSResult,
css, css,
PropertyDeclarations,
PropertyValues, PropertyValues,
property,
} from "lit-element"; } from "lit-element";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
@ -38,26 +38,14 @@ function AutomationEditor(mountEl, props, mergeEl) {
} }
export class HaAutomationEditor extends LitElement { export class HaAutomationEditor extends LitElement {
public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
public automation!: AutomationEntity; @property() public automation!: AutomationEntity;
public isWide?: boolean; @property() public isWide?: boolean;
public creatingNew?: boolean; @property() public creatingNew?: boolean;
private _config?: AutomationConfig; @property() private _config?: AutomationConfig;
private _dirty?: boolean; @property() private _dirty?: boolean;
private _rendered?: unknown; private _rendered?: unknown;
private _errors?: string; @property() private _errors?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
automation: {},
creatingNew: {},
isWide: {},
_errors: {},
_dirty: {},
_config: {},
};
}
constructor() { constructor() {
super(); super();

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