diff --git a/.gitignore b/.gitignore index f9a704e5d9..743889ba4e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ node_modules/* npm-debug.log .DS_Store hass_frontend/* -hass_frontend_es5/* .reify-cache demo/hademo-icons.html diff --git a/.nvmrc b/.nvmrc index 2f76972966..40e6bd96a6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -8.11.1 +12.1 diff --git a/gulp/.eslintrc b/build-scripts/.eslintrc similarity index 100% rename from gulp/.eslintrc rename to build-scripts/.eslintrc diff --git a/config/.eslintrc.json b/build-scripts/.eslintrc.json similarity index 100% rename from config/.eslintrc.json rename to build-scripts/.eslintrc.json diff --git a/config/babel.js b/build-scripts/babel.js similarity index 100% rename from config/babel.js rename to build-scripts/babel.js diff --git a/build-scripts/gulp/clean.js b/build-scripts/gulp/clean.js new file mode 100644 index 0000000000..d3eaed526a --- /dev/null +++ b/build-scripts/gulp/clean.js @@ -0,0 +1,5 @@ +const del = require("del"); +const gulp = require("gulp"); +const config = require("../paths"); + +gulp.task("clean", () => del([config.root, config.build_dir])); diff --git a/build-scripts/gulp/develop.js b/build-scripts/gulp/develop.js new file mode 100644 index 0000000000..2dbcbaac45 --- /dev/null +++ b/build-scripts/gulp/develop.js @@ -0,0 +1,29 @@ +// Run HA 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", + gulp.series( + async function setEnv() { + process.env.NODE_ENV = "development"; + }, + "clean", + gulp.parallel( + "copy-static", + "gen-service-worker-dev", + "gen-icons", + "gen-pages-dev", + "gen-index-html-dev", + gulp.series("build-translations", "copy-translations") + ), + "webpack-watch" + ) +); diff --git a/build-scripts/gulp/entry-html.js b/build-scripts/gulp/entry-html.js new file mode 100644 index 0000000000..235a7336d5 --- /dev/null +++ b/build-scripts/gulp/entry-html.js @@ -0,0 +1,108 @@ +// Tasks to generate entry HTML +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable global-require */ +const gulp = require("gulp"); +const fs = require("fs-extra"); +const path = require("path"); +const template = require("lodash.template"); +const minify = require("html-minifier").minify; +const config = require("../paths.js"); + +const templatePath = (tpl) => + path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`); + +const readFile = (pth) => fs.readFileSync(pth).toString(); + +const renderTemplate = (pth, data = {}) => { + const compiled = template(readFile(templatePath(pth))); + return compiled({ ...data, renderTemplate }); +}; + +const minifyHtml = (content) => + minify(content, { + collapseWhitespace: true, + minifyJS: true, + minifyCSS: true, + removeComments: true, + }); + +const PAGES = ["onboarding", "authorize"]; + +gulp.task("gen-pages-dev", (done) => { + for (const page of PAGES) { + const content = renderTemplate(page, { + latestPageJS: `/frontend_latest/${page}.js`, + latestHassIconsJS: "/frontend_latest/hass-icons.js", + + es5Compatibility: "/frontend_es5/compatibility.js", + es5PageJS: `/frontend_es5/${page}.js`, + es5HassIconsJS: "/frontend_es5/hass-icons.js", + }); + + fs.outputFileSync(path.resolve(config.root, `${page}.html`), content); + } + done(); +}); + +gulp.task("gen-pages-prod", (done) => { + const latestManifest = require(path.resolve(config.output, "manifest.json")); + const es5Manifest = require(path.resolve(config.output_es5, "manifest.json")); + + for (const page of PAGES) { + const content = renderTemplate(page, { + latestPageJS: latestManifest[`${page}.js`], + latestHassIconsJS: latestManifest["hass-icons.js"], + + es5Compatibility: es5Manifest["compatibility.js"], + es5PageJS: es5Manifest[`${page}.js`], + es5HassIconsJS: es5Manifest["hass-icons.js"], + }); + + fs.outputFileSync( + path.resolve(config.root, `${page}.html`), + minifyHtml(content) + ); + } + done(); +}); + +gulp.task("gen-index-html-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 = renderTemplate("index", { + latestAppJS: "/frontend_latest/app.js", + latestCoreJS: "/frontend_latest/core.js", + latestCustomPanelJS: "/frontend_latest/custom-panel.js", + latestHassIconsJS: "/frontend_latest/hass-icons.js", + + es5Compatibility: "/frontend_es5/compatibility.js", + es5AppJS: "/frontend_es5/app.js", + es5CoreJS: "/frontend_es5/core.js", + es5CustomPanelJS: "/frontend_es5/custom-panel.js", + es5HassIconsJS: "/frontend_es5/hass-icons.js", + }); + + fs.outputFileSync(path.resolve(config.root, "index.html"), content); + done(); +}); + +gulp.task("gen-index-html-prod", (done) => { + const latestManifest = require(path.resolve(config.output, "manifest.json")); + const es5Manifest = require(path.resolve(config.output_es5, "manifest.json")); + const content = renderTemplate("index", { + latestAppJS: latestManifest["app.js"], + latestCoreJS: latestManifest["core.js"], + latestCustomPanelJS: latestManifest["custom-panel.js"], + latestHassIconsJS: latestManifest["hass-icons.js"], + + es5Compatibility: es5Manifest["compatibility.js"], + es5AppJS: es5Manifest["app.js"], + es5CoreJS: es5Manifest["core.js"], + es5CustomPanelJS: es5Manifest["custom-panel.js"], + es5HassIconsJS: es5Manifest["hass-icons.js"], + }); + const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}"); + + fs.outputFileSync(path.resolve(config.root, "index.html"), minified); + done(); +}); diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js new file mode 100644 index 0000000000..733c0e876c --- /dev/null +++ b/build-scripts/gulp/gather-static.js @@ -0,0 +1,87 @@ +// Gulp task to gather all static files. + +const gulp = require("gulp"); +const path = require("path"); +const fs = require("fs-extra"); +const zopfli = require("gulp-zopfli-green"); +const merge = require("merge-stream"); +const config = require("../paths"); + +const npmPath = (...parts) => + path.resolve(config.polymer_dir, "node_modules", ...parts); +const polyPath = (...parts) => path.resolve(config.polymer_dir, ...parts); +const staticPath = (...parts) => path.resolve(config.root, "static", ...parts); + +const copyFileDir = (fromFile, toDir) => + fs.copySync(fromFile, path.join(toDir, path.basename(fromFile))); + +function copyTranslations() { + // Translation output + fs.copySync( + polyPath("build-translations/output"), + staticPath("translations") + ); +} + +function copyStatic() { + // Basic static files + fs.copySync(polyPath("public"), config.root); + + // Web Component polyfills and adapters + copyFileDir( + npmPath("@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"), + staticPath("polyfills/") + ); + copyFileDir( + npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js"), + staticPath("polyfills/") + ); + copyFileDir( + npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"), + staticPath("polyfills/") + ); + + // Local fonts + fs.copySync(npmPath("@polymer/font-roboto-local/fonts"), staticPath("fonts")); + + // External dependency assets + copyFileDir( + npmPath("react-big-calendar/lib/css/react-big-calendar.css"), + staticPath("panels/calendar/") + ); + copyFileDir( + npmPath("leaflet/dist/leaflet.css"), + staticPath("images/leaflet/") + ); + fs.copySync( + npmPath("leaflet/dist/images"), + staticPath("images/leaflet/images/") + ); +} + +gulp.task("copy-static", (done) => { + copyStatic(); + done(); +}); + +gulp.task("compress-static", () => { + const fonts = gulp + .src(staticPath("fonts/**/*.ttf")) + .pipe(zopfli()) + .pipe(gulp.dest(staticPath("fonts"))); + 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(fonts, polyfills, translations); +}); + +gulp.task("copy-translations", (done) => { + copyTranslations(); + done(); +}); diff --git a/gulp/tasks/gen-icons.js b/build-scripts/gulp/gen-icons.js similarity index 88% rename from gulp/tasks/gen-icons.js rename to build-scripts/gulp/gen-icons.js index c47b42f360..6563b9a8f1 100644 --- a/gulp/tasks/gen-icons.js +++ b/build-scripts/gulp/gen-icons.js @@ -1,7 +1,6 @@ const gulp = require("gulp"); const path = require("path"); const fs = require("fs"); -const config = require("../config"); const ICON_PACKAGE_PATH = path.resolve( __dirname, @@ -38,12 +37,12 @@ function loadIcon(name) { function transformXMLtoPolymer(name, xml) { const start = xml.indexOf(">${path}`; + const pth = xml.substr(start, end); + return `${pth}`; } // Given an iconset name and icon names, generate a polymer iconset -function generateIconset(name, iconNames) { +function generateIconset(iconsetName, iconNames) { const iconDefs = Array.from(iconNames) .map((name) => { const iconDef = loadIcon(name); @@ -53,7 +52,7 @@ function generateIconset(name, iconNames) { return transformXMLtoPolymer(name, iconDef); }) .join(""); - return `${iconDefs}`; + return `${iconDefs}`; } // Generate the full MDI iconset @@ -62,7 +61,9 @@ function genMDIIcons() { fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8") ); const iconNames = meta.map((iconInfo) => iconInfo.name); - fs.existsSync(OUTPUT_DIR) || fs.mkdirSync(OUTPUT_DIR); + if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR); + } fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames)); } @@ -81,7 +82,7 @@ function mapFiles(startPath, filter, mapFunc) { } // Find all icons used by the project. -function findIcons(path, iconsetName) { +function findIcons(searchPath, iconsetName) { const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g"); const icons = new Set(); function processFile(filename) { @@ -93,8 +94,8 @@ function findIcons(path, iconsetName) { icons.add(match[0].substr(iconsetName.length + 1)); } } - mapFiles(path, ".js", processFile); - mapFiles(path, ".ts", processFile); + mapFiles(searchPath, ".js", processFile); + mapFiles(searchPath, ".ts", processFile); return icons; } diff --git a/build-scripts/gulp/release.js b/build-scripts/gulp/release.js new file mode 100644 index 0000000000..f7e26b45ca --- /dev/null +++ b/build-scripts/gulp/release.js @@ -0,0 +1,31 @@ +// Run HA 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( + "build-release", + gulp.series( + async function setEnv() { + process.env.NODE_ENV = "production"; + }, + "clean", + gulp.parallel( + "copy-static", + "gen-icons", + gulp.series("build-translations", "copy-translations") + ), + gulp.parallel("webpack-prod", "compress-static"), + gulp.parallel( + "gen-pages-prod", + "gen-index-html-prod", + "gen-service-worker-prod" + ) + ) +); diff --git a/build-scripts/gulp/service-worker.js b/build-scripts/gulp/service-worker.js new file mode 100644 index 0000000000..1303983790 --- /dev/null +++ b/build-scripts/gulp/service-worker.js @@ -0,0 +1,29 @@ +// Generate service worker. +// Based on manifest, create a file with the content as service_worker.js +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable global-require */ +const gulp = require("gulp"); +const path = require("path"); +const fs = require("fs-extra"); +const config = require("../paths.js"); + +const swPath = path.resolve(config.root, "service_worker.js"); + +const writeSW = (content) => fs.outputFileSync(swPath, content.trim() + "\n"); + +gulp.task("gen-service-worker-dev", (done) => { + writeSW( + ` +console.debug('Service worker disabled in development'); + ` + ); + done(); +}); + +gulp.task("gen-service-worker-prod", (done) => { + fs.copySync( + path.resolve(config.output, "service_worker.js"), + path.resolve(config.root, "service_worker.js") + ); + done(); +}); diff --git a/gulp/tasks/translations.js b/build-scripts/gulp/translations.js similarity index 100% rename from gulp/tasks/translations.js rename to build-scripts/gulp/translations.js diff --git a/build-scripts/gulp/webpack.js b/build-scripts/gulp/webpack.js new file mode 100644 index 0000000000..6ba6fa1e4a --- /dev/null +++ b/build-scripts/gulp/webpack.js @@ -0,0 +1,63 @@ +// Tasks to run webpack. +const gulp = require("gulp"); +const webpack = require("webpack"); +const { createAppConfig } = require("../webpack"); + +const handler = (done) => (err, stats) => { + if (err) { + console.log(err.stack || err); + if (err.details) { + console.log(err.details); + } + return; + } + + console.log(`Build done @ ${new Date().toLocaleTimeString()}`); + + if (stats.hasErrors() || stats.hasWarnings()) { + console.log(stats.toString("minimal")); + } + + if (done) { + done(); + } +}; + +gulp.task("webpack-watch", () => { + 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 +}); + +gulp.task( + "webpack-prod", + () => + new Promise((resolve) => + webpack( + [ + createAppConfig({ + isProdBuild: true, + latestBuild: true, + isStatsBuild: false, + }), + createAppConfig({ + isProdBuild: true, + latestBuild: false, + isStatsBuild: false, + }), + ], + handler(resolve) + ) + ) +); diff --git a/build-scripts/paths.js b/build-scripts/paths.js new file mode 100644 index 0000000000..28d553717b --- /dev/null +++ b/build-scripts/paths.js @@ -0,0 +1,10 @@ +var path = require("path"); + +module.exports = { + polymer_dir: path.resolve(__dirname, ".."), + build_dir: path.resolve(__dirname, "../build"), + root: path.resolve(__dirname, "../hass_frontend"), + static: path.resolve(__dirname, "../hass_frontend/static"), + output: path.resolve(__dirname, "../hass_frontend/frontend_latest"), + output_es5: path.resolve(__dirname, "../hass_frontend/frontend_es5"), +}; diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js new file mode 100644 index 0000000000..1a48454661 --- /dev/null +++ b/build-scripts/webpack.js @@ -0,0 +1,184 @@ +const webpack = require("webpack"); +const fs = require("fs"); +const path = require("path"); +const TerserPlugin = require("terser-webpack-plugin"); +const WorkboxPlugin = require("workbox-webpack-plugin"); +const CompressionPlugin = require("compression-webpack-plugin"); +const zopfli = require("@gfx/zopfli"); +const ManifestPlugin = require("webpack-manifest-plugin"); +const paths = require("./paths.js"); +const { babelLoaderConfig } = require("./babel.js"); + +let version = fs + .readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8") + .match(/\d{8}\.\d+/); +if (!version) { + throw Error("Version not found"); +} +version = version[0]; + +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 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, + terserOptions: { + 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 publicPath = latestBuild ? "/frontend_latest/" : "/frontend_es5/"; + + 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", + }; + + return { + mode: isProdBuild ? "production" : "development", + devtool: isProdBuild + ? "cheap-source-map " + : "inline-cheap-module-source-map", + entry, + module: { + rules: [ + babelLoaderConfig({ latestBuild }), + { + test: /\.css$/, + use: "raw-loader", + }, + { + test: /\.(html)$/, + use: { + loader: "html-loader", + options: { + exportAsEs6Default: true, + }, + }, + }, + ], + }, + optimization: optimization(latestBuild), + plugins: [ + new ManifestPlugin(), + new webpack.DefinePlugin({ + __DEV__: JSON.stringify(!isProdBuild), + __DEMO__: false, + __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"), + __VERSION__: JSON.stringify(version), + __STATIC_PATH__: "/static/", + "process.env.NODE_ENV": JSON.stringify( + isProdBuild ? "production" : "development" + ), + }), + ...plugins, + isProdBuild && + !isCI && + !isStatsBuild && + new CompressionPlugin({ + cache: true, + exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/], + algorithm(input, compressionOptions, callback) { + return zopfli.gzip(input, compressionOptions, callback); + }, + }), + latestBuild && + new WorkboxPlugin.InjectManifest({ + swSrc: "./src/entrypoints/service-worker-hass.js", + swDest: "service_worker.js", + importWorkboxFrom: "local", + include: [/\.js$/], + templatedURLs: { + ...workBoxTranslationsTemplatedURLs, + "/static/icons/favicon-192x192.png": + "public/icons/favicon-192x192.png", + "/static/fonts/roboto/Roboto-Light.ttf": + "node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Light.ttf", + "/static/fonts/roboto/Roboto-Medium.ttf": + "node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Medium.ttf", + "/static/fonts/roboto/Roboto-Regular.ttf": + "node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Regular.ttf", + "/static/fonts/roboto/Roboto-Bold.ttf": + "node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Bold.ttf", + }, + }), + ].filter(Boolean), + output: { + filename: ({ chunk }) => { + const dontHash = new Set([ + // Files who'se names should not be hashed. + // We currently have none. + ]); + 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: latestBuild ? paths.output : paths.output_es5, + publicPath, + }, + resolve, + }; +}; + +module.exports = { + resolve, + plugins, + optimization, + createAppConfig, +}; diff --git a/config/webpack.js b/config/webpack.js deleted file mode 100644 index 9c61f7fb84..0000000000 --- a/config/webpack.js +++ /dev/null @@ -1,48 +0,0 @@ -const webpack = require("webpack"); -const path = require("path"); -const TerserPlugin = require("terser-webpack-plugin"); - -module.exports.resolve = { - extensions: [".ts", ".js", ".json", ".tsx"], - alias: { - react: "preact-compat", - "react-dom": "preact-compat", - // Not necessary unless you consume a module using `createClass` - "create-react-class": "preact-compat/lib/create-react-class", - // Not necessary unless you consume a module requiring `react-dom-factories` - "react-dom-factories": "preact-compat/lib/react-dom-factories", - }, -}; - -module.exports.plugins = [ - // Ignore moment.js locales - new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), - // Color.js is bloated, it contains all color definitions for all material color sets. - new webpack.NormalModuleReplacementPlugin( - /@polymer\/paper-styles\/color\.js$/, - path.resolve(__dirname, "../src/util/empty.js") - ), - // Ignore roboto pointing at CDN. We use local font-roboto-local. - new webpack.NormalModuleReplacementPlugin( - /@polymer\/font-roboto\/roboto\.js$/, - path.resolve(__dirname, "../src/util/empty.js") - ), - // Ignore mwc icons pointing at CDN. - new webpack.NormalModuleReplacementPlugin( - /@material\/mwc-icon\/mwc-icon-font\.js$/, - path.resolve(__dirname, "../src/util/empty.js") - ), -]; - -module.exports.optimization = (latestBuild) => ({ - minimizer: [ - new TerserPlugin({ - cache: true, - parallel: true, - extractComments: true, - terserOptions: { - ecma: latestBuild ? undefined : 5, - }, - }), - ], -}); diff --git a/demo/script/gen-icons.js b/demo/script/gen-icons.js index 40a0f15a0d..b3c20a842a 100755 --- a/demo/script/gen-icons.js +++ b/demo/script/gen-icons.js @@ -4,7 +4,7 @@ const { findIcons, generateIconset, genMDIIcons, -} = require("../../gulp/tasks/gen-icons.js"); +} = require("../../build-scripts/gulp/gen-icons.js"); function genHademoIcons() { const iconNames = findIcons("./src", "hademo"); diff --git a/demo/webpack.config.js b/demo/webpack.config.js index 35c1ccdc93..a58d562e52 100644 --- a/demo/webpack.config.js +++ b/demo/webpack.config.js @@ -2,8 +2,8 @@ const path = require("path"); const webpack = require("webpack"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const WorkboxPlugin = require("workbox-webpack-plugin"); -const { babelLoaderConfig } = require("../config/babel.js"); -const webpackBase = require("../config/webpack.js"); +const { babelLoaderConfig } = require("../build-scripts/babel.js"); +const webpackBase = require("../build-scripts/webpack.js"); const isProd = process.env.NODE_ENV === "production"; const isStatsBuild = process.env.STATS === "1"; @@ -72,7 +72,7 @@ module.exports = { ...webpackBase.plugins, isProd && new WorkboxPlugin.GenerateSW({ - swDest: "service_worker_es5.js", + swDest: "service_worker.js", importWorkboxFrom: "local", include: [], }), diff --git a/gallery/webpack.config.js b/gallery/webpack.config.js index 0db9c2f935..b252c03551 100644 --- a/gallery/webpack.config.js +++ b/gallery/webpack.config.js @@ -1,7 +1,7 @@ const path = require("path"); const CopyWebpackPlugin = require("copy-webpack-plugin"); -const { babelLoaderConfig } = require("../config/babel.js"); -const webpackBase = require("../config/webpack.js"); +const { babelLoaderConfig } = require("../build-scripts/babel.js"); +const webpackBase = require("../build-scripts/webpack.js"); const isProd = process.env.NODE_ENV === "production"; const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js"; diff --git a/gulp/config.js b/gulp/config.js deleted file mode 100644 index b011a04cf3..0000000000 --- a/gulp/config.js +++ /dev/null @@ -1,8 +0,0 @@ -var path = require("path"); - -module.exports = { - polymer_dir: path.resolve(__dirname, ".."), - build_dir: path.resolve(__dirname, "../build"), - output: path.resolve(__dirname, "../hass_frontend"), - output_es5: path.resolve(__dirname, "../hass_frontend_es5"), -}; diff --git a/gulpfile.js b/gulpfile.js index f3cb47a7e6..78578bad7b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,3 +1,3 @@ -var requireDir = require('require-dir'); +var requireDir = require("require-dir"); -requireDir('./gulp/tasks/'); +requireDir("./build-scripts/gulp/"); diff --git a/hassio/script/gen-icons.js b/hassio/script/gen-icons.js index 61fc39e212..b355ef752b 100755 --- a/hassio/script/gen-icons.js +++ b/hassio/script/gen-icons.js @@ -4,7 +4,7 @@ const { findIcons, generateIconset, genMDIIcons, -} = require("../../gulp/tasks/gen-icons.js"); +} = require("../../build-scripts/gulp/gen-icons.js"); function genHassioIcons() { const iconNames = findIcons("./src", "hassio"); diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts index 6ced46ca76..3d1cc54cdd 100644 --- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts +++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts @@ -6,7 +6,7 @@ import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-input/paper-input"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { getSignedPath } from "../../../../src/auth/data"; +import { getSignedPath } from "../../../../src/data/auth"; import "../../../../src/resources/ha-style"; import "../../../../src/components/dialog/ha-paper-dialog"; diff --git a/hassio/webpack.config.js b/hassio/webpack.config.js index 022bbb563c..e58d2df301 100644 --- a/hassio/webpack.config.js +++ b/hassio/webpack.config.js @@ -3,8 +3,8 @@ const CompressionPlugin = require("compression-webpack-plugin"); const zopfli = require("@gfx/zopfli"); const config = require("./config.js"); -const { babelLoaderConfig } = require("../config/babel.js"); -const webpackBase = require("../config/webpack.js"); +const { babelLoaderConfig } = require("../build-scripts/babel.js"); +const webpackBase = require("../build-scripts/webpack.js"); const isProdBuild = process.env.NODE_ENV === "production"; const isCI = process.env.CI === "true"; diff --git a/package.json b/package.json index fc793d7e09..5b3213a8a7 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "eslint-plugin-import": "^2.16.0", "eslint-plugin-prettier": "^3.0.1", "eslint-plugin-react": "^7.12.4", + "fs-extra": "^7.0.1", "gulp": "^4.0.0", "gulp-foreach": "^0.1.0", "gulp-hash": "^4.2.2", @@ -134,10 +135,12 @@ "gulp-jsonminify": "^1.1.0", "gulp-merge-json": "^1.3.1", "gulp-rename": "^1.4.0", + "gulp-zopfli-green": "^3.0.1", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "husky": "^1.3.1", "lint-staged": "^8.1.5", + "lodash.template": "^4.4.0", "merge-stream": "^1.0.1", "mocha": "^6.0.2", "parse5": "^5.1.0", @@ -160,6 +163,7 @@ "webpack": "^4.29.6", "webpack-cli": "^3.3.0", "webpack-dev-server": "^3.2.1", + "webpack-manifest-plugin": "^2.0.4", "workbox-webpack-plugin": "^4.1.1" }, "resolutions": { diff --git a/public/__init__.py b/public/__init__.py index 50b6b4a508..1debde5485 100644 --- a/public/__init__.py +++ b/public/__init__.py @@ -1,30 +1,7 @@ """Frontend for Home Assistant.""" -import os -from user_agents import parse - -FAMILY_MIN_VERSION = { - 'Chrome': 55, # Async/await - 'Chrome Mobile': 55, - 'Firefox': 52, # Async/await - 'Firefox Mobile': 52, - 'Opera': 42, # Async/await - 'Edge': 15, # Async/await - 'Safari': 10.1, # Async/await -} +from pathlib import Path def where(): """Return path to the frontend.""" - return os.path.dirname(__file__) - - -def version(useragent): - """Get the version for given user agent.""" - useragent = parse(useragent) - - # on iOS every browser uses the Safari engine - if useragent.os.family == 'iOS': - return useragent.os.version[0] >= FAMILY_MIN_VERSION['Safari'] - - version = FAMILY_MIN_VERSION.get(useragent.browser.family) - return version and useragent.browser.version[0] >= version + return Path(__file__).parent diff --git a/public/icons/favicon-1024x1024.png b/public/static/icons/favicon-1024x1024.png similarity index 100% rename from public/icons/favicon-1024x1024.png rename to public/static/icons/favicon-1024x1024.png diff --git a/public/icons/favicon-192x192.png b/public/static/icons/favicon-192x192.png similarity index 100% rename from public/icons/favicon-192x192.png rename to public/static/icons/favicon-192x192.png diff --git a/public/icons/favicon-384x384.png b/public/static/icons/favicon-384x384.png similarity index 100% rename from public/icons/favicon-384x384.png rename to public/static/icons/favicon-384x384.png diff --git a/public/icons/favicon-512x512.png b/public/static/icons/favicon-512x512.png similarity index 100% rename from public/icons/favicon-512x512.png rename to public/static/icons/favicon-512x512.png diff --git a/public/icons/favicon-apple-180x180.png b/public/static/icons/favicon-apple-180x180.png similarity index 100% rename from public/icons/favicon-apple-180x180.png rename to public/static/icons/favicon-apple-180x180.png diff --git a/public/icons/favicon.ico b/public/static/icons/favicon.ico similarity index 100% rename from public/icons/favicon.ico rename to public/static/icons/favicon.ico diff --git a/public/icons/mask-icon.svg b/public/static/icons/mask-icon.svg similarity index 100% rename from public/icons/mask-icon.svg rename to public/static/icons/mask-icon.svg diff --git a/public/icons/tile-win-150x150.png b/public/static/icons/tile-win-150x150.png similarity index 100% rename from public/icons/tile-win-150x150.png rename to public/static/icons/tile-win-150x150.png diff --git a/public/icons/tile-win-310x150.png b/public/static/icons/tile-win-310x150.png similarity index 100% rename from public/icons/tile-win-310x150.png rename to public/static/icons/tile-win-310x150.png diff --git a/public/icons/tile-win-310x310.png b/public/static/icons/tile-win-310x310.png similarity index 100% rename from public/icons/tile-win-310x310.png rename to public/static/icons/tile-win-310x310.png diff --git a/public/icons/tile-win-70x70.png b/public/static/icons/tile-win-70x70.png similarity index 100% rename from public/icons/tile-win-70x70.png rename to public/static/icons/tile-win-70x70.png diff --git a/public/images/card_media_player_bg.png b/public/static/images/card_media_player_bg.png similarity index 100% rename from public/images/card_media_player_bg.png rename to public/static/images/card_media_player_bg.png diff --git a/public/images/config_ecobee_thermostat.png b/public/static/images/config_ecobee_thermostat.png similarity index 100% rename from public/images/config_ecobee_thermostat.png rename to public/static/images/config_ecobee_thermostat.png diff --git a/public/images/config_fitbit_app.png b/public/static/images/config_fitbit_app.png similarity index 100% rename from public/images/config_fitbit_app.png rename to public/static/images/config_fitbit_app.png diff --git a/public/images/config_flows/config_homematicip_cloud.png b/public/static/images/config_flows/config_homematicip_cloud.png similarity index 100% rename from public/images/config_flows/config_homematicip_cloud.png rename to public/static/images/config_flows/config_homematicip_cloud.png diff --git a/public/images/config_icloud.png b/public/static/images/config_icloud.png similarity index 100% rename from public/images/config_icloud.png rename to public/static/images/config_icloud.png diff --git a/public/images/config_insteon.png b/public/static/images/config_insteon.png similarity index 100% rename from public/images/config_insteon.png rename to public/static/images/config_insteon.png diff --git a/public/images/config_philips_hue.jpg b/public/static/images/config_philips_hue.jpg similarity index 100% rename from public/images/config_philips_hue.jpg rename to public/static/images/config_philips_hue.jpg diff --git a/public/images/config_webos.png b/public/static/images/config_webos.png similarity index 100% rename from public/images/config_webos.png rename to public/static/images/config_webos.png diff --git a/public/images/config_wink.png b/public/static/images/config_wink.png similarity index 100% rename from public/images/config_wink.png rename to public/static/images/config_wink.png diff --git a/public/images/darksky/weather-cloudy.svg b/public/static/images/darksky/weather-cloudy.svg similarity index 100% rename from public/images/darksky/weather-cloudy.svg rename to public/static/images/darksky/weather-cloudy.svg diff --git a/public/images/darksky/weather-fog.svg b/public/static/images/darksky/weather-fog.svg similarity index 100% rename from public/images/darksky/weather-fog.svg rename to public/static/images/darksky/weather-fog.svg diff --git a/public/images/darksky/weather-hail.svg b/public/static/images/darksky/weather-hail.svg similarity index 100% rename from public/images/darksky/weather-hail.svg rename to public/static/images/darksky/weather-hail.svg diff --git a/public/images/darksky/weather-night.svg b/public/static/images/darksky/weather-night.svg similarity index 100% rename from public/images/darksky/weather-night.svg rename to public/static/images/darksky/weather-night.svg diff --git a/public/images/darksky/weather-partlycloudy.svg b/public/static/images/darksky/weather-partlycloudy.svg similarity index 100% rename from public/images/darksky/weather-partlycloudy.svg rename to public/static/images/darksky/weather-partlycloudy.svg diff --git a/public/images/darksky/weather-pouring.svg b/public/static/images/darksky/weather-pouring.svg similarity index 100% rename from public/images/darksky/weather-pouring.svg rename to public/static/images/darksky/weather-pouring.svg diff --git a/public/images/darksky/weather-rainy.svg b/public/static/images/darksky/weather-rainy.svg similarity index 100% rename from public/images/darksky/weather-rainy.svg rename to public/static/images/darksky/weather-rainy.svg diff --git a/public/images/darksky/weather-snowy.svg b/public/static/images/darksky/weather-snowy.svg similarity index 100% rename from public/images/darksky/weather-snowy.svg rename to public/static/images/darksky/weather-snowy.svg diff --git a/public/images/darksky/weather-sunny.svg b/public/static/images/darksky/weather-sunny.svg similarity index 100% rename from public/images/darksky/weather-sunny.svg rename to public/static/images/darksky/weather-sunny.svg diff --git a/public/images/darksky/weather-windy.svg b/public/static/images/darksky/weather-windy.svg similarity index 100% rename from public/images/darksky/weather-windy.svg rename to public/static/images/darksky/weather-windy.svg diff --git a/public/images/image-broken.svg b/public/static/images/image-broken.svg similarity index 100% rename from public/images/image-broken.svg rename to public/static/images/image-broken.svg diff --git a/public/images/logo_automatic.png b/public/static/images/logo_automatic.png similarity index 100% rename from public/images/logo_automatic.png rename to public/static/images/logo_automatic.png diff --git a/public/images/logo_axis.png b/public/static/images/logo_axis.png similarity index 100% rename from public/images/logo_axis.png rename to public/static/images/logo_axis.png diff --git a/public/images/logo_deconz.jpeg b/public/static/images/logo_deconz.jpeg similarity index 100% rename from public/images/logo_deconz.jpeg rename to public/static/images/logo_deconz.jpeg diff --git a/public/images/logo_philips_hue.png b/public/static/images/logo_philips_hue.png similarity index 100% rename from public/images/logo_philips_hue.png rename to public/static/images/logo_philips_hue.png diff --git a/public/images/logo_plex_mediaserver.png b/public/static/images/logo_plex_mediaserver.png similarity index 100% rename from public/images/logo_plex_mediaserver.png rename to public/static/images/logo_plex_mediaserver.png diff --git a/public/images/notification-badge.png b/public/static/images/notification-badge.png similarity index 100% rename from public/images/notification-badge.png rename to public/static/images/notification-badge.png diff --git a/public/images/smart-tv.png b/public/static/images/smart-tv.png similarity index 100% rename from public/images/smart-tv.png rename to public/static/images/smart-tv.png diff --git a/script/build_frontend b/script/build_frontend index 81b4986f3b..eddb895a53 100755 --- a/script/build_frontend +++ b/script/build_frontend @@ -6,19 +6,4 @@ set -e cd "$(dirname "$0")/.." -BUILD_DIR=build -BUILD_TRANSLATIONS_DIR=build-translations -OUTPUT_DIR=hass_frontend -OUTPUT_DIR_ES5=hass_frontend_es5 - -rm -rf $OUTPUT_DIR $OUTPUT_DIR_ES5 $BUILD_DIR $BUILD_TRANSLATIONS_DIR - -# Build frontend -./node_modules/.bin/gulp build-translations gen-icons -NODE_ENV=production ./node_modules/.bin/webpack - -# Generate the __init__ file -echo "VERSION = '`git rev-parse HEAD`'" >> $OUTPUT_DIR/__init__.py -echo "CREATED_AT = `date +%s`" >> $OUTPUT_DIR/__init__.py -echo "VERSION = '`git rev-parse HEAD`'" >> $OUTPUT_DIR_ES5/__init__.py -echo "CREATED_AT = `date +%s`" >> $OUTPUT_DIR_ES5/__init__.py +./node_modules/.bin/gulp build-release diff --git a/script/develop b/script/develop index 24c1253dc0..f0be37820c 100755 --- a/script/develop +++ b/script/develop @@ -6,14 +6,4 @@ set -e cd "$(dirname "$0")/.." -BUILD_DIR=build -OUTPUT_DIR=hass_frontend -OUTPUT_DIR_ES5=hass_frontend_es5 - -rm -rf $OUTPUT_DIR $OUTPUT_DIR_ES5 $BUILD_DIR -mkdir $OUTPUT_DIR $OUTPUT_DIR_ES5 -# Needed in case frontend repo installed with pip3 install -e -cp -r public/__init__.py $OUTPUT_DIR_ES5/ - -./node_modules/.bin/gulp build-translations gen-icons -./node_modules/.bin/webpack --watch --progress +./node_modules/.bin/gulp develop diff --git a/script/size_stats b/script/size_stats index 4b6601a480..f6c95545eb 100755 --- a/script/size_stats +++ b/script/size_stats @@ -7,5 +7,5 @@ set -e cd "$(dirname "$0")/.." STATS=1 NODE_ENV=production webpack --profile --json > compilation-stats.json -npx webpack-bundle-analyzer compilation-stats.json hass_frontend +npx webpack-bundle-analyzer compilation-stats.json hass_frontend/frontend_latest rm compilation-stats.json diff --git a/setup.py b/setup.py index 0e754b6250..d2d90d398b 100644 --- a/setup.py +++ b/setup.py @@ -2,21 +2,13 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20190427.0", + version="20190502.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", author_email="hello@home-assistant.io", license="Apache License 2.0", - packages=find_packages( - include=[ - "hass_frontend", - "hass_frontend_es5", - "hass_frontend.*", - "hass_frontend_es5.*", - ] - ), - install_requires=["user-agents==2.0.0"], + packages=find_packages(include=["hass_frontend", "hass_frontend.*"]), include_package_data=True, zip_safe=False, ) diff --git a/src/auth/data.ts b/src/auth/data.ts deleted file mode 100644 index f970e90d52..0000000000 --- a/src/auth/data.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { HomeAssistant } from "../types"; -import { SignedPath } from "./types"; - -export const getSignedPath = ( - hass: HomeAssistant, - path: string -): Promise => hass.callWS({ type: "auth/sign_path", path }); diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index 10a190cca8..4935fac9dd 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -8,7 +8,7 @@ import { css, } from "lit-element"; import "./ha-auth-flow"; -import { AuthProvider } from "../data/auth"; +import { AuthProvider, fetchAuthProviders } from "../data/auth"; import { registerServiceWorker } from "../util/register-service-worker"; import(/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider"); @@ -135,7 +135,9 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) { private async _fetchAuthProviders() { // Fetch auth providers try { - const response = await (window as any).providersPromise; + // We prefetch this data on page load in authorize.html.template for modern builds + const response = await ((window as any).providersPromise || + fetchAuthProviders()); const authProviders = await response.json(); // Forward to main screen which will redirect to right onboarding page. diff --git a/src/auth/types.ts b/src/auth/types.ts deleted file mode 100644 index bb1b00180a..0000000000 --- a/src/auth/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface SignedPath { - path: string; -} diff --git a/src/cards/ha-camera-card.js b/src/cards/ha-camera-card.js index c26b60b72c..c59f847429 100644 --- a/src/cards/ha-camera-card.js +++ b/src/cards/ha-camera-card.js @@ -5,6 +5,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element"; import computeStateName from "../common/entity/compute_state_name"; import EventsMixin from "../mixins/events-mixin"; import LocalizeMixin from "../mixins/localize-mixin"; +import { fetchThumbnailUrlWithCache } from "../data/camera"; const UPDATE_INTERVAL = 10000; // ms /* @@ -54,6 +55,8 @@ class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) { src="[[cameraFeedSrc]]" class="camera-feed" alt="[[_computeStateName(stateObj)]]" + on-load="_imageLoaded" + on-error="_imageError" />
@@ -98,23 +101,23 @@ class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) { clearInterval(this.timer); } + _imageLoaded() { + this.imageLoaded = true; + } + + _imageError() { + this.imageLoaded = false; + } + cardTapped() { this.fire("hass-more-info", { entityId: this.stateObj.entity_id }); } async updateCameraFeedSrc() { - try { - const { content_type: contentType, content } = await this.hass.callWS({ - type: "camera_thumbnail", - entity_id: this.stateObj.entity_id, - }); - this.setProperties({ - imageLoaded: true, - cameraFeedSrc: `data:${contentType};base64, ${content}`, - }); - } catch (err) { - this.imageLoaded = false; - } + this.cameraFeedSrc = await fetchThumbnailUrlWithCache( + this.hass, + this.stateObj.entity_id + ); } _computeStateName(stateObj) { diff --git a/src/components/ha-card.ts b/src/components/ha-card.ts index 3cc329a87f..471905673f 100644 --- a/src/components/ha-card.ts +++ b/src/components/ha-card.ts @@ -28,19 +28,22 @@ class HaCard extends LitElement { display: block; transition: all 0.3s ease-out; } - .header:not(:empty) { - font-size: 24px; + .header:not(:empty), + .header::slotted(*) { + color: var(--ha-card-header-color, --primary-text-color); + font-family: var(--ha-card-header-font-family, inherit); + font-size: var(--ha-card-header-font-size, 24px); letter-spacing: -0.012em; line-height: 32px; - opacity: 0.87; padding: 24px 16px 16px; + display: block; } `; } protected render(): TemplateResult { return html` -
${this.header}
+ ${this.header} `; } diff --git a/src/data/auth.ts b/src/data/auth.ts index 92a44130e2..77c7b40d17 100644 --- a/src/data/auth.ts +++ b/src/data/auth.ts @@ -1,3 +1,5 @@ +import { HomeAssistant } from "../types"; + export interface AuthProvider { name: string; id: string; @@ -7,3 +9,17 @@ export interface AuthProvider { export interface Credential { type: string; } + +export interface SignedPath { + path: string; +} + +export const getSignedPath = ( + hass: HomeAssistant, + path: string +): Promise => hass.callWS({ type: "auth/sign_path", path }); + +export const fetchAuthProviders = () => + fetch("/auth/providers", { + credentials: "same-origin", + }); diff --git a/src/data/camera.ts b/src/data/camera.ts index 62ff707130..ab40da3dd8 100644 --- a/src/data/camera.ts +++ b/src/data/camera.ts @@ -1,5 +1,6 @@ import { HomeAssistant, CameraEntity } from "../types"; import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; +import { getSignedPath } from "./auth"; export const CAMERA_SUPPORT_ON_OFF = 1; export const CAMERA_SUPPORT_STREAM = 2; @@ -22,16 +23,29 @@ export const computeMJPEGStreamUrl = (entity: CameraEntity) => entity.attributes.access_token }`; -export const fetchThumbnailWithCache = ( +export const fetchThumbnailUrlWithCache = ( hass: HomeAssistant, entityId: string -) => timeCachePromiseFunc("_cameraTmb", 9000, fetchThumbnail, hass, entityId); +) => + timeCachePromiseFunc( + "_cameraTmbUrl", + 9000, + fetchThumbnailUrl, + hass, + entityId + ); -export const fetchThumbnail = (hass: HomeAssistant, entityId: string) => - hass.callWS({ +export const fetchThumbnailUrl = (hass: HomeAssistant, entityId: string) => + getSignedPath(hass, `/api/camera_proxy/${entityId}`).then(({ path }) => path); + +export const fetchThumbnail = (hass: HomeAssistant, entityId: string) => { + // tslint:disable-next-line: no-console + console.warn("This method has been deprecated."); + return hass.callWS({ type: "camera_thumbnail", entity_id: entityId, }); +}; export const fetchStreamUrl = ( hass: HomeAssistant, diff --git a/src/data/onboarding.ts b/src/data/onboarding.ts index 05a54be1a0..83002acd6b 100644 --- a/src/data/onboarding.ts +++ b/src/data/onboarding.ts @@ -9,7 +9,10 @@ interface UserStepResponse { auth_code: string; } -export const onboardUserStep = async (params: { +export const fetchOnboardingOverview = () => + fetch("/api/onboarding", { credentials: "same-origin" }); + +export const onboardUserStep = (params: { client_id: string; name: string; username: string; diff --git a/src/dialogs/more-info/controls/more-info-climate.js b/src/dialogs/more-info/controls/more-info-climate.js index d0c4d419af..3fb90fed61 100644 --- a/src/dialogs/more-info/controls/more-info-climate.js +++ b/src/dialogs/more-info/controls/more-info-climate.js @@ -200,14 +200,15 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) { > @@ -224,13 +225,19 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) { dynamic-align="" label="[[localize('ui.card.climate.fan_mode')]]" > - + @@ -244,13 +251,17 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) { dynamic-align="" label="[[localize('ui.card.climate.swing_mode')]]" > - + @@ -297,23 +308,6 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) { observer: "stateObjChanged", }, - operationIndex: { - type: Number, - value: -1, - observer: "handleOperationmodeChanged", - }, - - fanIndex: { - type: Number, - value: -1, - observer: "handleFanmodeChanged", - }, - - swingIndex: { - type: Number, - value: -1, - observer: "handleSwingmodeChanged", - }, awayToggleChecked: Boolean, auxToggleChecked: Boolean, onToggleChecked: Boolean, @@ -346,36 +340,6 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) { } } - handleOperationListUpdate() { - // force polymer to recognize selected item change (to update actual label) - this.operationIndex = -1; - if (this.stateObj.attributes.operation_list) { - this.operationIndex = this.stateObj.attributes.operation_list.indexOf( - this.stateObj.attributes.operation_mode - ); - } - } - - handleSwingListUpdate() { - // force polymer to recognize selected item change (to update actual label) - this.swingIndex = -1; - if (this.stateObj.attributes.swing_list) { - this.swingIndex = this.stateObj.attributes.swing_list.indexOf( - this.stateObj.attributes.swing_mode - ); - } - } - - handleFanListUpdate() { - // force polymer to recognize selected item change (to update actual label) - this.fanIndex = -1; - if (this.stateObj.attributes.fan_list) { - this.fanIndex = this.stateObj.attributes.fan_list.indexOf( - this.stateObj.attributes.fan_mode - ); - } - } - computeTemperatureStepSize(hass, stateObj) { if (stateObj.attributes.target_temp_step) { return stateObj.attributes.target_temp_step; @@ -517,33 +481,27 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) { this.callServiceHelper(newVal ? "turn_on" : "turn_off", {}); } - handleFanmodeChanged(fanIndex) { - // Selected Option will transition to '' before transitioning to new value - if (fanIndex === "" || fanIndex === -1) return; - const fanInput = this.stateObj.attributes.fan_list[fanIndex]; - if (fanInput === this.stateObj.attributes.fan_mode) return; - this.callServiceHelper("set_fan_mode", { fan_mode: fanInput }); + handleFanmodeChanged(ev) { + const oldVal = this.stateObj.attributes.fan_mode; + const newVal = ev.detail.value; + if (!newVal || oldVal === newVal) return; + this.callServiceHelper("set_fan_mode", { fan_mode: newVal }); } - handleOperationmodeChanged(operationIndex) { - // Selected Option will transition to '' before transitioning to new value - if (operationIndex === "" || operationIndex === -1) return; - const operationInput = this.stateObj.attributes.operation_list[ - operationIndex - ]; - if (operationInput === this.stateObj.attributes.operation_mode) return; - + handleOperationmodeChanged(ev) { + const oldVal = this.stateObj.attributes.operation_mode; + const newVal = ev.detail.value; + if (!newVal || oldVal === newVal) return; this.callServiceHelper("set_operation_mode", { - operation_mode: operationInput, + operation_mode: newVal, }); } - handleSwingmodeChanged(swingIndex) { - // Selected Option will transition to '' before transitioning to new value - if (swingIndex === "" || swingIndex === -1) return; - const swingInput = this.stateObj.attributes.swing_list[swingIndex]; - if (swingInput === this.stateObj.attributes.swing_mode) return; - this.callServiceHelper("set_swing_mode", { swing_mode: swingInput }); + handleSwingmodeChanged(ev) { + const oldVal = this.stateObj.attributes.swing_mode; + const newVal = ev.detail.value; + if (!newVal || oldVal === newVal) return; + this.callServiceHelper("set_swing_mode", { swing_mode: newVal }); } callServiceHelper(service, data) { diff --git a/src/dialogs/more-info/controls/more-info-fan.js b/src/dialogs/more-info/controls/more-info-fan.js index a57b702f9f..63837ea052 100644 --- a/src/dialogs/more-info/controls/more-info-fan.js +++ b/src/dialogs/more-info/controls/more-info-fan.js @@ -49,12 +49,17 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) { dynamic-align="" label="[[localize('ui.card.fan.speed')]]" > - + @@ -108,12 +113,6 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) { observer: "stateObjChanged", }, - speedIndex: { - type: Number, - value: -1, - observer: "speedChanged", - }, - oscillationToggleChecked: { type: Boolean, }, @@ -124,9 +123,6 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) { if (newVal) { this.setProperties({ oscillationToggleChecked: newVal.attributes.oscillating, - speedIndex: newVal.attributes.speed_list - ? newVal.attributes.speed_list.indexOf(newVal.attributes.speed) - : -1, }); } @@ -144,17 +140,15 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) { ); } - speedChanged(speedIndex) { - var speedInput; - // Selected Option will transition to '' before transitioning to new value - if (speedIndex === "" || speedIndex === -1) return; + speedChanged(ev) { + var oldVal = this.stateObj.attributes.speed; + var newVal = ev.detail.value; - speedInput = this.stateObj.attributes.speed_list[speedIndex]; - if (speedInput === this.stateObj.attributes.speed) return; + if (!newVal || oldVal === newVal) return; this.hass.callService("fan", "turn_on", { entity_id: this.stateObj.entity_id, - speed: speedInput, + speed: newVal, }); } diff --git a/src/dialogs/more-info/controls/more-info-media_player.js b/src/dialogs/more-info/controls/more-info-media_player.js index 0251f67f07..495cb33fbd 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.js +++ b/src/dialogs/more-info/controls/more-info-media_player.js @@ -154,9 +154,14 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) { label-float="" label="[[localize('ui.card.media_player.source')]]" > - + @@ -174,7 +179,8 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {