Merge pull request #3149 from home-assistant/dev

20190502.0
This commit is contained in:
Paulus Schoutsen 2019-05-02 11:51:22 -07:00 committed by GitHub
commit aa33b00a1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 1117 additions and 734 deletions

1
.gitignore vendored
View File

@ -4,7 +4,6 @@ node_modules/*
npm-debug.log npm-debug.log
.DS_Store .DS_Store
hass_frontend/* hass_frontend/*
hass_frontend_es5/*
.reify-cache .reify-cache
demo/hademo-icons.html demo/hademo-icons.html

2
.nvmrc
View File

@ -1 +1 @@
8.11.1 12.1

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
const gulp = require("gulp"); const gulp = require("gulp");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const config = require("../config");
const ICON_PACKAGE_PATH = path.resolve( const ICON_PACKAGE_PATH = path.resolve(
__dirname, __dirname,
@ -38,12 +37,12 @@ function loadIcon(name) {
function transformXMLtoPolymer(name, xml) { function transformXMLtoPolymer(name, xml) {
const start = xml.indexOf("><path") + 1; const start = xml.indexOf("><path") + 1;
const end = xml.length - start - 6; const end = xml.length - start - 6;
const path = xml.substr(start, end); const pth = xml.substr(start, end);
return `<g id="${name}">${path}</g>`; return `<g id="${name}">${pth}</g>`;
} }
// Given an iconset name and icon names, generate a polymer iconset // Given an iconset name and icon names, generate a polymer iconset
function generateIconset(name, iconNames) { function generateIconset(iconsetName, iconNames) {
const iconDefs = Array.from(iconNames) const iconDefs = Array.from(iconNames)
.map((name) => { .map((name) => {
const iconDef = loadIcon(name); const iconDef = loadIcon(name);
@ -53,7 +52,7 @@ function generateIconset(name, iconNames) {
return transformXMLtoPolymer(name, iconDef); return transformXMLtoPolymer(name, iconDef);
}) })
.join(""); .join("");
return `<ha-iconset-svg name="${name}" 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 // Generate the full MDI iconset
@ -62,7 +61,9 @@ function genMDIIcons() {
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8") fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
); );
const iconNames = meta.map((iconInfo) => iconInfo.name); 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)); fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
} }
@ -81,7 +82,7 @@ function mapFiles(startPath, filter, mapFunc) {
} }
// Find all icons used by the project. // Find all icons used by the project.
function findIcons(path, iconsetName) { function findIcons(searchPath, iconsetName) {
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g"); const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
const icons = new Set(); const icons = new Set();
function processFile(filename) { function processFile(filename) {
@ -93,8 +94,8 @@ function findIcons(path, iconsetName) {
icons.add(match[0].substr(iconsetName.length + 1)); icons.add(match[0].substr(iconsetName.length + 1));
} }
} }
mapFiles(path, ".js", processFile); mapFiles(searchPath, ".js", processFile);
mapFiles(path, ".ts", processFile); mapFiles(searchPath, ".ts", processFile);
return icons; return icons;
} }

View File

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

View File

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

View File

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

10
build-scripts/paths.js Normal file
View File

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

184
build-scripts/webpack.js Normal file
View File

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

View File

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

View File

@ -4,7 +4,7 @@ const {
findIcons, findIcons,
generateIconset, generateIconset,
genMDIIcons, genMDIIcons,
} = require("../../gulp/tasks/gen-icons.js"); } = require("../../build-scripts/gulp/gen-icons.js");
function genHademoIcons() { function genHademoIcons() {
const iconNames = findIcons("./src", "hademo"); const iconNames = findIcons("./src", "hademo");

View File

@ -2,8 +2,8 @@ const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin"); const WorkboxPlugin = require("workbox-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js"); const { babelLoaderConfig } = require("../build-scripts/babel.js");
const webpackBase = require("../config/webpack.js"); const webpackBase = require("../build-scripts/webpack.js");
const isProd = process.env.NODE_ENV === "production"; const isProd = process.env.NODE_ENV === "production";
const isStatsBuild = process.env.STATS === "1"; const isStatsBuild = process.env.STATS === "1";
@ -72,7 +72,7 @@ module.exports = {
...webpackBase.plugins, ...webpackBase.plugins,
isProd && isProd &&
new WorkboxPlugin.GenerateSW({ new WorkboxPlugin.GenerateSW({
swDest: "service_worker_es5.js", swDest: "service_worker.js",
importWorkboxFrom: "local", importWorkboxFrom: "local",
include: [], include: [],
}), }),

View File

@ -1,7 +1,7 @@
const path = require("path"); const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js"); const { babelLoaderConfig } = require("../build-scripts/babel.js");
const webpackBase = require("../config/webpack.js"); const webpackBase = require("../build-scripts/webpack.js");
const isProd = process.env.NODE_ENV === "production"; const isProd = process.env.NODE_ENV === "production";
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js"; const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";

View File

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

View File

@ -1,3 +1,3 @@
var requireDir = require('require-dir'); var requireDir = require("require-dir");
requireDir('./gulp/tasks/'); requireDir("./build-scripts/gulp/");

View File

@ -4,7 +4,7 @@ const {
findIcons, findIcons,
generateIconset, generateIconset,
genMDIIcons, genMDIIcons,
} = require("../../gulp/tasks/gen-icons.js"); } = require("../../build-scripts/gulp/gen-icons.js");
function genHassioIcons() { function genHassioIcons() {
const iconNames = findIcons("./src", "hassio"); const iconNames = findIcons("./src", "hassio");

View File

@ -6,7 +6,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
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";
import { getSignedPath } from "../../../../src/auth/data"; import { getSignedPath } from "../../../../src/data/auth";
import "../../../../src/resources/ha-style"; import "../../../../src/resources/ha-style";
import "../../../../src/components/dialog/ha-paper-dialog"; import "../../../../src/components/dialog/ha-paper-dialog";

View File

@ -3,8 +3,8 @@ const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require("@gfx/zopfli"); const zopfli = require("@gfx/zopfli");
const config = require("./config.js"); const config = require("./config.js");
const { babelLoaderConfig } = require("../config/babel.js"); const { babelLoaderConfig } = require("../build-scripts/babel.js");
const webpackBase = require("../config/webpack.js"); const webpackBase = require("../build-scripts/webpack.js");
const isProdBuild = process.env.NODE_ENV === "production"; const isProdBuild = process.env.NODE_ENV === "production";
const isCI = process.env.CI === "true"; const isCI = process.env.CI === "true";

View File

@ -125,6 +125,7 @@
"eslint-plugin-import": "^2.16.0", "eslint-plugin-import": "^2.16.0",
"eslint-plugin-prettier": "^3.0.1", "eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4", "eslint-plugin-react": "^7.12.4",
"fs-extra": "^7.0.1",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-foreach": "^0.1.0", "gulp-foreach": "^0.1.0",
"gulp-hash": "^4.2.2", "gulp-hash": "^4.2.2",
@ -134,10 +135,12 @@
"gulp-jsonminify": "^1.1.0", "gulp-jsonminify": "^1.1.0",
"gulp-merge-json": "^1.3.1", "gulp-merge-json": "^1.3.1",
"gulp-rename": "^1.4.0", "gulp-rename": "^1.4.0",
"gulp-zopfli-green": "^3.0.1",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"husky": "^1.3.1", "husky": "^1.3.1",
"lint-staged": "^8.1.5", "lint-staged": "^8.1.5",
"lodash.template": "^4.4.0",
"merge-stream": "^1.0.1", "merge-stream": "^1.0.1",
"mocha": "^6.0.2", "mocha": "^6.0.2",
"parse5": "^5.1.0", "parse5": "^5.1.0",
@ -160,6 +163,7 @@
"webpack": "^4.29.6", "webpack": "^4.29.6",
"webpack-cli": "^3.3.0", "webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.2.1", "webpack-dev-server": "^3.2.1",
"webpack-manifest-plugin": "^2.0.4",
"workbox-webpack-plugin": "^4.1.1" "workbox-webpack-plugin": "^4.1.1"
}, },
"resolutions": { "resolutions": {

View File

@ -1,30 +1,7 @@
"""Frontend for Home Assistant.""" """Frontend for Home Assistant."""
import os from pathlib import Path
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
}
def where(): def where():
"""Return path to the frontend.""" """Return path to the frontend."""
return os.path.dirname(__file__) return Path(__file__).parent
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

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 824 B

View File

Before

Width:  |  Height:  |  Size: 292 B

After

Width:  |  Height:  |  Size: 292 B

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 424 B

After

Width:  |  Height:  |  Size: 424 B

View File

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 683 B

View File

Before

Width:  |  Height:  |  Size: 734 B

After

Width:  |  Height:  |  Size: 734 B

View File

Before

Width:  |  Height:  |  Size: 790 B

After

Width:  |  Height:  |  Size: 790 B

View File

Before

Width:  |  Height:  |  Size: 1016 B

After

Width:  |  Height:  |  Size: 1016 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 768 B

View File

Before

Width:  |  Height:  |  Size: 1022 B

After

Width:  |  Height:  |  Size: 1022 B

View File

Before

Width:  |  Height:  |  Size: 803 B

After

Width:  |  Height:  |  Size: 803 B

View File

Before

Width:  |  Height:  |  Size: 822 B

After

Width:  |  Height:  |  Size: 822 B

View File

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 571 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 947 B

After

Width:  |  Height:  |  Size: 947 B

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 852 B

After

Width:  |  Height:  |  Size: 852 B

View File

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 263 B

View File

@ -6,19 +6,4 @@ set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
BUILD_DIR=build ./node_modules/.bin/gulp build-release
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

View File

@ -6,14 +6,4 @@ set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
BUILD_DIR=build ./node_modules/.bin/gulp develop
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

View File

@ -7,5 +7,5 @@ set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
STATS=1 NODE_ENV=production webpack --profile --json > compilation-stats.json 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 rm compilation-stats.json

View File

@ -2,21 +2,13 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20190427.0", version="20190502.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",
author_email="hello@home-assistant.io", author_email="hello@home-assistant.io",
license="Apache License 2.0", license="Apache License 2.0",
packages=find_packages( packages=find_packages(include=["hass_frontend", "hass_frontend.*"]),
include=[
"hass_frontend",
"hass_frontend_es5",
"hass_frontend.*",
"hass_frontend_es5.*",
]
),
install_requires=["user-agents==2.0.0"],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
) )

View File

@ -1,7 +0,0 @@
import { HomeAssistant } from "../types";
import { SignedPath } from "./types";
export const getSignedPath = (
hass: HomeAssistant,
path: string
): Promise<SignedPath> => hass.callWS({ type: "auth/sign_path", path });

View File

@ -8,7 +8,7 @@ import {
css, css,
} from "lit-element"; } from "lit-element";
import "./ha-auth-flow"; import "./ha-auth-flow";
import { AuthProvider } from "../data/auth"; import { AuthProvider, fetchAuthProviders } from "../data/auth";
import { registerServiceWorker } from "../util/register-service-worker"; import { registerServiceWorker } from "../util/register-service-worker";
import(/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider"); import(/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider");
@ -135,7 +135,9 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
private async _fetchAuthProviders() { private async _fetchAuthProviders() {
// Fetch auth providers // Fetch auth providers
try { 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(); const authProviders = await response.json();
// Forward to main screen which will redirect to right onboarding page. // Forward to main screen which will redirect to right onboarding page.

View File

@ -1,3 +0,0 @@
export interface SignedPath {
path: string;
}

View File

@ -5,6 +5,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import computeStateName from "../common/entity/compute_state_name"; import computeStateName from "../common/entity/compute_state_name";
import EventsMixin from "../mixins/events-mixin"; import EventsMixin from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin"; import LocalizeMixin from "../mixins/localize-mixin";
import { fetchThumbnailUrlWithCache } from "../data/camera";
const UPDATE_INTERVAL = 10000; // ms const UPDATE_INTERVAL = 10000; // ms
/* /*
@ -54,6 +55,8 @@ class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
src="[[cameraFeedSrc]]" src="[[cameraFeedSrc]]"
class="camera-feed" class="camera-feed"
alt="[[_computeStateName(stateObj)]]" alt="[[_computeStateName(stateObj)]]"
on-load="_imageLoaded"
on-error="_imageError"
/> />
</template> </template>
<div class="caption"> <div class="caption">
@ -98,23 +101,23 @@ class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
clearInterval(this.timer); clearInterval(this.timer);
} }
_imageLoaded() {
this.imageLoaded = true;
}
_imageError() {
this.imageLoaded = false;
}
cardTapped() { cardTapped() {
this.fire("hass-more-info", { entityId: this.stateObj.entity_id }); this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
} }
async updateCameraFeedSrc() { async updateCameraFeedSrc() {
try { this.cameraFeedSrc = await fetchThumbnailUrlWithCache(
const { content_type: contentType, content } = await this.hass.callWS({ this.hass,
type: "camera_thumbnail", this.stateObj.entity_id
entity_id: this.stateObj.entity_id, );
});
this.setProperties({
imageLoaded: true,
cameraFeedSrc: `data:${contentType};base64, ${content}`,
});
} catch (err) {
this.imageLoaded = false;
}
} }
_computeStateName(stateObj) { _computeStateName(stateObj) {

View File

@ -28,19 +28,22 @@ class HaCard extends LitElement {
display: block; display: block;
transition: all 0.3s ease-out; transition: all 0.3s ease-out;
} }
.header:not(:empty) { .header:not(:empty),
font-size: 24px; .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; letter-spacing: -0.012em;
line-height: 32px; line-height: 32px;
opacity: 0.87;
padding: 24px 16px 16px; padding: 24px 16px 16px;
display: block;
} }
`; `;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="header">${this.header}</div> <slot class="header" name="header">${this.header}</slot>
<slot></slot> <slot></slot>
`; `;
} }

View File

@ -1,3 +1,5 @@
import { HomeAssistant } from "../types";
export interface AuthProvider { export interface AuthProvider {
name: string; name: string;
id: string; id: string;
@ -7,3 +9,17 @@ export interface AuthProvider {
export interface Credential { export interface Credential {
type: string; type: string;
} }
export interface SignedPath {
path: string;
}
export const getSignedPath = (
hass: HomeAssistant,
path: string
): Promise<SignedPath> => hass.callWS({ type: "auth/sign_path", path });
export const fetchAuthProviders = () =>
fetch("/auth/providers", {
credentials: "same-origin",
});

View File

@ -1,5 +1,6 @@
import { HomeAssistant, CameraEntity } from "../types"; import { HomeAssistant, CameraEntity } from "../types";
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
import { getSignedPath } from "./auth";
export const CAMERA_SUPPORT_ON_OFF = 1; export const CAMERA_SUPPORT_ON_OFF = 1;
export const CAMERA_SUPPORT_STREAM = 2; export const CAMERA_SUPPORT_STREAM = 2;
@ -22,16 +23,29 @@ export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
entity.attributes.access_token entity.attributes.access_token
}`; }`;
export const fetchThumbnailWithCache = ( export const fetchThumbnailUrlWithCache = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string entityId: string
) => timeCachePromiseFunc("_cameraTmb", 9000, fetchThumbnail, hass, entityId); ) =>
timeCachePromiseFunc(
"_cameraTmbUrl",
9000,
fetchThumbnailUrl,
hass,
entityId
);
export const fetchThumbnail = (hass: HomeAssistant, entityId: string) => export const fetchThumbnailUrl = (hass: HomeAssistant, entityId: string) =>
hass.callWS<CameraThumbnail>({ 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<CameraThumbnail>({
type: "camera_thumbnail", type: "camera_thumbnail",
entity_id: entityId, entity_id: entityId,
}); });
};
export const fetchStreamUrl = ( export const fetchStreamUrl = (
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -9,7 +9,10 @@ interface UserStepResponse {
auth_code: string; auth_code: string;
} }
export const onboardUserStep = async (params: { export const fetchOnboardingOverview = () =>
fetch("/api/onboarding", { credentials: "same-origin" });
export const onboardUserStep = (params: {
client_id: string; client_id: string;
name: string; name: string;
username: string; username: string;

View File

@ -200,14 +200,15 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
> >
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
selected="{{operationIndex}}" selected="[[stateObj.attributes.operation_mode]]"
attr-for-selected="item-name"
on-selected-changed="handleOperationmodeChanged"
> >
<template <template
is="dom-repeat" is="dom-repeat"
items="[[stateObj.attributes.operation_list]]" items="[[stateObj.attributes.operation_list]]"
on-dom-change="handleOperationListUpdate"
> >
<paper-item <paper-item item-name$="[[item]]"
>[[_localizeOperationMode(localize, item)]]</paper-item >[[_localizeOperationMode(localize, item)]]</paper-item
> >
</template> </template>
@ -224,13 +225,19 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
dynamic-align="" dynamic-align=""
label="[[localize('ui.card.climate.fan_mode')]]" label="[[localize('ui.card.climate.fan_mode')]]"
> >
<paper-listbox slot="dropdown-content" selected="{{fanIndex}}"> <paper-listbox
slot="dropdown-content"
selected="[[stateObj.attributes.fan_mode]]"
attr-for-selected="item-name"
on-selected-changed="handleFanmodeChanged"
>
<template <template
is="dom-repeat" is="dom-repeat"
items="[[stateObj.attributes.fan_list]]" items="[[stateObj.attributes.fan_list]]"
on-dom-change="handleFanListUpdate"
> >
<paper-item>[[_localizeFanMode(localize, item)]]</paper-item> <paper-item item-name$="[[item]]"
>[[_localizeFanMode(localize, item)]]
</paper-item>
</template> </template>
</paper-listbox> </paper-listbox>
</ha-paper-dropdown-menu> </ha-paper-dropdown-menu>
@ -244,13 +251,17 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
dynamic-align="" dynamic-align=""
label="[[localize('ui.card.climate.swing_mode')]]" label="[[localize('ui.card.climate.swing_mode')]]"
> >
<paper-listbox slot="dropdown-content" selected="{{swingIndex}}"> <paper-listbox
slot="dropdown-content"
selected="[[stateObj.attributes.swing_mode]]"
attr-for-selected="item-name"
on-selected-changed="handleSwingmodeChanged"
>
<template <template
is="dom-repeat" is="dom-repeat"
items="[[stateObj.attributes.swing_list]]" items="[[stateObj.attributes.swing_list]]"
on-dom-change="handleSwingListUpdate"
> >
<paper-item>[[item]]</paper-item> <paper-item item-name$="[[item]]">[[item]]</paper-item>
</template> </template>
</paper-listbox> </paper-listbox>
</ha-paper-dropdown-menu> </ha-paper-dropdown-menu>
@ -297,23 +308,6 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
observer: "stateObjChanged", 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, awayToggleChecked: Boolean,
auxToggleChecked: Boolean, auxToggleChecked: Boolean,
onToggleChecked: 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) { computeTemperatureStepSize(hass, stateObj) {
if (stateObj.attributes.target_temp_step) { if (stateObj.attributes.target_temp_step) {
return 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", {}); this.callServiceHelper(newVal ? "turn_on" : "turn_off", {});
} }
handleFanmodeChanged(fanIndex) { handleFanmodeChanged(ev) {
// Selected Option will transition to '' before transitioning to new value const oldVal = this.stateObj.attributes.fan_mode;
if (fanIndex === "" || fanIndex === -1) return; const newVal = ev.detail.value;
const fanInput = this.stateObj.attributes.fan_list[fanIndex]; if (!newVal || oldVal === newVal) return;
if (fanInput === this.stateObj.attributes.fan_mode) return; this.callServiceHelper("set_fan_mode", { fan_mode: newVal });
this.callServiceHelper("set_fan_mode", { fan_mode: fanInput });
} }
handleOperationmodeChanged(operationIndex) { handleOperationmodeChanged(ev) {
// Selected Option will transition to '' before transitioning to new value const oldVal = this.stateObj.attributes.operation_mode;
if (operationIndex === "" || operationIndex === -1) return; const newVal = ev.detail.value;
const operationInput = this.stateObj.attributes.operation_list[ if (!newVal || oldVal === newVal) return;
operationIndex
];
if (operationInput === this.stateObj.attributes.operation_mode) return;
this.callServiceHelper("set_operation_mode", { this.callServiceHelper("set_operation_mode", {
operation_mode: operationInput, operation_mode: newVal,
}); });
} }
handleSwingmodeChanged(swingIndex) { handleSwingmodeChanged(ev) {
// Selected Option will transition to '' before transitioning to new value const oldVal = this.stateObj.attributes.swing_mode;
if (swingIndex === "" || swingIndex === -1) return; const newVal = ev.detail.value;
const swingInput = this.stateObj.attributes.swing_list[swingIndex]; if (!newVal || oldVal === newVal) return;
if (swingInput === this.stateObj.attributes.swing_mode) return; this.callServiceHelper("set_swing_mode", { swing_mode: newVal });
this.callServiceHelper("set_swing_mode", { swing_mode: swingInput });
} }
callServiceHelper(service, data) { callServiceHelper(service, data) {

View File

@ -49,12 +49,17 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
dynamic-align="" dynamic-align=""
label="[[localize('ui.card.fan.speed')]]" label="[[localize('ui.card.fan.speed')]]"
> >
<paper-listbox slot="dropdown-content" selected="{{speedIndex}}"> <paper-listbox
slot="dropdown-content"
selected="[[stateObj.attributes.speed]]"
on-selected-changed="speedChanged"
attr-for-selected="item-name"
>
<template <template
is="dom-repeat" is="dom-repeat"
items="[[stateObj.attributes.speed_list]]" items="[[stateObj.attributes.speed_list]]"
> >
<paper-item>[[item]]</paper-item> <paper-item item-name$="[[item]]">[[item]]</paper-item>
</template> </template>
</paper-listbox> </paper-listbox>
</ha-paper-dropdown-menu> </ha-paper-dropdown-menu>
@ -108,12 +113,6 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
observer: "stateObjChanged", observer: "stateObjChanged",
}, },
speedIndex: {
type: Number,
value: -1,
observer: "speedChanged",
},
oscillationToggleChecked: { oscillationToggleChecked: {
type: Boolean, type: Boolean,
}, },
@ -124,9 +123,6 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
if (newVal) { if (newVal) {
this.setProperties({ this.setProperties({
oscillationToggleChecked: newVal.attributes.oscillating, 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) { speedChanged(ev) {
var speedInput; var oldVal = this.stateObj.attributes.speed;
// Selected Option will transition to '' before transitioning to new value var newVal = ev.detail.value;
if (speedIndex === "" || speedIndex === -1) return;
speedInput = this.stateObj.attributes.speed_list[speedIndex]; if (!newVal || oldVal === newVal) return;
if (speedInput === this.stateObj.attributes.speed) return;
this.hass.callService("fan", "turn_on", { this.hass.callService("fan", "turn_on", {
entity_id: this.stateObj.entity_id, entity_id: this.stateObj.entity_id,
speed: speedInput, speed: newVal,
}); });
} }

View File

@ -154,9 +154,14 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
label-float="" label-float=""
label="[[localize('ui.card.media_player.source')]]" label="[[localize('ui.card.media_player.source')]]"
> >
<paper-listbox slot="dropdown-content" selected="{{sourceIndex}}"> <paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
selected="[[playerObj.source]]"
on-selected-changed="handleSourceChanged"
>
<template is="dom-repeat" items="[[playerObj.sourceList]]"> <template is="dom-repeat" items="[[playerObj.sourceList]]">
<paper-item>[[item]]</paper-item> <paper-item item-name$="[[item]]">[[item]]</paper-item>
</template> </template>
</paper-listbox> </paper-listbox>
</ha-paper-dropdown-menu> </ha-paper-dropdown-menu>
@ -174,7 +179,8 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
attr-for-selected="item-name" attr-for-selected="item-name"
selected="{{SoundModeInput}}" selected="[[playerObj.soundMode]]"
on-selected-changed="handleSoundModeChanged"
> >
<template is="dom-repeat" items="[[playerObj.soundModeList]]"> <template is="dom-repeat" items="[[playerObj.soundModeList]]">
<paper-item item-name$="[[item]]">[[item]]</paper-item> <paper-item item-name$="[[item]]">[[item]]</paper-item>
@ -214,18 +220,6 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
observer: "playerObjChanged", observer: "playerObjChanged",
}, },
sourceIndex: {
type: Number,
value: 0,
observer: "handleSourceChanged",
},
SoundModeInput: {
type: String,
value: "",
observer: "handleSoundModeChanged",
},
ttsLoaded: { ttsLoaded: {
type: Boolean, type: Boolean,
computed: "computeTTSLoaded(hass)", computed: "computeTTSLoaded(hass)",
@ -248,14 +242,6 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
} }
playerObjChanged(newVal, oldVal) { playerObjChanged(newVal, oldVal) {
if (newVal && newVal.sourceList !== undefined) {
this.sourceIndex = newVal.sourceList.indexOf(newVal.source);
}
if (newVal && newVal.soundModeList !== undefined) {
this.SoundModeInput = newVal.soundMode;
}
if (oldVal) { if (oldVal) {
setTimeout(() => { setTimeout(() => {
this.fire("iron-resize"); this.fire("iron-resize");
@ -342,36 +328,26 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
this.playerObj.nextTrack(); this.playerObj.nextTrack();
} }
handleSourceChanged(sourceIndex, sourceIndexOld) { handleSourceChanged(ev) {
// Selected Option will transition to '' before transitioning to new value if (!this.playerObj) return;
if (
!this.playerObj ||
!this.playerObj.supportsSelectSource ||
this.playerObj.sourceList === undefined ||
sourceIndex < 0 ||
sourceIndex >= this.playerObj.sourceList ||
sourceIndexOld === undefined
) {
return;
}
const sourceInput = this.playerObj.sourceList[sourceIndex]; var oldVal = this.playerObj.source;
var newVal = ev.detail.value;
if (sourceInput === this.playerObj.source) { if (!newVal || oldVal === newVal) return;
return;
}
this.playerObj.selectSource(sourceInput); this.playerObj.selectSource(newVal);
} }
handleSoundModeChanged(newVal, oldVal) { handleSoundModeChanged(ev) {
if ( if (!this.playerObj) return;
oldVal &&
newVal !== this.playerObj.soundMode && var oldVal = this.playerObj.soundMode;
this.playerObj.supportsSelectSoundMode var newVal = ev.detail.value;
) {
this.playerObj.selectSoundMode(newVal); if (!newVal || oldVal === newVal) return;
}
this.playerObj.selectSoundMode(newVal);
} }
handleVolumeTap() { handleVolumeTap() {

View File

@ -100,14 +100,15 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
> >
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
selected="{{operationIndex}}" selected="[[stateObj.attributes.operation_mode]]"
attr-for-selected="item-name"
on-selected-changed="handleOperationmodeChanged"
> >
<template <template
is="dom-repeat" is="dom-repeat"
items="[[stateObj.attributes.operation_list]]" items="[[stateObj.attributes.operation_list]]"
on-dom-change="handleOperationListUpdate"
> >
<paper-item <paper-item item-name$="[[item]]"
>[[_localizeOperationMode(localize, item)]]</paper-item >[[_localizeOperationMode(localize, item)]]</paper-item
> >
</template> </template>
@ -146,11 +147,6 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
observer: "stateObjChanged", observer: "stateObjChanged",
}, },
operationIndex: {
type: Number,
value: -1,
observer: "handleOperationmodeChanged",
},
awayToggleChecked: Boolean, awayToggleChecked: Boolean,
}; };
} }
@ -173,16 +169,6 @@ class MoreInfoWaterHeater 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
);
}
}
computeTemperatureStepSize(hass, stateObj) { computeTemperatureStepSize(hass, stateObj) {
if (stateObj.attributes.target_temp_step) { if (stateObj.attributes.target_temp_step) {
return stateObj.attributes.target_temp_step; return stateObj.attributes.target_temp_step;
@ -239,16 +225,12 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
this.callServiceHelper("set_away_mode", { away_mode: newVal }); this.callServiceHelper("set_away_mode", { away_mode: newVal });
} }
handleOperationmodeChanged(operationIndex) { handleOperationmodeChanged(ev) {
// Selected Option will transition to '' before transitioning to new value const oldVal = this.stateObj.attributes.operation_mode;
if (operationIndex === "" || operationIndex === -1) return; const newVal = ev.detail.value;
const operationInput = this.stateObj.attributes.operation_list[ if (!newVal || oldVal === newVal) return;
operationIndex
];
if (operationInput === this.stateObj.attributes.operation_mode) return;
this.callServiceHelper("set_operation_mode", { this.callServiceHelper("set_operation_mode", {
operation_mode: operationInput, operation_mode: newVal,
}); });
} }

View File

@ -1,2 +0,0 @@
/* global importScripts */
importScripts("/static/service-worker-hass.js");

