1
.gitignore
vendored
@ -4,7 +4,6 @@ node_modules/*
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
hass_frontend/*
|
||||
hass_frontend_es5/*
|
||||
.reify-cache
|
||||
demo/hademo-icons.html
|
||||
|
||||
|
5
build-scripts/gulp/clean.js
Normal 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]));
|
29
build-scripts/gulp/develop.js
Normal 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"
|
||||
)
|
||||
);
|
108
build-scripts/gulp/entry-html.js
Normal 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();
|
||||
});
|
87
build-scripts/gulp/gather-static.js
Normal 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();
|
||||
});
|
@ -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") + 1;
|
||||
const end = xml.length - start - 6;
|
||||
const path = xml.substr(start, end);
|
||||
return `<g id="${name}">${path}</g>`;
|
||||
const pth = xml.substr(start, end);
|
||||
return `<g id="${name}">${pth}</g>`;
|
||||
}
|
||||
|
||||
// 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 `<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
|
||||
@ -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;
|
||||
}
|
||||
|
31
build-scripts/gulp/release.js
Normal 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"
|
||||
)
|
||||
)
|
||||
);
|
29
build-scripts/gulp/service-worker.js
Normal 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();
|
||||
});
|
63
build-scripts/gulp/webpack.js
Normal 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
@ -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
@ -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,
|
||||
};
|
@ -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,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
@ -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");
|
||||
|
@ -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: [],
|
||||
}),
|
||||
|
@ -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";
|
||||
|
@ -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"),
|
||||
};
|
@ -1,3 +1,3 @@
|
||||
var requireDir = require('require-dir');
|
||||
var requireDir = require("require-dir");
|
||||
|
||||
requireDir('./gulp/tasks/');
|
||||
requireDir("./build-scripts/gulp/");
|
||||
|
@ -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");
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 824 B After Width: | Height: | Size: 824 B |
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 424 B After Width: | Height: | Size: 424 B |
Before Width: | Height: | Size: 683 B After Width: | Height: | Size: 683 B |
Before Width: | Height: | Size: 734 B After Width: | Height: | Size: 734 B |
Before Width: | Height: | Size: 790 B After Width: | Height: | Size: 790 B |
Before Width: | Height: | Size: 1016 B After Width: | Height: | Size: 1016 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 768 B |
Before Width: | Height: | Size: 1022 B After Width: | Height: | Size: 1022 B |
Before Width: | Height: | Size: 803 B After Width: | Height: | Size: 803 B |
Before Width: | Height: | Size: 822 B After Width: | Height: | Size: 822 B |
Before Width: | Height: | Size: 571 B After Width: | Height: | Size: 571 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 947 B After Width: | Height: | Size: 947 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 852 B After Width: | Height: | Size: 852 B |
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 263 B |
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
12
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,
|
||||
)
|
||||
|
@ -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 });
|
@ -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.
|
||||
|
@ -1,3 +0,0 @@
|
||||
export interface SignedPath {
|
||||
path: string;
|
||||
}
|
@ -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"
|
||||
/>
|
||||
</template>
|
||||
<div class="caption">
|
||||
@ -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) {
|
||||
|
@ -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`
|
||||
<div class="header">${this.header}</div>
|
||||
<slot class="header" name="header">${this.header}</slot>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
@ -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<SignedPath> => hass.callWS({ type: "auth/sign_path", path });
|
||||
|
||||
export const fetchAuthProviders = () =>
|
||||
fetch("/auth/providers", {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
|
@ -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<CameraThumbnail>({
|
||||
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<CameraThumbnail>({
|
||||
type: "camera_thumbnail",
|
||||
entity_id: entityId,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchStreamUrl = (
|
||||
hass: HomeAssistant,
|
||||
|
@ -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;
|
||||
|
@ -200,14 +200,15 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
selected="{{operationIndex}}"
|
||||
selected="[[stateObj.attributes.operation_mode]]"
|
||||
attr-for-selected="item-name"
|
||||
on-selected-changed="handleOperationmodeChanged"
|
||||
>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[stateObj.attributes.operation_list]]"
|
||||
on-dom-change="handleOperationListUpdate"
|
||||
>
|
||||
<paper-item
|
||||
<paper-item item-name$="[[item]]"
|
||||
>[[_localizeOperationMode(localize, item)]]</paper-item
|
||||
>
|
||||
</template>
|
||||
@ -224,13 +225,19 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
dynamic-align=""
|
||||
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
|
||||
is="dom-repeat"
|
||||
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>
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
@ -244,13 +251,17 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
dynamic-align=""
|
||||
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
|
||||
is="dom-repeat"
|
||||
items="[[stateObj.attributes.swing_list]]"
|
||||
on-dom-change="handleSwingListUpdate"
|
||||
>
|
||||
<paper-item>[[item]]</paper-item>
|
||||
<paper-item item-name$="[[item]]">[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
@ -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) {
|
||||
|
@ -49,12 +49,17 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
dynamic-align=""
|
||||
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
|
||||
is="dom-repeat"
|
||||
items="[[stateObj.attributes.speed_list]]"
|
||||
>
|
||||
<paper-item>[[item]]</paper-item>
|
||||
<paper-item item-name$="[[item]]">[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -154,9 +154,14 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
label-float=""
|
||||
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]]">
|
||||
<paper-item>[[item]]</paper-item>
|
||||
<paper-item item-name$="[[item]]">[[item]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
@ -174,7 +179,8 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
selected="{{SoundModeInput}}"
|
||||
selected="[[playerObj.soundMode]]"
|
||||
on-selected-changed="handleSoundModeChanged"
|
||||
>
|
||||
<template is="dom-repeat" items="[[playerObj.soundModeList]]">
|
||||
<paper-item item-name$="[[item]]">[[item]]</paper-item>
|
||||
@ -214,18 +220,6 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
observer: "playerObjChanged",
|
||||
},
|
||||
|
||||
sourceIndex: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
observer: "handleSourceChanged",
|
||||
},
|
||||
|
||||
SoundModeInput: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer: "handleSoundModeChanged",
|
||||
},
|
||||
|
||||
ttsLoaded: {
|
||||
type: Boolean,
|
||||
computed: "computeTTSLoaded(hass)",
|
||||
@ -248,14 +242,6 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
}
|
||||
|
||||
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) {
|
||||
setTimeout(() => {
|
||||
this.fire("iron-resize");
|
||||
@ -342,37 +328,27 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
this.playerObj.nextTrack();
|
||||
}
|
||||
|
||||
handleSourceChanged(sourceIndex, sourceIndexOld) {
|
||||
// Selected Option will transition to '' before transitioning to new value
|
||||
if (
|
||||
!this.playerObj ||
|
||||
!this.playerObj.supportsSelectSource ||
|
||||
this.playerObj.sourceList === undefined ||
|
||||
sourceIndex < 0 ||
|
||||
sourceIndex >= this.playerObj.sourceList ||
|
||||
sourceIndexOld === undefined
|
||||
) {
|
||||
return;
|
||||
handleSourceChanged(ev) {
|
||||
if (!this.playerObj) return;
|
||||
|
||||
var oldVal = this.playerObj.source;
|
||||
var newVal = ev.detail.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
this.playerObj.selectSource(newVal);
|
||||
}
|
||||
|
||||
const sourceInput = this.playerObj.sourceList[sourceIndex];
|
||||
handleSoundModeChanged(ev) {
|
||||
if (!this.playerObj) return;
|
||||
|
||||
if (sourceInput === this.playerObj.source) {
|
||||
return;
|
||||
}
|
||||
var oldVal = this.playerObj.soundMode;
|
||||
var newVal = ev.detail.value;
|
||||
|
||||
this.playerObj.selectSource(sourceInput);
|
||||
}
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
handleSoundModeChanged(newVal, oldVal) {
|
||||
if (
|
||||
oldVal &&
|
||||
newVal !== this.playerObj.soundMode &&
|
||||
this.playerObj.supportsSelectSoundMode
|
||||
) {
|
||||
this.playerObj.selectSoundMode(newVal);
|
||||
}
|
||||
}
|
||||
|
||||
handleVolumeTap() {
|
||||
if (!this.playerObj.supportsVolumeMute) {
|
||||
|
@ -100,14 +100,15 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
selected="{{operationIndex}}"
|
||||
selected="[[stateObj.attributes.operation_mode]]"
|
||||
attr-for-selected="item-name"
|
||||
on-selected-changed="handleOperationmodeChanged"
|
||||
>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[stateObj.attributes.operation_list]]"
|
||||
on-dom-change="handleOperationListUpdate"
|
||||
>
|
||||
<paper-item
|
||||
<paper-item item-name$="[[item]]"
|
||||
>[[_localizeOperationMode(localize, item)]]</paper-item
|
||||
>
|
||||
</template>
|
||||
@ -146,11 +147,6 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
observer: "stateObjChanged",
|
||||
},
|
||||
|
||||
operationIndex: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: "handleOperationmodeChanged",
|
||||
},
|
||||
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) {
|
||||
if (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 });
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,2 +0,0 @@
|
||||
/* global importScripts */
|
||||
importScripts("/static/service-worker-hass.js");
|
@ -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 */
|
||||
|
||||
function initRouting() {
|
||||
@ -17,9 +21,7 @@ function initRouting() {
|
||||
|
||||
// Get manifest and service worker from network.
|
||||
workbox.routing.registerRoute(
|
||||
new RegExp(
|
||||
`${location.host}/(service_worker.js|service_worker_es5.js|manifest.json)`
|
||||
),
|
||||
new RegExp(`${location.host}/(service_worker.js|manifest.json)`),
|
||||
new workbox.strategies.NetworkOnly()
|
||||
);
|
||||
|
||||
@ -161,11 +163,8 @@ self.addEventListener("message", (message) => {
|
||||
});
|
||||
|
||||
workbox.setConfig({
|
||||
debug: __DEV__,
|
||||
debug: false,
|
||||
});
|
||||
|
||||
if (!__DEV__) {
|
||||
initRouting();
|
||||
}
|
||||
|
||||
initRouting();
|
||||
initPushNotifications();
|
||||
|
25
src/html/_js_base.html.template
Normal 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>
|
@ -1,10 +1,21 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Home Assistant</title>
|
||||
<link 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 />
|
||||
<%= require('raw-loader!./_header.html.template').default %>
|
||||
<link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin />
|
||||
<link
|
||||
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>
|
||||
.content {
|
||||
padding: 20px 16px;
|
||||
@ -27,29 +38,33 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<div class='header'>
|
||||
<img src="/static/icons/favicon-192x192.png" height="52">
|
||||
<div class="header">
|
||||
<img src="/static/icons/favicon-192x192.png" height="52" />
|
||||
Home Assistant
|
||||
</div>
|
||||
<ha-authorize><p>Initializing</p></ha-authorize>
|
||||
</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 = (
|
||||
'customElements' in window &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
var e = document.createElement('script');
|
||||
e.src = '/static/webcomponents-bundle.js';
|
||||
document.write(e.outerHTML);
|
||||
}
|
||||
<%= renderTemplate('_js_base') %>
|
||||
|
||||
<script type="module">
|
||||
import "<%= latestPageJS %>";
|
||||
import "<%= latestHassIconsJS %>";
|
||||
window.providersPromise = fetch("/auth/providers", {
|
||||
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 src="<%= entrypoint %>"></script>
|
||||
<script src='<%= hassIconsJS %>' async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,70 +1,87 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel='preload' href='<%= coreJS %>' as='script'/>
|
||||
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
|
||||
<link rel='preload' href='/static/fonts/roboto/Roboto-Medium.ttf' as='font' crossorigin />
|
||||
<%= require('raw-loader!./_header.html.template').default %>
|
||||
<link rel="preload" href="<%= latestCoreJS %>" as="script" crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
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>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/icons/favicon-apple-180x180.png'>
|
||||
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4">
|
||||
<meta name="apple-itunes-app" content="app-id=1099568401">
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
|
||||
<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='{{ theme_color }}'>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/static/icons/favicon-apple-180x180.png"
|
||||
/>
|
||||
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
|
||||
<meta name="apple-itunes-app" content="app-id=1099568401" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
name="msapplication-square70x70logo"
|
||||
content="/static/icons/tile-win-70x70.png"
|
||||
/>
|
||||
<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>
|
||||
#ha-init-skeleton::before {
|
||||
display: block;
|
||||
content: "";
|
||||
height: 112px;
|
||||
background-color: {{ theme_color }};
|
||||
background-color: #THEMEC;
|
||||
}
|
||||
</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>
|
||||
<body>
|
||||
<div id='ha-init-skeleton'></div>
|
||||
<home-assistant>
|
||||
</home-assistant>
|
||||
<% if (!latestBuild) { %>
|
||||
<script src="/static/custom-elements-es5-adapter.js"></script>
|
||||
<script src="<%= compatibility %>"></script>
|
||||
<% } %>
|
||||
<script>
|
||||
var webComponentsSupported = (
|
||||
'customElements' in window &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
(function() {
|
||||
var e = document.createElement('script');
|
||||
e.src = '/static/webcomponents-bundle.js';
|
||||
document.write(e.outerHTML);
|
||||
}());
|
||||
}
|
||||
<div id="ha-init-skeleton"></div>
|
||||
<home-assistant> </home-assistant>
|
||||
|
||||
<%= renderTemplate('_js_base') %>
|
||||
|
||||
<script type="module">
|
||||
import "<%= latestCoreJS %>";
|
||||
import "<%= latestAppJS %>";
|
||||
import "<%= latestHassIconsJS %>";
|
||||
window.customPanelJS = "<%= latestCustomPanelJS %>";
|
||||
</script>
|
||||
<script src='<%= coreJS %>'></script>
|
||||
<script src='<%= appJS %>'></script>
|
||||
<script src='<%= hassIconsJS %>' async></script>
|
||||
|
||||
<script nomodule>
|
||||
(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 -%}
|
||||
<link rel='import' href='{{ extra_url }}' async>
|
||||
<link rel="import" href="{{ extra_url }}" async />
|
||||
{% endfor -%}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,10 +1,21 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Home Assistant</title>
|
||||
<link 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 />
|
||||
<%= require('raw-loader!./_header.html.template').default %>
|
||||
<link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin />
|
||||
<link
|
||||
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>
|
||||
.content {
|
||||
padding: 20px 16px;
|
||||
@ -28,30 +39,34 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<div class='header'>
|
||||
<img src="/static/icons/favicon-192x192.png" height="52">
|
||||
<div class="header">
|
||||
<img src="/static/icons/favicon-192x192.png" height="52" />
|
||||
Home Assistant
|
||||
</div>
|
||||
|
||||
<ha-onboarding>Initializing</ha-onboarding>
|
||||
</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 = (
|
||||
'customElements' in window &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
var e = document.createElement('script');
|
||||
e.src = '/static/webcomponents-bundle.js';
|
||||
document.write(e.outerHTML);
|
||||
}
|
||||
<%= renderTemplate('_js_base') %>
|
||||
|
||||
<script type="module">
|
||||
import "<%= latestPageJS %>";
|
||||
import "<%= latestHassIconsJS %>";
|
||||
window.stepsPromise = fetch("/api/onboarding", {
|
||||
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 src="<%= entrypoint %>"></script>
|
||||
<script src='<%= hassIconsJS %>' async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -89,7 +89,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
${!title && !show_header_toggle
|
||||
? html``
|
||||
: html`
|
||||
<div class="header">
|
||||
<div class="header" slot="header">
|
||||
<div class="name">${title}</div>
|
||||
${show_header_toggle === false
|
||||
? html``
|
||||
@ -114,12 +114,8 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#states {
|
||||
margin: -4px 0;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
#states > * {
|
||||
@ -131,28 +127,16 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
.header {
|
||||
/* start paper-font-headline style */
|
||||
font-family: "Roboto", "Noto", sans-serif;
|
||||
-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;
|
||||
margin-bottom: -8px;
|
||||
padding-bottom: 0px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header .name {
|
||||
/* start paper-font-common-nowrap style */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* end paper-font-common-nowrap */
|
||||
}
|
||||
|
||||
.state-card-dialog {
|
||||
|
@ -287,7 +287,7 @@ export const generateLovelaceConfig = (
|
||||
|
||||
// User has no entities
|
||||
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({
|
||||
type: "custom:hui-empty-state-card",
|
||||
});
|
||||
|
@ -17,8 +17,7 @@ import {
|
||||
import { HomeAssistant, CameraEntity } from "../../../types";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { b64toBlob } from "../../../common/file/b64-to-blob";
|
||||
import { fetchThumbnailWithCache } from "../../../data/camera";
|
||||
import { fetchThumbnailUrlWithCache } from "../../../data/camera";
|
||||
|
||||
const UPDATE_INTERVAL = 10000;
|
||||
const DEFAULT_FILTER = "grayscale(100%)";
|
||||
@ -197,21 +196,20 @@ export class HuiImage extends LitElement {
|
||||
if (!this.hass || !this.cameraImage) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const {
|
||||
content_type: contentType,
|
||||
content,
|
||||
} = await fetchThumbnailWithCache(this.hass, this.cameraImage);
|
||||
if (this._cameraImageSrc) {
|
||||
URL.revokeObjectURL(this._cameraImageSrc);
|
||||
}
|
||||
this._cameraImageSrc = URL.createObjectURL(
|
||||
b64toBlob(content, contentType)
|
||||
);
|
||||
this._onImageLoad();
|
||||
} catch (err) {
|
||||
|
||||
const cameraState = this.hass.states[this.cameraImage] as
|
||||
| CameraEntity
|
||||
| undefined;
|
||||
|
||||
if (!cameraState) {
|
||||
this._onImageError();
|
||||
return;
|
||||
}
|
||||
|
||||
this._cameraImageSrc = await fetchThumbnailUrlWithCache(
|
||||
this.hass,
|
||||
this.cameraImage
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
|
@ -56,7 +56,11 @@ export class HuiStateBadgeElement extends LitElement
|
||||
<ha-state-label-badge
|
||||
.hass="${this.hass}"
|
||||
.state="${stateObj}"
|
||||
.title="${computeStateName(stateObj)}"
|
||||
.title="${this._config.title === undefined
|
||||
? computeStateName(stateObj)
|
||||
: this._config.title === null
|
||||
? ""
|
||||
: this._config.title}"
|
||||
></ha-state-label-badge>
|
||||
`;
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ export interface ServiceButtonElementConfig extends LovelaceElementConfig {
|
||||
|
||||
export interface StateBadgeElementConfig extends LovelaceElementConfig {
|
||||
entity: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface StateIconElementConfig extends LovelaceElementConfig {
|
||||
|
@ -549,6 +549,7 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"developer_tools": "Developer tools",
|
||||
"external_app_configuration": "App Configuration",
|
||||
"log_out": "Log out"
|
||||
},
|
||||
"panel": {
|
||||
|
@ -1,6 +1,3 @@
|
||||
const serviceWorkerUrl =
|
||||
__BUILD__ === "latest" ? "/service_worker.js" : "/service_worker_es5.js";
|
||||
|
||||
export const registerServiceWorker = (notifyUpdate = true) => {
|
||||
if (
|
||||
!("serviceWorker" in navigator) ||
|
||||
@ -9,7 +6,7 @@ export const registerServiceWorker = (notifyUpdate = true) => {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
|
||||
navigator.serviceWorker.register("/service_worker.js").then((reg) => {
|
||||
reg.addEventListener("updatefound", () => {
|
||||
const installingWorker = reg.installing;
|
||||
if (!installingWorker || !notifyUpdate) {
|
||||
|
@ -8,10 +8,4 @@ describe("formatDate", () => {
|
||||
it("Formats English dates", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
@ -11,10 +11,4 @@ describe("formatDateTime", () => {
|
||||
"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");
|
||||
});
|
||||
});
|
||||
|
@ -8,10 +8,4 @@ describe("formatTime", () => {
|
||||
it("Formats English times", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
@ -769,7 +769,7 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Ongeldige Gebruikersnaam of wagwoord",
|
||||
"invalid_auth": "Ongeldige gebruikersnaam of wagwoord",
|
||||
"invalid_code": "Ongeldige verifikasiekode"
|
||||
},
|
||||
"abort": {
|
||||
@ -868,6 +868,14 @@
|
||||
"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.",
|
||||
"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": {
|
||||
@ -884,7 +892,7 @@
|
||||
"migrate": {
|
||||
"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_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"
|
||||
},
|
||||
"header": "Wysig gebruikerskoppelvlak",
|
||||
|
@ -868,6 +868,14 @@
|
||||
"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.",
|
||||
"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": {
|
||||
|
@ -868,6 +868,14 @@
|
||||
"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.",
|
||||
"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": {
|
||||
|