View File

@ -1,3 +1,7 @@
/*
This file is not run through webpack, but instead is directly manipulated
by Workbox Webpack plugin. So we cannot use __DEV__ or other constants.
*/
/* global workbox clients */ /* global workbox clients */
function initRouting() { function initRouting() {
@ -17,9 +21,7 @@ function initRouting() {
// Get manifest and service worker from network. // Get manifest and service worker from network.
workbox.routing.registerRoute( workbox.routing.registerRoute(
new RegExp( new RegExp(`${location.host}/(service_worker.js|manifest.json)`),
`${location.host}/(service_worker.js|service_worker_es5.js|manifest.json)`
),
new workbox.strategies.NetworkOnly() new workbox.strategies.NetworkOnly()
); );
@ -161,11 +163,8 @@ self.addEventListener("message", (message) => {
}); });
workbox.setConfig({ workbox.setConfig({
debug: __DEV__, debug: false,
}); });
if (!__DEV__) { initRouting();
initRouting();
}
initPushNotifications(); initPushNotifications();

View File

@ -0,0 +1,25 @@
<script>
function _ls(src) {
var doc = document.documentElement;
var script = doc.insertBefore(
document.createElement("script"),
doc.lastChild
);
script.type = "text/javascript";
script.src = src;
}
window.Polymer = {
lazyRegister: true,
useNativeCSSProperties: true,
dom: "shadow",
suppressTemplateNotifications: true,
suppressBindingNotifications: true,
};
var webComponentsSupported =
"customElements" in window &&
"content" in document.createElement("template");
if (!webComponentsSupported) {
_ls("/static/polyfills/webcomponents-bundle.js");
}
var isS101 = /\s+Version\/10\.1(?:\.\d+)?\s+Safari\//.test(navigator.userAgent);
</script>

View File

@ -1,10 +1,21 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Home Assistant</title> <title>Home Assistant</title>
<link rel='preload' href='/static/fonts/roboto/Roboto-Light.ttf' as='font' crossorigin /> <link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin /> <link
<%= require('raw-loader!./_header.html.template').default %> rel="preload"
href="/static/fonts/roboto/Roboto-Light.ttf"
as="font"
crossorigin
/>
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Regular.ttf"
as="font"
crossorigin
/>
<%= renderTemplate('_header') %>
<style> <style>
.content { .content {
padding: 20px 16px; padding: 20px 16px;
@ -27,29 +38,33 @@
</head> </head>
<body> <body>
<div class="content"> <div class="content">
<div class='header'> <div class="header">
<img src="/static/icons/favicon-192x192.png" height="52"> <img src="/static/icons/favicon-192x192.png" height="52" />
Home Assistant Home Assistant
</div> </div>
<ha-authorize><p>Initializing</p></ha-authorize> <ha-authorize><p>Initializing</p></ha-authorize>
</div> </div>
<% if (!latestBuild) { %>
<script src="/static/custom-elements-es5-adapter.js"></script>
<script src="<%= compatibility %>"></script>
<% } %>
<script>
window.providersPromise = fetch('/auth/providers', { credentials: 'same-origin' });
var webComponentsSupported = ( <%= renderTemplate('_js_base') %>
'customElements' in window &&
'content' in document.createElement('template')); <script type="module">
if (!webComponentsSupported) { import "<%= latestPageJS %>";
var e = document.createElement('script'); import "<%= latestHassIconsJS %>";
e.src = '/static/webcomponents-bundle.js'; window.providersPromise = fetch("/auth/providers", {
document.write(e.outerHTML); credentials: "same-origin",
} });
</script>
<script nomodule>
(function() {
// Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
_ls("/static/polyfills/custom-elements-es5-adapter.js");
_ls("<%= es5Compatibility %>");
_ls("<%= es5PageJS %>");
_ls("<%= es5HassIconsJS %>");
}
})();
</script> </script>
<script src="<%= entrypoint %>"></script>
<script src='<%= hassIconsJS %>' async></script>
</body> </body>
</html> </html>

View File

@ -1,70 +1,87 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<link rel='preload' href='<%= coreJS %>' as='script'/> <link rel="preload" href="<%= latestCoreJS %>" as="script" crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin /> <link
<link rel='preload' href='/static/fonts/roboto/Roboto-Medium.ttf' as='font' crossorigin /> rel="preload"
<%= require('raw-loader!./_header.html.template').default %> href="/static/fonts/roboto/Roboto-Regular.ttf"
as="font"
crossorigin
/>
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Medium.ttf"
as="font"
crossorigin
/>
<%= renderTemplate('_header') %>
<title>Home Assistant</title> <title>Home Assistant</title>
<link rel='apple-touch-icon' sizes='180x180' <link
href='/static/icons/favicon-apple-180x180.png'> rel="apple-touch-icon"
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4"> sizes="180x180"
<meta name="apple-itunes-app" content="app-id=1099568401"> href="/static/icons/favicon-apple-180x180.png"
<meta name='apple-mobile-web-app-capable' content='yes'> />
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/> <link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/> <meta name="apple-itunes-app" content="app-id=1099568401" />
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/> <meta
<meta name="msapplication-TileColor" content="#03a9f4ff"/> name="msapplication-square70x70logo"
<meta name='mobile-web-app-capable' content='yes'> content="/static/icons/tile-win-70x70.png"
<meta name='referrer' content='same-origin'> />
<meta name='theme-color' content='{{ theme_color }}'> <meta
name="msapplication-square150x150logo"
content="/static/icons/tile-win-150x150.png"
/>
<meta
name="msapplication-wide310x150logo"
content="/static/icons/tile-win-310x150.png"
/>
<meta
name="msapplication-square310x310logo"
content="/static/icons/tile-win-310x310.png"
/>
<meta name="msapplication-TileColor" content="#03a9f4ff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="referrer" content="same-origin" />
<meta name="theme-color" content="#THEMEC" />
<style> <style>
#ha-init-skeleton::before { #ha-init-skeleton::before {
display: block; display: block;
content: ""; content: "";
height: 112px; height: 112px;
background-color: {{ theme_color }}; background-color: #THEMEC;
} }
</style> </style>
<script>
window.customPanelJS = '<%= customPanelJS %>';
window.noAuth = '{{ no_auth }}';
window.useOAuth = '{{ use_oauth }}'
window.Polymer = {
lazyRegister: true,
useNativeCSSProperties: true,
dom: 'shadow',
suppressTemplateNotifications: true,
suppressBindingNotifications: true,
};
</script>
</head> </head>
<body> <body>
<div id='ha-init-skeleton'></div> <div id="ha-init-skeleton"></div>
<home-assistant> <home-assistant> </home-assistant>
</home-assistant>
<% if (!latestBuild) { %> <%= renderTemplate('_js_base') %>
<script src="/static/custom-elements-es5-adapter.js"></script>
<script src="<%= compatibility %>"></script> <script type="module">
<% } %> import "<%= latestCoreJS %>";
<script> import "<%= latestAppJS %>";
var webComponentsSupported = ( import "<%= latestHassIconsJS %>";
'customElements' in window && window.customPanelJS = "<%= latestCustomPanelJS %>";
'content' in document.createElement('template'));
if (!webComponentsSupported) {
(function() {
var e = document.createElement('script');
e.src = '/static/webcomponents-bundle.js';
document.write(e.outerHTML);
}());
}
</script> </script>
<script src='<%= coreJS %>'></script>
<script src='<%= appJS %>'></script> <script nomodule>
<script src='<%= hassIconsJS %>' async></script> (function() {
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
window.customPanelJS = "<%= es5CustomPanelJS %>";
_ls("/static/polyfills/custom-elements-es5-adapter.js");
_ls("<%= es5Compatibility %>");
_ls("<%= es5CoreJS %>");
_ls("<%= es5AppJS %>");
_ls("<%= es5HassIconsJS %>");
}
})();
</script>
{% for extra_url in extra_urls -%} {% for extra_url in extra_urls -%}
<link rel='import' href='{{ extra_url }}' async> <link rel="import" href="{{ extra_url }}" async />
{% endfor -%} {% endfor -%}
</body> </body>
</html> </html>

View File

@ -1,10 +1,21 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Home Assistant</title> <title>Home Assistant</title>
<link rel='preload' href='/static/fonts/roboto/Roboto-Light.ttf' as='font' crossorigin /> <link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin /> <link
<%= require('raw-loader!./_header.html.template').default %> rel="preload"
href="/static/fonts/roboto/Roboto-Light.ttf"
as="font"
crossorigin
/>
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Regular.ttf"
as="font"
crossorigin
/>
<%= renderTemplate('_header') %>
<style> <style>
.content { .content {
padding: 20px 16px; padding: 20px 16px;
@ -28,30 +39,34 @@
</head> </head>
<body> <body>
<div class="content"> <div class="content">
<div class='header'> <div class="header">
<img src="/static/icons/favicon-192x192.png" height="52"> <img src="/static/icons/favicon-192x192.png" height="52" />
Home Assistant Home Assistant
</div> </div>
<ha-onboarding>Initializing</ha-onboarding> <ha-onboarding>Initializing</ha-onboarding>
</div> </div>
<% if (!latestBuild) { %>
<script src="/static/custom-elements-es5-adapter.js"></script>
<script src="<%= compatibility %>"></script>
<% } %>
<script>
window.stepsPromise = fetch('/api/onboarding', { credentials: 'same-origin' });
var webComponentsSupported = ( <%= renderTemplate('_js_base') %>
'customElements' in window &&
'content' in document.createElement('template')); <script type="module">
if (!webComponentsSupported) { import "<%= latestPageJS %>";
var e = document.createElement('script'); import "<%= latestHassIconsJS %>";
e.src = '/static/webcomponents-bundle.js'; window.stepsPromise = fetch("/api/onboarding", {
document.write(e.outerHTML); credentials: "same-origin",
} });
</script>
<script nomodule>
(function() {
// Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
_ls("/static/polyfills/custom-elements-es5-adapter.js");
_ls("<%= es5Compatibility %>");
_ls("<%= es5PageJS %>");
_ls("<%= es5HassIconsJS %>");
}
})();
</script> </script>
<script src="<%= entrypoint %>"></script>
<script src='<%= hassIconsJS %>' async></script>
</body> </body>
</html> </html>

View File

@ -89,7 +89,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
${!title && !show_header_toggle ${!title && !show_header_toggle
? html`` ? html``
: html` : html`
<div class="header"> <div class="header" slot="header">
<div class="name">${title}</div> <div class="name">${title}</div>
${show_header_toggle === false ${show_header_toggle === false
? html`` ? html``
@ -114,12 +114,8 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
ha-card {
padding: 16px;
}
#states { #states {
margin: -4px 0; padding: 12px 16px;
} }
#states > * { #states > * {
@ -131,28 +127,16 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
} }
.header { .header {
/* start paper-font-headline style */ margin-bottom: -8px;
font-family: "Roboto", "Noto", sans-serif; padding-bottom: 0px;
-webkit-font-smoothing: antialiased; /* OS X subpixel AA bleed bug */
text-rendering: optimizeLegibility;
font-size: 24px;
font-weight: 400;
letter-spacing: -0.012em;
/* end paper-font-headline style */
line-height: 40px;
color: var(--primary-text-color);
padding: 4px 0 12px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.header .name { .header .name {
/* start paper-font-common-nowrap style */
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
/* end paper-font-common-nowrap */
} }
.state-card-dialog { .state-card-dialog {

View File

@ -287,7 +287,7 @@ export const generateLovelaceConfig = (
// User has no entities // User has no entities
if (views.length === 1 && views[0].cards!.length === 0) { if (views.length === 1 && views[0].cards!.length === 0) {
import("../cards/hui-empty-state-card"); import(/* webpackChunkName: "hui-empty-state-card" */ "../cards/hui-empty-state-card");
views[0].cards!.push({ views[0].cards!.push({
type: "custom:hui-empty-state-card", type: "custom:hui-empty-state-card",
}); });

View File

@ -17,8 +17,7 @@ import {
import { HomeAssistant, CameraEntity } from "../../../types"; import { HomeAssistant, CameraEntity } from "../../../types";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { b64toBlob } from "../../../common/file/b64-to-blob"; import { fetchThumbnailUrlWithCache } from "../../../data/camera";
import { fetchThumbnailWithCache } from "../../../data/camera";
const UPDATE_INTERVAL = 10000; const UPDATE_INTERVAL = 10000;
const DEFAULT_FILTER = "grayscale(100%)"; const DEFAULT_FILTER = "grayscale(100%)";
@ -197,21 +196,20 @@ export class HuiImage extends LitElement {
if (!this.hass || !this.cameraImage) { if (!this.hass || !this.cameraImage) {
return; return;
} }
try {
const { const cameraState = this.hass.states[this.cameraImage] as
content_type: contentType, | CameraEntity
content, | undefined;
} = await fetchThumbnailWithCache(this.hass, this.cameraImage);
if (this._cameraImageSrc) { if (!cameraState) {
URL.revokeObjectURL(this._cameraImageSrc);
}
this._cameraImageSrc = URL.createObjectURL(
b64toBlob(content, contentType)
);
this._onImageLoad();
} catch (err) {
this._onImageError(); this._onImageError();
return;
} }
this._cameraImageSrc = await fetchThumbnailUrlWithCache(
this.hass,
this.cameraImage
);
} }
static get styles(): CSSResult { static get styles(): CSSResult {

View File

@ -56,7 +56,11 @@ export class HuiStateBadgeElement extends LitElement
<ha-state-label-badge <ha-state-label-badge
.hass="${this.hass}" .hass="${this.hass}"
.state="${stateObj}" .state="${stateObj}"
.title="${computeStateName(stateObj)}" .title="${this._config.title === undefined
? computeStateName(stateObj)
: this._config.title === null
? ""
: this._config.title}"
></ha-state-label-badge> ></ha-state-label-badge>
`; `;
} }

View File

@ -45,6 +45,7 @@ export interface ServiceButtonElementConfig extends LovelaceElementConfig {
export interface StateBadgeElementConfig extends LovelaceElementConfig { export interface StateBadgeElementConfig extends LovelaceElementConfig {
entity: string; entity: string;
title?: string;
} }
export interface StateIconElementConfig extends LovelaceElementConfig { export interface StateIconElementConfig extends LovelaceElementConfig {

View File

@ -549,6 +549,7 @@
}, },
"sidebar": { "sidebar": {
"developer_tools": "Developer tools", "developer_tools": "Developer tools",
"external_app_configuration": "App Configuration",
"log_out": "Log out" "log_out": "Log out"
}, },
"panel": { "panel": {

View File

@ -1,6 +1,3 @@
const serviceWorkerUrl =
__BUILD__ === "latest" ? "/service_worker.js" : "/service_worker_es5.js";
export const registerServiceWorker = (notifyUpdate = true) => { export const registerServiceWorker = (notifyUpdate = true) => {
if ( if (
!("serviceWorker" in navigator) || !("serviceWorker" in navigator) ||
@ -9,7 +6,7 @@ export const registerServiceWorker = (notifyUpdate = true) => {
return; return;
} }
navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => { navigator.serviceWorker.register("/service_worker.js").then((reg) => {
reg.addEventListener("updatefound", () => { reg.addEventListener("updatefound", () => {
const installingWorker = reg.installing; const installingWorker = reg.installing;
if (!installingWorker || !notifyUpdate) { if (!installingWorker || !notifyUpdate) {

View File

@ -8,10 +8,4 @@ describe("formatDate", () => {
it("Formats English dates", () => { it("Formats English dates", () => {
assert.strictEqual(formatDate(dateObj, "en"), "November 18, 2017"); assert.strictEqual(formatDate(dateObj, "en"), "November 18, 2017");
}); });
// Node only contains intl support for english formats. This test at least ensures
// the fallback to a different locale
it("Formats other dates", () => {
assert.strictEqual(formatDate(dateObj, "fr"), "2017 M11 18");
});
}); });

View File

@ -11,10 +11,4 @@ describe("formatDateTime", () => {
"November 18, 2017, 11:12 AM" "November 18, 2017, 11:12 AM"
); );
}); });
// Node only contains intl support for english formats. This test at least ensures
// the fallback to a different locale
it("Formats other date times", () => {
assert.strictEqual(formatDateTime(dateObj, "fr"), "2017 M11 18 11:12");
});
}); });

View File

@ -8,10 +8,4 @@ describe("formatTime", () => {
it("Formats English times", () => { it("Formats English times", () => {
assert.strictEqual(formatTime(dateObj, "en"), "11:12 AM"); assert.strictEqual(formatTime(dateObj, "en"), "11:12 AM");
}); });
// Node only contains intl support for english formats. This test at least ensures
// the fallback to a different locale
it("Formats other times", () => {
assert.strictEqual(formatTime(dateObj, "fr"), "11:12");
});
}); });

View File

@ -769,7 +769,7 @@
} }
}, },
"error": { "error": {
"invalid_auth": "Ongeldige Gebruikersnaam of wagwoord", "invalid_auth": "Ongeldige gebruikersnaam of wagwoord",
"invalid_code": "Ongeldige verifikasiekode" "invalid_code": "Ongeldige verifikasiekode"
}, },
"abort": { "abort": {
@ -868,6 +868,14 @@
"title": "Welkom tuis", "title": "Welkom tuis",
"no_devices": "Hierdie bladsy laat u toe om u toestelle te beheer, maar dit lyk asof u nog nie toestelle opgestel het nie. Gaan na die integrasies bladsy om te begin.", "no_devices": "Hierdie bladsy laat u toe om u toestelle te beheer, maar dit lyk asof u nog nie toestelle opgestel het nie. Gaan na die integrasies bladsy om te begin.",
"go_to_integrations_page": "Gaan na die integrasies bladsy." "go_to_integrations_page": "Gaan na die integrasies bladsy."
},
"picture-elements": {
"hold": "Hou:",
"tap": "Tik:",
"navigate_to": "Navigeer na {location}",
"toggle": "Wissel {name}",
"call_service": "Call diens {name}",
"more_info": "Wys meer inligting: {name}"
} }
}, },
"editor": { "editor": {
@ -884,7 +892,7 @@
"migrate": { "migrate": {
"header": "Opstellings Onversoenbaar", "header": "Opstellings Onversoenbaar",
"para_no_id": "Hierdie element het nie 'n ID nie. Voeg asseblief 'n ID by vir hierdie element in 'ui-lovelace.yaml'.", "para_no_id": "Hierdie element het nie 'n ID nie. Voeg asseblief 'n ID by vir hierdie element in 'ui-lovelace.yaml'.",
"para_migrate": "Druk die 'Migreer opstellings' knoppie as u wil hê Home Assistant moet vir u ID's by al u kaarte en aansigte outomaties byvoeg.", "para_migrate": "Druk die 'Migreer konfigurasie' knoppie as u wil hê Home Assistant moet vir u ID's by al u kaarte en aansigte outomaties byvoeg.",
"migrate": "Migreer opstellings" "migrate": "Migreer opstellings"
}, },
"header": "Wysig gebruikerskoppelvlak", "header": "Wysig gebruikerskoppelvlak",

View File

@ -868,6 +868,14 @@
"title": "Benvingut\/da a casa", "title": "Benvingut\/da a casa",
"no_devices": "Aquesta pàgina et permet controlar els teus dispositius, però sembla que encara no en tens cap configurat. Vés a la pàgina d'integracions per a començar.", "no_devices": "Aquesta pàgina et permet controlar els teus dispositius, però sembla que encara no en tens cap configurat. Vés a la pàgina d'integracions per a començar.",
"go_to_integrations_page": "Vés a la pàgina d'integracions." "go_to_integrations_page": "Vés a la pàgina d'integracions."
},
"picture-elements": {
"hold": "Manté:",
"tap": "Tocar:",
"navigate_to": "Navega a {location}",
"toggle": "Commuta {name}",
"call_service": "Cridar servei {name}",
"more_info": "Mostra més informació: {name}"
} }
}, },
"editor": { "editor": {

View File

@ -868,6 +868,14 @@
"title": "Willkommen zu Hause", "title": "Willkommen zu Hause",
"no_devices": "Auf dieser Seite kannst du deine Geräte steuern, es sieht jedoch so aus, als hättest du noch keine eingerichtet. Gehe zur Integrationsseite, um damit zu beginnen.", "no_devices": "Auf dieser Seite kannst du deine Geräte steuern, es sieht jedoch so aus, als hättest du noch keine eingerichtet. Gehe zur Integrationsseite, um damit zu beginnen.",
"go_to_integrations_page": "Gehe zur Integrationsseite." "go_to_integrations_page": "Gehe zur Integrationsseite."
},
"picture-elements": {
"hold": "Halten:",
"tap": "Tippe auf:",
"navigate_to": "Navigiere zu {location}",
"toggle": "{name} umschalten",
"call_service": "Dienst {name} ausführen",
"more_info": "Zeige weitere Informationen: {name}"
} }
}, },
"editor": { "editor": {

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