Compare commits

..

1 Commits

Author SHA1 Message Date
Paulus Schoutsen
3272c32f87 Move compatibility to own entrypoint again 2020-06-03 16:24:23 -07:00
331 changed files with 6808 additions and 10194 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
hass_frontend
hass_frontend_es5
.git

View File

@@ -34,8 +34,10 @@ jobs:
run: yarn install
env:
CI: true
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations gather-gallery-demos
- name: Build icons
run: ./node_modules/.bin/gulp gen-icons-json
- name: Build translations
run: ./node_modules/.bin/gulp build-translations
- name: Run eslint
run: ./node_modules/.bin/eslint '{**/src,src}/**/*.{js,ts,html}' --ignore-path .gitignore
- name: Run tsc

2
.gitignore vendored
View File

@@ -24,7 +24,7 @@ dist
.vscode/*
!.vscode/extensions.json
# Cast dev settings
# Cast dev settings
src/cast/dev_const.ts
# Secrets

31
Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM node:8.11.1-alpine
# install yarn
ENV PATH /root/.yarn/bin:$PATH
## Install/force base tools
RUN apk update \
&& apk add make g++ curl bash binutils tar git python2 python3 \
&& rm -rf /var/cache/apk/* \
&& /bin/bash \
&& touch ~/.bashrc
## Install yarn
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
## Setup the project
RUN mkdir -p /frontend
WORKDIR /frontend
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
COPY script/docker_entrypoint.sh /usr/bin/docker_entrypoint.sh
RUN chmod +x /usr/bin/docker_entrypoint.sh
CMD [ "docker_entrypoint.sh" ]

View File

@@ -22,6 +22,15 @@ This is the repository for the official [Home Assistant](https://home-assistant.
A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project.
### Docker environment
It is possible to compile the project and/or run commands in the development environment having only the [Docker](https://www.docker.com) pre-installed in the system. On the root of project you can do:
- `sh ./script/docker_run.sh build` Build all the project with one command
- `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the _classic environment_) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
**Note**: if you have installed `npm` in addition to the `docker`, you can use the commands `npm run docker_build` and `npm run bash` to get a full build or bash as explained above
## License
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.

View File

@@ -20,13 +20,7 @@ module.exports.emptyPackages = ({ latestBuild }) =>
// Loads stuff from a CDN
require.resolve("@polymer/font-roboto/roboto.js"),
require.resolve("@vaadin/vaadin-material-styles/font-roboto.js"),
// Compatibility not needed for latest builds
latestBuild &&
// wrapped in require.resolve so it blows up if file no longer exists
require.resolve(
path.resolve(paths.polymer_dir, "src/resources/compatibility.ts")
),
// This polyfill is loaded in workers to support ES5, filter it out.
// Polyfill only needed for ES5 workers so filter out in latestBuild
latestBuild && require.resolve("proxy-polyfill/src/index.js"),
].filter(Boolean);
@@ -57,7 +51,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
].filter(Boolean),
plugins: [
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
!latestBuild && [
[
"@babel/plugin-proposal-object-rest-spread",
{ loose: true, useBuiltIns: true },
],
@@ -73,7 +67,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
require("@babel/plugin-proposal-class-properties").default,
{ loose: true },
],
].filter(Boolean),
],
});
// Are already ES5, cause warnings when babelified.
@@ -85,8 +79,8 @@ module.exports.babelExclude = () => [
const outputPath = (outputRoot, latestBuild) =>
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
const publicPath = (latestBuild, root = "") =>
latestBuild ? `${root}/frontend_latest/` : `${root}/frontend_es5/`;
const publicPath = (latestBuild) =>
latestBuild ? "/frontend_latest/" : "/frontend_es5/";
/*
BundleConfig {
@@ -118,6 +112,7 @@ module.exports.config = {
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",
},
outputPath: outputPath(paths.app_output_root, latestBuild),
@@ -132,6 +127,10 @@ module.exports.config = {
return {
entry: {
main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
compatibility: path.resolve(
paths.polymer_dir,
"src/entrypoints/compatibility.ts"
),
},
outputPath: outputPath(paths.demo_output_root, latestBuild),
publicPath: publicPath(latestBuild),
@@ -170,12 +169,15 @@ module.exports.config = {
},
hassio({ isProdBuild, latestBuild }) {
if (latestBuild) {
throw new Error("Hass.io does not support latest build!");
}
return {
entry: {
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
},
outputPath: outputPath(paths.hassio_output_root, latestBuild),
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
outputPath: paths.hassio_output_root,
publicPath: paths.hassio_publicPath,
isProdBuild,
latestBuild,
dontHash: new Set(["entrypoint"]),

View File

@@ -20,7 +20,6 @@ gulp.task(
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-cast",
"gen-index-cast-dev",
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
)
);

View File

@@ -6,32 +6,30 @@ const merge = require("merge-stream");
const path = require("path");
const paths = require("../paths");
const zopfliOptions = { threshold: 150 };
gulp.task("compress-app", function compressApp() {
const jsLatest = gulp
.src(path.resolve(paths.app_output_latest, "**/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(zopfli({ threshold: 150 }))
.pipe(gulp.dest(paths.app_output_latest));
const jsEs5 = gulp
.src(path.resolve(paths.app_output_es5, "**/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(zopfli({ threshold: 150 }))
.pipe(gulp.dest(paths.app_output_es5));
const polyfills = gulp
.src(path.resolve(paths.app_output_static, "polyfills/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(zopfli({ threshold: 150 }))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "polyfills")));
const translations = gulp
.src(path.resolve(paths.app_output_static, "translations/**/*.json"))
.pipe(zopfli(zopfliOptions))
.pipe(zopfli({ threshold: 150 }))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "translations")));
const icons = gulp
.src(path.resolve(paths.app_output_static, "mdi/*.json"))
.pipe(zopfli(zopfliOptions))
.pipe(zopfli({ threshold: 150 }))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "mdi")));
return merge(jsLatest, jsEs5, polyfills, translations, icons);
@@ -40,6 +38,6 @@ gulp.task("compress-app", function compressApp() {
gulp.task("compress-hassio", function compressApp() {
return gulp
.src(path.resolve(paths.hassio_output_root, "**/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(zopfli())
.pipe(gulp.dest(paths.hassio_output_root));
});

View File

@@ -53,6 +53,7 @@ gulp.task("gen-pages-dev", (done) => {
const content = renderTemplate(page, {
latestPageJS: `/frontend_latest/${page}.js`,
es5Compatibility: "/frontend_es5/compatibility.js",
es5PageJS: `/frontend_es5/${page}.js`,
});
@@ -78,6 +79,7 @@ gulp.task("gen-pages-prod", (done) => {
const content = renderTemplate(page, {
latestPageJS: latestManifest[`${page}.js`],
es5Compatibility: es5Manifest["compatibility.js"],
es5PageJS: es5Manifest[`${page}.js`],
});
@@ -97,6 +99,7 @@ gulp.task("gen-index-app-dev", (done) => {
latestCoreJS: "/frontend_latest/core.js",
latestCustomPanelJS: "/frontend_latest/custom-panel.js",
es5Compatibility: "/frontend_es5/compatibility.js",
es5AppJS: "/frontend_es5/app.js",
es5CoreJS: "/frontend_es5/core.js",
es5CustomPanelJS: "/frontend_es5/custom-panel.js",
@@ -120,6 +123,7 @@ gulp.task("gen-index-app-prod", (done) => {
latestCoreJS: latestManifest["core.js"],
latestCustomPanelJS: latestManifest["custom-panel.js"],
es5Compatibility: es5Manifest["compatibility.js"],
es5AppJS: es5Manifest["app.js"],
es5CoreJS: es5Manifest["core.js"],
es5CustomPanelJS: es5Manifest["custom-panel.js"],
@@ -206,6 +210,7 @@ gulp.task("gen-index-demo-dev", (done) => {
const content = renderDemoTemplate("index", {
latestDemoJS: "/frontend_latest/main.js",
es5Compatibility: "/frontend_es5/compatibility.js",
es5DemoJS: "/frontend_es5/main.js",
});
@@ -228,6 +233,7 @@ gulp.task("gen-index-demo-prod", (done) => {
const content = renderDemoTemplate("index", {
latestDemoJS: latestManifest["main.js"],
es5Compatibility: es5Manifest["compatibility.js"],
es5DemoJS: es5Manifest["main.js"],
});
const minified = minifyHtml(content);

View File

@@ -1,10 +1,7 @@
// Run demo develop mode
const gulp = require("gulp");
const fs = require("fs");
const path = require("path");
const env = require("../env");
const paths = require("../paths");
require("./clean.js");
require("./translations.js");
@@ -15,31 +12,6 @@ require("./service-worker.js");
require("./entry-html.js");
require("./rollup.js");
gulp.task("gather-gallery-demos", async function gatherDemos() {
const files = await fs.promises.readdir(
path.resolve(paths.gallery_dir, "src/demos")
);
let content = "export const DEMOS = {\n";
for (const file of files) {
const demoId = path.basename(file, ".ts");
const demoPath = "../src/demos/" + demoId;
content += ` "${demoId}": () => import("${demoPath}"),\n`;
}
content += "};";
const galleryBuild = path.resolve(paths.gallery_dir, "build");
fs.mkdirSync(galleryBuild, { recursive: true });
fs.writeFileSync(
path.resolve(galleryBuild, "import-demos.ts"),
content,
"utf-8"
);
});
gulp.task(
"develop-gallery",
gulp.series(
@@ -48,11 +20,7 @@ gulp.task(
},
"clean-gallery",
"translations-enable-merge-backend",
gulp.parallel(
"gen-icons-json",
"build-translations",
"gather-gallery-demos"
),
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-gallery",
"gen-index-gallery-dev",
env.useRollup() ? "rollup-dev-server-gallery" : "webpack-dev-server-gallery"
@@ -67,11 +35,7 @@ gulp.task(
},
"clean-gallery",
"translations-enable-merge-backend",
gulp.parallel(
"gen-icons-json",
"build-translations",
"gather-gallery-demos"
),
gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-gallery",
env.useRollup() ? "rollup-prod-gallery" : "webpack-prod-gallery",
"gen-index-gallery-prod"

View File

@@ -36,13 +36,11 @@ function copyMdiIcons(staticDir) {
function copyPolyfills(staticDir) {
const staticPath = genStaticPath(staticDir);
// For custom panels using ES5 builds that don't use Babel 7+
// Web Component polyfills and adapters
copyFileDir(
npmPath("@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"),
staticPath("polyfills/")
);
// Web Component polyfills and adapters
copyFileDir(
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js"),
staticPath("polyfills/")

View File

@@ -1,9 +1,6 @@
const gulp = require("gulp");
const fs = require("fs");
const path = require("path");
const env = require("../env");
const paths = require("../paths");
require("./clean.js");
require("./gen-icons-json.js");
@@ -11,24 +8,6 @@ require("./webpack.js");
require("./compress.js");
require("./rollup.js");
async function writeEntrypointJS() {
// We ship two builds and we need to do feature detection on what version to load.
fs.mkdirSync(paths.hassio_output_root, { recursive: true });
fs.writeFileSync(
path.resolve(paths.hassio_output_root, "entrypoint.js"),
`
try {
new Function("import('${paths.hassio_publicPath}/frontend_latest/entrypoint.js')")();
} catch (err) {
var el = document.createElement('script');
el.src = '${paths.hassio_publicPath}/frontend_es5/entrypoint.js';
document.body.appendChild(el);
}
`,
{ encoding: "utf-8" }
);
}
gulp.task(
"develop-hassio",
gulp.series(
@@ -37,7 +16,6 @@ gulp.task(
},
"clean-hassio",
"gen-icons-json",
writeEntrypointJS,
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
)
);
@@ -51,7 +29,6 @@ gulp.task(
"clean-hassio",
"gen-icons-json",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
writeEntrypointJS,
...// Don't compress running tests
(env.isTest() ? [] : ["compress-hassio"])
)

View File

@@ -92,7 +92,11 @@ gulp.task("rollup-watch-app", () => {
});
gulp.task("rollup-watch-hassio", () => {
watchRollup(rollupConfig.createHassioConfig, ["hassio/src/**"]);
watchRollup(
// Force latestBuild = false for hassio config.
(conf) => rollupConfig.createHassioConfig({ ...conf, latestBuild: false }),
["hassio/src/**"]
);
});
gulp.task("rollup-dev-server-demo", () => {
@@ -133,7 +137,12 @@ gulp.task(
);
gulp.task("rollup-prod-hassio", () =>
bothBuilds(rollupConfig.createHassioConfig, { isProdBuild: true })
buildRollup(
rollupConfig.createHassioConfig({
isProdBuild: true,
latestBuild: false,
})
)
);
gulp.task("rollup-prod-gallery", () =>

View File

@@ -129,7 +129,7 @@ gulp.task("webpack-watch-hassio", () => {
webpack(
createHassioConfig({
isProdBuild: false,
latestBuild: true,
latestBuild: false,
})
).watch({}, handler());
});
@@ -139,8 +139,9 @@ gulp.task(
() =>
new Promise((resolve) =>
webpack(
bothBuilds(createHassioConfig, {
createHassioConfig({
isProdBuild: true,
latestBuild: false,
}),
handler(resolve)
)

View File

@@ -34,7 +34,7 @@ module.exports = {
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_publicPath: "/api/hassio/app",
hassio_publicPath: "/api/hassio/app/",
translations_src: path.resolve(__dirname, "../src/translations"),
};

View File

@@ -14,6 +14,32 @@ module.exports = function (userOptions = {}) {
return {
name: "ignore",
resolveId(importee, importer) {
// Only use ignore to intercept imports that we don't control
// inside node_module dependencies.
if (
importee.endsWith("commonjsHelpers.js") ||
importee.endsWith("rollupPluginBabelHelpers.js") ||
importee.endsWith("?commonjs-proxy") ||
!importer ||
!importer.includes("/node_modules/")
) {
return null;
}
let fullPath;
try {
fullPath = importee.startsWith(".")
? path.resolve(importee, importer)
: require.resolve(importee);
} catch (err) {
console.error("Error in ignore plugin", { importee, importer }, err);
throw err;
}
return files.some((toIgnorePath) => fullPath.startsWith(toIgnorePath))
? fullPath
: null;
},
load(id) {
return files.some((toIgnorePath) => id.startsWith(toIgnorePath))

View File

@@ -70,9 +70,7 @@ const createWebpackConfig = ({
if (
!context.includes("/node_modules/") ||
// calling define.amd will call require("!!webpack amd options")
resource.startsWith("!!webpack") ||
// loaded by webpack dev server but doesn't exist.
resource === "webpack/hot"
resource.startsWith("!!webpack")
) {
return false;
}
@@ -82,11 +80,7 @@ const createWebpackConfig = ({
? path.resolve(context, resource)
: require.resolve(resource);
} catch (err) {
console.error(
"Error in Home Assistant ignore plugin",
resource,
context
);
console.error("Error in ignore plugin", resource, context);
throw err;
}

View File

@@ -37,21 +37,24 @@
<body>
<%= renderTemplate('_js_base') %>
<script>
import("<%= latestLauncherJS %>");
window.latestJS = true;
<script type="module" crossorigin="use-credentials">
import "<%= latestLauncherJS %>";
</script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>");
};
<% } else { %>
_ls("<%= es5LauncherJS %>");
<% } %>
}
<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");
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>");
};
<% } else { %>
_ls("<%= es5LauncherJS %>");
<% } %>
}
})();
</script>
<hc-layout subtitle="FAQ">
@@ -251,7 +254,7 @@ http:
<script>
var _gaq = [["_setAccount", "UA-57927901-9"], ["_trackPageview"]];
(function (d, t) {
(function(d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =

View File

@@ -28,21 +28,24 @@
<hc-connect></hc-connect>
<script>
import("<%= latestLauncherJS %>");
window.latestJS = true;
<script type="module" crossorigin="use-credentials">
import "<%= latestLauncherJS %>";
</script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>");
};
<% } else { %>
_ls("<%= es5LauncherJS %>");
<% } %>
}
<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");
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>");
};
<% } else { %>
_ls("<%= es5LauncherJS %>");
<% } %>
}
})();
</script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){

View File

@@ -192,8 +192,6 @@ export class HcMain extends HassElement {
this._handleNewLovelaceConfig(lovelaceConfig)
);
} catch (err) {
// eslint-disable-next-line
console.log("Error fetching Lovelace configuration", err, msg);
// Generate a Lovelace config.
this._unsubLovelace = () => undefined;
await this._generateLovelaceConfig();

View File

@@ -1,8 +1,11 @@
const { createCastConfig } = require("../build-scripts/webpack.js");
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
const { isProdBuild } = require("../build-scripts/env.js");
// File just used for stats builds
const latestBuild = true;
module.exports = createCastConfig({
isProdBuild: isProdBuild(),
isStatsBuild: isStatsBuild(),
latestBuild: true,
latestBuild,
});

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button";
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -9,7 +10,6 @@ import {
} from "lit-element";
import { until } from "lit-html/directives/until";
import "../../../src/components/ha-card";
import "../../../src/components/ha-circular-progress";
import { LovelaceCardConfig } from "../../../src/data/lovelace";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { Lovelace, LovelaceCard } from "../../../src/panels/lovelace/types";
@@ -49,7 +49,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
<div class="picker">
<div class="label">
${this._switching
? html`<ha-circular-progress active></ha-circular-progress>`
? html` <paper-spinner-lite active></paper-spinner-lite> `
: until(
selectedDemoConfig.then(
(conf) => html`

View File

@@ -1,4 +1,3 @@
import "../../src/resources/compatibility";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate";
import {

View File

@@ -86,26 +86,30 @@
<%= renderTemplate('_js_base') %>
<%= renderTemplate('_preload_roboto') %>
<script>
import("<%= latestDemoJS %>");
window.latestJS = true;
</script>
<script type="module" src="<%= latestDemoJS %>"></script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5DemoJS %>");
};
<% } else { %>
_ls("<%= es5DemoJS %>");
<% } %>
}
<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");
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5Compatibility %>").then(function() {
System.import("<%= es5DemoJS %>");
});
};
<% } else { %>
_ls("<%= es5Compatibility %>");
_ls("<%= es5DemoJS %>");
<% } %>
}
})();
</script>
<script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function (d, t) {
(function(d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =

View File

@@ -3,7 +3,6 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-switch";
import "../../../src/components/ha-formfield";
import "./demo-card";
class DemoCards extends PolymerElement {
@@ -27,10 +26,9 @@ class DemoCards extends PolymerElement {
</style>
<app-toolbar>
<div class="filters">
<ha-formfield label="Show config">
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
</ha-switch>
</ha-formfield>
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
Show config
</ha-switch>
</div>
</app-toolbar>
<div class="cards">

View File

@@ -11,7 +11,9 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/components/ha-card";
import "../../src/managers/notification-manager";
import "../../src/styles/polymer-ha-style";
import { DEMOS } from "../build/import-demos";
// eslint-disable-next-line no-undef
const DEMOS = require.context("./demos", true, /^(.*\.(ts$))[^.]*$/im);
const fixPath = (path) => path.substr(2, path.length - 5);
@@ -161,7 +163,7 @@ class HaGallery extends PolymerElement {
},
_demos: {
type: Array,
value: Object.keys(DEMOS),
value: DEMOS.keys().map(fixPath),
},
_lovelaceDemos: {
type: Array,
@@ -208,7 +210,7 @@ class HaGallery extends PolymerElement {
while (root.lastChild) root.removeChild(root.lastChild);
if (demo) {
DEMOS[demo]();
DEMOS(`./${demo}.ts`);
const el = document.createElement(demo);
root.appendChild(el);
}

View File

@@ -1,8 +1,5 @@
const { createGalleryConfig } = require("../build-scripts/webpack.js");
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
module.exports = createGalleryConfig({
isProdBuild: isProdBuild(),
isStatsBuild: isStatsBuild(),
latestBuild: true,
});

View File

@@ -22,7 +22,7 @@ import "../../../src/layouts/hass-tabs-subpage";
import "../../../src/layouts/loading-screen";
import { HomeAssistant, Route } from "../../../src/types";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs";
import { supervisorTabs } from "../hassio-panel";
import "./hassio-addon-repository";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {

View File

@@ -1,3 +1,4 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -11,7 +12,6 @@ import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/components/ha-circular-progress";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-network";
@@ -24,7 +24,7 @@ class HassioAddonConfigDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<div class="content">

View File

@@ -1,3 +1,4 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -13,7 +14,6 @@ import {
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import "../../../../src/layouts/loading-screen";
import "../../../../src/components/ha-circular-progress";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -35,7 +35,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<div class="content">

View File

@@ -4,6 +4,7 @@ import {
mdiInformationVariant,
mdiMathLog,
} from "@mdi/js";
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -19,7 +20,6 @@ import {
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import "../../../src/layouts/hass-tabs-subpage";
import "../../../src/components/ha-circular-progress";
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
@@ -56,7 +56,7 @@ class HassioAddonDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
const addonTabs: PageNavigation[] = [

View File

@@ -1,3 +1,4 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -8,7 +9,6 @@ import {
TemplateResult,
} from "lit-element";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import "../../../../src/components/ha-circular-progress";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -24,7 +24,7 @@ class HassioAddonInfoDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`

View File

@@ -1,3 +1,4 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -8,7 +9,6 @@ import {
TemplateResult,
} from "lit-element";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import "../../../../src/components/ha-circular-progress";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -22,7 +22,7 @@ class HassioAddonLogDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html` <ha-circular-progress active></ha-circular-progress> `;
return html` <paper-spinner-lite active></paper-spinner-lite> `;
}
return html`
<div class="content">

View File

@@ -15,7 +15,7 @@ import {
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import { supervisorTabs } from "../hassio-panel";
import "./hassio-addons";
import "./hassio-update";

View File

@@ -5,7 +5,7 @@ import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "../../../../src/components/ha-circular-progress";
import "@polymer/paper-spinner/paper-spinner";
import {
css,
CSSResult,
@@ -108,7 +108,7 @@ class HassioRepositoriesDialog extends LitElement {
></paper-input>
<mwc-button @click=${this._addRepository}>
${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>`
? html`<paper-spinner active></paper-spinner>`
: "Add"}
</mwc-button>
</div>

View File

@@ -1,6 +1,9 @@
import "../../src/resources/compatibility";
import "../../src/resources/roboto";
import "./hassio-main";
window.loadES5Adapter().then(() => {
// eslint-disable-next-line
import(/* webpackChunkName: "roboto" */ "../../src/resources/roboto");
// eslint-disable-next-line
import(/* webpackChunkName: "hassio-main" */ "./hassio-main");
});
const styleEl = document.createElement("style");
styleEl.innerHTML = `

View File

@@ -14,9 +14,7 @@ import {
createHassioSession,
fetchHassioHomeAssistantInfo,
fetchHassioSupervisorInfo,
fetchHassioInfo,
HassioHomeAssistantInfo,
HassioInfo,
HassioPanelInfo,
HassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
@@ -77,8 +75,6 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
@property() private _hostInfo: HassioHostInfo;
@property() private _hassioInfo?: HassioInfo;
@property() private _hassOsInfo?: HassioHassOSInfo;
@property() private _hassInfo: HassioHomeAssistantInfo;
@@ -151,7 +147,6 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
hass: this.hass,
narrow: this.narrow,
supervisorInfo: this._supervisorInfo,
hassioInfo: this._hassioInfo,
hostInfo: this._hostInfo,
hassInfo: this._hassInfo,
hassOsInfo: this._hassOsInfo,
@@ -161,7 +156,6 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
el.hass = this.hass;
el.narrow = this.narrow;
el.supervisorInfo = this._supervisorInfo;
el.hassioInfo = this._hassioInfo;
el.hostInfo = this._hostInfo;
el.hassInfo = this._hassInfo;
el.hassOsInfo = this._hassOsInfo;
@@ -175,14 +169,12 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
return;
}
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
const [supervisorInfo, hostInfo, hassInfo] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
]);
this._supervisorInfo = supervisorInfo;
this._hassioInfo = hassioInfo;
this._hostInfo = hostInfo;
this._hassInfo = hassInfo;

View File

@@ -3,7 +3,6 @@ import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioInfo,
} from "../../src/data/hassio/supervisor";
import {
HassRouterPage,
@@ -27,8 +26,6 @@ class HassioPanelRouter extends HassRouterPage {
@property({ attribute: false }) public supervisorInfo: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo: HassioHostInfo;
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
@@ -57,7 +54,6 @@ class HassioPanelRouter extends HassRouterPage {
el.route = this.route;
el.narrow = this.narrow;
el.supervisorInfo = this.supervisorInfo;
el.hassioInfo = this.hassioInfo;
el.hostInfo = this.hostInfo;
el.hassInfo = this.hassInfo;
el.hassOsInfo = this.hassOsInfo;

View File

@@ -1,3 +1,4 @@
import { mdiBackupRestore, mdiCogs, mdiStore, mdiViewDashboard } from "@mdi/js";
import {
customElement,
html,
@@ -9,11 +10,34 @@ import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioInfo,
} from "../../src/data/hassio/supervisor";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
export const supervisorTabs: PageNavigation[] = [
{
name: "Dashboard",
path: `/hassio/dashboard`,
iconPath: mdiViewDashboard,
},
{
name: "Add-on store",
path: `/hassio/store`,
iconPath: mdiStore,
},
{
name: "Snapshots",
path: `/hassio/snapshots`,
iconPath: mdiBackupRestore,
},
{
name: "System",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];
@customElement("hassio-panel")
class HassioPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -24,8 +48,6 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo!: HassioHostInfo;
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
@@ -39,7 +61,6 @@ class HassioPanel extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.supervisorInfo=${this.supervisorInfo}
.hassioInfo=${this.hassioInfo}
.hostInfo=${this.hostInfo}
.hassInfo=${this.hassInfo}
.hassOsInfo=${this.hassOsInfo}

View File

@@ -1,25 +0,0 @@
import { mdiBackupRestore, mdiCogs, mdiStore, mdiViewDashboard } from "@mdi/js";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
export const supervisorTabs: PageNavigation[] = [
{
name: "Dashboard",
path: `/hassio/dashboard`,
iconPath: mdiViewDashboard,
},
{
name: "Add-on store",
path: `/hassio/store`,
iconPath: mdiStore,
},
{
name: "Snapshots",
path: `/hassio/snapshots`,
iconPath: mdiBackupRestore,
},
{
name: "System",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];

View File

@@ -37,7 +37,7 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import "../components/hassio-card-content";
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
import { supervisorTabs } from "../hassio-tabs";
import { supervisorTabs } from "../hassio-panel";
import { hassioStyle } from "../resources/hassio-style";
interface CheckboxItem {

View File

@@ -19,7 +19,6 @@ import {
shutdownHost,
updateOS,
} from "../../../src/data/hassio/host";
import { HassioInfo } from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -36,8 +35,6 @@ class HassioHostInfo extends LitElement {
@property() public hostInfo!: HassioHostInfoType;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
@property() private _errors?: string;
@@ -57,12 +54,6 @@ class HassioHostInfo extends LitElement {
<td>System</td>
<td>${this.hostInfo.operating_system}</td>
</tr>
${!this.hostInfo.features.includes("hassos")
? html`<tr>
<td>Docker version</td>
<td>${this.hassioInfo.docker}</td>
</tr>`
: ""}
${this.hostInfo.deployment
? html`
<tr>

View File

@@ -1,3 +1,4 @@
import "@polymer/paper-menu-button/paper-menu-button";
import {
css,
CSSResult,
@@ -11,14 +12,11 @@ import {
HassioHassOSInfo,
HassioHostInfo,
} from "../../../src/data/hassio/host";
import {
HassioSupervisorInfo,
HassioInfo,
} from "../../../src/data/hassio/supervisor";
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import { supervisorTabs } from "../hassio-panel";
import { hassioStyle } from "../resources/hassio-style";
import "./hassio-host-info";
import "./hassio-supervisor-info";
@@ -34,11 +32,9 @@ class HassioSystem extends LitElement {
@property() public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property() public hostInfo!: HassioHostInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
public render(): TemplateResult | void {
return html`
@@ -60,7 +56,6 @@ class HassioSystem extends LitElement {
></hassio-supervisor-info>
<hassio-host-info
.hass=${this.hass}
.hassioInfo=${this.hassioInfo}
.hostInfo=${this.hostInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-host-info>

View File

@@ -1,8 +1,11 @@
const { createHassioConfig } = require("../build-scripts/webpack.js");
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
const { isProdBuild } = require("../build-scripts/env.js");
// File just used for stats builds
const latestBuild = false;
module.exports = createHassioConfig({
isProdBuild: isProdBuild(),
isStatsBuild: isStatsBuild(),
latestBuild: true,
latestBuild,
});

View File

@@ -17,7 +17,9 @@
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
"format": "npm run format:eslint && npm run format:prettier",
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
"test": "npm run lint && npm run mocha"
"test": "npm run lint && npm run mocha",
"docker_build": "sh ./script/docker_run.sh build $npm_package_version",
"bash": "sh ./script/docker_run.sh bash $npm_package_version"
},
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
@@ -26,7 +28,6 @@
"@fullcalendar/core": "^5.0.0-beta.2",
"@fullcalendar/daygrid": "^5.0.0-beta.2",
"@material/chips": "7.0.0-canary.d92d8c93e.0",
"@material/circular-progress": "7.0.0-canary.d92d8c93e.0",
"@material/mwc-button": "^0.15.0",
"@material/mwc-checkbox": "^0.15.0",
"@material/mwc-dialog": "^0.15.0",
@@ -48,6 +49,7 @@
"@polymer/iron-image": "^3.0.1",
"@polymer/iron-input": "^3.0.1",
"@polymer/iron-label": "^3.0.1",
"@polymer/iron-media-query": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.2",
"@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-card": "^3.0.1",
@@ -65,6 +67,7 @@
"@polymer/paper-radio-group": "^3.0.1",
"@polymer/paper-ripple": "^3.0.1",
"@polymer/paper-slider": "^3.0.1",
"@polymer/paper-spinner": "^3.0.2",
"@polymer/paper-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.0.1",
"@polymer/paper-toast": "^3.0.1",
@@ -73,7 +76,6 @@
"@thomasloven/round-slider": "0.5.0",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0",
@@ -87,7 +89,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^5.3.0",
"home-assistant-js-websocket": "^5.2.1",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9",
"js-yaml": "^3.13.1",
@@ -106,8 +108,6 @@
"roboto-fontface": "^0.10.0",
"superstruct": "^0.6.1",
"unfetch": "^4.1.0",
"vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2",
"workbox-core": "^5.1.3",
"workbox-precaching": "^5.1.3",
@@ -187,7 +187,7 @@
"sinon": "^7.3.1",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6",
"terser-webpack-plugin": "^1.2.3",
"ts-lit-plugin": "^1.1.10",
"ts-mocha": "^6.0.0",
"typescript": "^3.8.3",

View File

@@ -0,0 +1,14 @@
#!/bin/bash
# Docker entry point inspired by travis build and script/build_frontend
# Stop on errors
set -e
# Build the frontend but not used the npm run build
/bin/bash script/build_frontend
# TEST
npm run test
#
#xvfb-run wct

103
script/docker_run.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/bin/bash
# Basic Docker Management scripts
# With this script you can build software, or enter an agnostic development environment and run commands interactively.
check_mandatory_tools(){
if [ "x$(which docker)" == "x" ]; then
echo "UNKNOWN - Missing docker binary! Are you sure it is installed and reachable?"
exit 3
fi
docker info > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "UNKNOWN - Unable to talk to the docker daemon! Maybe the docker daemon is not running"
exit 3
fi
}
check_dev_image(){
if [[ "$(docker images -q ${IMAGE_NAME}:$IMAGE_TAG 2> /dev/null)" == "" ]]; then
echo "UNKNOWN - Can't find the development docker image ${IMAGE_NAME}:$IMAGE_TAG"
while true; do
read -p "Do you want to create it now?" yn
case $yn in
[Yy]* ) create_image; break;;
[Nn]* ) exit 3;;
* ) echo "Please answer y or n";;
esac
done
fi
}
# Building the basic image for compiling the production frontend
create_image(){
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
}
#
# Execute interactive bash on basic image
#
run_bash_on_docker(){
check_dev_image
docker run -it \
-v $PWD/:/frontend/ \
-v /frontend/node_modules \
-v /frontend/bower_components \
${IMAGE_NAME}:${IMAGE_TAG} /bin/bash
}
#
# Execute the basic image for compiling the production frontend
#
build_all(){
check_dev_image
docker run -it \
-v $PWD/:/frontend/ \
-v /frontend/node_modules \
-v /frontend/bower_components \
${IMAGE_NAME}:${IMAGE_TAG} /bin/bash script/build_frontend
}
# Init Global Variable
IMAGE_NAME=home_assistant_fe_image
IMAGE_TAG=${2:-latest}
check_mandatory_tools
case "$1" in
setup_env)
create_image
;;
bash)
run_bash_on_docker
;;
build)
build_all
;;
*)
echo "NAME"
echo " Docker Management."
echo ""
echo "SYNOPSIS"
echo " ${0} command [version]"
echo ""
echo "DESCRIPTION"
echo " With this script you can build software, or enter an agnostic development environment and run commands interactively."
echo ""
echo " The command are:"
echo " setup_env Create develop images"
echo " bash Run bash on develop enviroments"
echo " build Run silent build"
echo ""
echo " The version is optional, if not inserted it assumes \"latest\". "
exit 1
;;
esac
exit 0

View File

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

View File

@@ -6,18 +6,19 @@ import {
property,
PropertyValues,
} from "lit-element";
import {
AuthProvider,
fetchAuthProviders,
AuthUrlSearchParams,
} from "../data/auth";
import { AuthProvider, fetchAuthProviders } from "../data/auth";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { registerServiceWorker } from "../util/register-service-worker";
import "./ha-auth-flow";
import { extractSearchParamsObject } from "../common/url/search-params";
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
interface QueryParams {
client_id?: string;
redirect_uri?: string;
state?: string;
}
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public clientId?: string;
@@ -32,7 +33,14 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
constructor() {
super();
this.translationFragment = "page-authorize";
const query = extractSearchParamsObject() as AuthUrlSearchParams;
const query: QueryParams = {};
const values = location.search.substr(1).split("&");
for (const item of values) {
const value = item.split("=");
if (value.length > 1) {
query[decodeURIComponent(value[0])] = decodeURIComponent(value[1]);
}
}
if (query.client_id) {
this.clientId = query.client_id;
}
@@ -137,7 +145,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
response.status === 400 &&
authProviders.code === "onboarding_required"
) {
location.href = `/onboarding.html${location.search}`;
location.href = "/?";
return;
}

View File

@@ -37,7 +37,6 @@ export const DOMAINS_WITH_MORE_INFO = [
"fan",
"group",
"history_graph",
"humidifier",
"input_datetime",
"light",
"lock",
@@ -80,7 +79,6 @@ export const DOMAINS_TOGGLE = new Set([
"switch",
"group",
"automation",
"humidifier",
]);
/** Temperature units. */

View File

@@ -55,12 +55,6 @@ export const computeStateDisplay = (
return formatDateTime(date, language);
}
if (domain === "humidifier") {
if (stateObj.state === "on" && stateObj.attributes.humidity) {
return `${stateObj.attributes.humidity}%`;
}
}
return (
// Return device class translation
(stateObj.attributes.device_class &&

View File

@@ -22,7 +22,6 @@ const fixedIcons = {
history_graph: "hass:chart-line",
homeassistant: "hass:home-assistant",
homekit: "hass:home-automation",
humidifier: "hass:air-humidifier",
image_processing: "hass:image-filter-frames",
input_boolean: "hass:toggle-switch-outline",
input_datetime: "hass:calendar-clock",

View File

@@ -6,7 +6,7 @@ export const isValidEntityId = (entityId: string) =>
export const createValidEntityId = (input: string) =>
input
.toLowerCase()
.replace(/\s|'|\./g, "_") // replace spaces, points and quotes with underscore
.replace(/\s|'/g, "_") // replace spaces and quotes with underscore
.replace(/\W/g, "") // remove not allowed chars
.replace(/_{2,}/g, "_") // replace multiple underscores with 1
.replace(/_$/, ""); // remove underscores at the end

View File

@@ -8,7 +8,6 @@ export const iconColorCSS = css`
ha-icon[data-domain="camera"][data-state="streaming"],
ha-icon[data-domain="cover"][data-state="open"],
ha-icon[data-domain="fan"][data-state="on"],
ha-icon[data-domain="humidifier"][data-state="on"],
ha-icon[data-domain="light"][data-state="on"],
ha-icon[data-domain="input_boolean"][data-state="on"],
ha-icon[data-domain="lock"][data-state="unlocked"],
@@ -58,11 +57,8 @@ export const iconColorCSS = css`
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
opacity: 0;
}
}

View File

@@ -1,8 +0,0 @@
export const extractSearchParamsObject = (): { [key: string]: string } => {
const query = {};
const searchParams = new URLSearchParams(location.search);
for (const [key, value] of searchParams.entries()) {
query[key] = value;
}
return query;
};

View File

@@ -1,5 +1,5 @@
import "@material/mwc-button";
import "../ha-circular-progress";
import "@polymer/paper-spinner/paper-spinner";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -8,9 +8,6 @@ class HaProgressButton extends PolymerElement {
static get template() {
return html`
<style>
:host {
outline: none;
}
.container {
position: relative;
display: inline-block;
@@ -51,9 +48,7 @@ class HaProgressButton extends PolymerElement {
<slot></slot>
</mwc-button>
<template is="dom-if" if="[[progress]]">
<div class="progress">
<ha-circular-progress active size="small"></ha-circular-progress>
</div>
<div class="progress"><paper-spinner active=""></paper-spinner></div>
</template>
</div>
`;

View File

@@ -619,11 +619,6 @@ export class HaDataTable extends LitElement {
text-transform: inherit;
}
.mdc-data-table__cell a {
color: inherit;
text-decoration: none;
}
.mdc-data-table__cell--numeric {
text-align: right;
}

View File

@@ -1,228 +0,0 @@
import Vue from "vue";
import wrap from "@vue/web-component-wrapper";
import DateRangePicker from "vue2-daterange-picker";
// @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event";
import { Constructor } from "../types";
import { customElement } from "lit-element/lib/decorators";
const Component = Vue.extend({
props: {
twentyfourHours: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
ranges: {
type: Boolean,
default: true,
},
startDate: {
type: [String, Date],
default() {
return new Date();
},
},
endDate: {
type: [String, Date],
default() {
return new Date();
},
},
},
render(createElement) {
// @ts-ignore
return createElement(DateRangePicker, {
props: {
"time-picker": true,
"auto-apply": false,
opens: "right",
"show-dropdowns": false,
"time-picker24-hour": this.twentyfourHours,
disabled: this.disabled,
ranges: this.ranges ? {} : false,
},
model: {
value: {
startDate: this.startDate,
endDate: this.endDate,
},
callback: (value) => {
// @ts-ignore
fireEvent(this.$el as HTMLElement, "change", value);
},
expression: "dateRange",
},
scopedSlots: {
input() {
return createElement("slot", {
domProps: { name: "input" },
});
},
header() {
return createElement("slot", {
domProps: { name: "header" },
});
},
ranges() {
return createElement("slot", {
domProps: { name: "ranges" },
});
},
footer() {
return createElement("slot", {
domProps: { name: "footer" },
});
},
},
});
},
});
const WrappedElement: Constructor<HTMLElement> = wrap(Vue, Component);
@customElement("date-range-picker")
class DateRangePickerElement extends WrappedElement {
constructor() {
super();
const style = document.createElement("style");
style.innerHTML = `
${dateRangePickerStyles}
.calendars {
display: flex;
}
.daterangepicker {
left: 0px !important;
top: auto;
background-color: var(--card-background-color);
border: none;
border-radius: var(--ha-card-border-radius, 4px);
box-shadow: var(
--ha-card-box-shadow,
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12)
);
color: var(--primary-text-color);
min-width: initial !important;
}
.daterangepicker:after {
border-bottom: 6px solid var(--card-background-color);
}
.daterangepicker .calendar-table {
background-color: var(--card-background-color);
border: none;
}
.daterangepicker .calendar-table td,
.daterangepicker .calendar-table th {
background-color: transparent;
color: var(--secondary-text-color);
border-radius: 0;
outline: none;
width: 32px;
height: 32px;
}
.daterangepicker td.off,
.daterangepicker td.off.end-date,
.daterangepicker td.off.in-range,
.daterangepicker td.off.start-date {
background-color: var(--secondary-background-color);
color: var(--disabled-text-color);
}
.daterangepicker td.in-range {
background-color: var(--light-primary-color);
color: var(--primary-text-color);
}
.daterangepicker td.active,
.daterangepicker td.active:hover {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
.daterangepicker td.start-date.end-date {
border-radius: 50%;
}
.daterangepicker td.start-date {
border-radius: 50% 0 0 50%;
}
.daterangepicker td.end-date {
border-radius: 0 50% 50% 0;
}
.reportrange-text {
background: none !important;
padding: 0 !important;
border: none !important;
}
.daterangepicker .calendar-table .next span,
.daterangepicker .calendar-table .prev span {
border: solid var(--primary-text-color);
border-width: 0 2px 2px 0;
}
.daterangepicker .ranges li {
outline: none;
}
.daterangepicker .ranges li:hover {
background-color: var(--secondary-background-color);
}
.daterangepicker .ranges li.active {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
.daterangepicker select.ampmselect,
.daterangepicker select.hourselect,
.daterangepicker select.minuteselect,
.daterangepicker select.secondselect {
background: transparent;
border: 1px solid var(--divider-color);
color: var(--primary-color);
}
.daterangepicker .drp-buttons .btn {
border: 1px solid var(--primary-color);
background-color: transparent;
color: var(--primary-color);
border-radius: 4px;
padding: 8px;
cursor: pointer;
}
.calendars-container {
flex-direction: column;
align-items: center;
}
.drp-calendar.col.right .calendar-table {
display: none;
}
.daterangepicker.show-ranges .drp-calendar.left {
border-left: 0px;
}
.daterangepicker .drp-calendar.left {
padding: 8px;
}
.daterangepicker.show-calendar .ranges {
margin-top: 0;
padding-top: 8px;
border-right: 1px solid var(--divider-color);
}
@media only screen and (max-width: 800px) {
.calendars {
flex-direction: column;
}
}
.calendar-table {
padding: 0 !important;
}
`;
const shadowRoot = this.shadowRoot!;
shadowRoot.appendChild(style);
// Stop click events from reaching the document, otherwise it will close the picker immediately.
shadowRoot.addEventListener("click", (ev) => ev.stopPropagation());
}
}
declare global {
interface HTMLElementTagNameMap {
"date-range-picker": DateRangePickerElement;
}
}

View File

@@ -22,7 +22,7 @@ const isOn = (stateObj?: HassEntity) =>
!STATES_OFF.includes(stateObj.state) &&
!UNAVAILABLE_STATES.includes(stateObj.state);
export class HaEntityToggle extends LitElement {
class HaEntityToggle extends LitElement {
// hass is not a property so that we only re-render on stateObj changes
public hass?: HomeAssistant;

View File

@@ -40,7 +40,7 @@ export class HaButtonMenu extends LitElement {
static get styles(): CSSResult {
return css`
:host {
display: inline-block;
display: block;
position: relative;
}
`;

View File

@@ -176,9 +176,7 @@ class HaCameraStream extends LitElement {
Hls: HLSModule,
url: string
) {
const hls = new Hls({
liveBackBufferLength: 60,
});
const hls = new Hls();
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);
hls.on(Hls.Events.MEDIA_ATTACHED, () => {

View File

@@ -12,8 +12,6 @@ import {
class HaCard extends LitElement {
@property() public header?: string;
@property({ type: Boolean, reflect: true }) public outlined = false;
static get styles(): CSSResult {
return css`
:host {
@@ -21,12 +19,12 @@ class HaCard extends LitElement {
--ha-card-background,
var(--paper-card-background-color, white)
);
border-radius: var(--ha-card-border-radius, 4px);
border-radius: var(--ha-card-border-radius, 2px);
box-shadow: var(
--ha-card-box-shadow,
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12)
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2)
);
color: var(--primary-text-color);
display: block;
@@ -34,16 +32,6 @@ class HaCard extends LitElement {
position: relative;
}
:host([outlined]) {
box-shadow: none;
border-width: 1px;
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
}
.card-header,
:host ::slotted(.card-header) {
color: var(--ha-card-header-color, --primary-text-color);

View File

@@ -1,93 +0,0 @@
import {
LitElement,
TemplateResult,
property,
svg,
html,
customElement,
unsafeCSS,
SVGTemplateResult,
css,
} from "lit-element";
// @ts-ignore
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
import { classMap } from "lit-html/directives/class-map";
@customElement("ha-circular-progress")
export class HaCircularProgress extends LitElement {
@property({ type: Boolean })
public active = false;
@property()
public alt = "Loading";
@property()
public size: "small" | "medium" | "large" = "medium";
protected render(): TemplateResult | void {
let indeterminatePart: SVGTemplateResult;
if (this.size === "small") {
indeterminatePart = svg`
<svg class="mdc-circular-progress__indeterminate-circle-graphic" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="8.75" stroke-dasharray="54.978" stroke-dashoffset="27.489"/>
</svg>`;
} else if (this.size === "large") {
indeterminatePart = svg`
<svg class="mdc-circular-progress__indeterminate-circle-graphic" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="18" stroke-dasharray="113.097" stroke-dashoffset="56.549"/>
</svg>`;
} else {
// medium
indeterminatePart = svg`
<svg class="mdc-circular-progress__indeterminate-circle-graphic" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="12.5" stroke-dasharray="78.54" stroke-dashoffset="39.27"/>
</svg>`;
}
// ignoring prettier as it will introduce unwanted whitespace
// We have not implemented the determinate support of mdc circular progress.
// prettier-ignore
return html`
<div
class="mdc-circular-progress ${classMap({
"mdc-circular-progress--indeterminate": this.active,
[`mdc-circular-progress--${this.size}`]: true,
})}"
role="progressbar"
aria-label=${this.alt}
aria-valuemin="0"
aria-valuemax="1"
>
<div class="mdc-circular-progress__indeterminate-container">
<div class="mdc-circular-progress__spinner-layer">
<div class="mdc-circular-progress__circle-clipper mdc-circular-progress__circle-left">
${indeterminatePart}
</div><div class="mdc-circular-progress__gap-patch">
${indeterminatePart}
</div><div class="mdc-circular-progress__circle-clipper mdc-circular-progress__circle-right">
${indeterminatePart}
</div>
</div>
</div>
</div>
`;
}
static get styles() {
return [
unsafeCSS(progressStyles),
css`
:host {
text-align: initial;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-circular-progress": HaCircularProgress;
}
}

View File

@@ -1,195 +0,0 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
PropertyValues,
} from "lit-element";
import { HomeAssistant } from "../types";
import { mdiCalendar } from "@mdi/js";
import { formatDateTime } from "../common/datetime/format_date_time";
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import "./ha-svg-icon";
import "@polymer/paper-input/paper-input";
import "@material/mwc-list/mwc-list";
import "./date-range-picker";
export interface DateRangePickerRanges {
[key: string]: [Date, Date];
}
@customElement("ha-date-range-picker")
export class HaDateRangePicker extends LitElement {
@property() public hass!: HomeAssistant;
@property() public startDate!: Date;
@property() public endDate!: Date;
@property() public ranges?: DateRangePickerRanges;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) private _hour24format = false;
protected updated(changedProps: PropertyValues) {
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this._hour24format = this._compute24hourFormat();
}
}
}
protected render(): TemplateResult {
return html`
<date-range-picker
?disabled=${this.disabled}
twentyfour-hours=${this._hour24format}
start-date=${this.startDate}
end-date=${this.endDate}
?ranges=${this.ranges !== undefined}
>
<div slot="input" class="date-range-inputs">
<ha-svg-icon path=${mdiCalendar}></ha-svg-icon>
<paper-input
.value=${formatDateTime(this.startDate, this.hass.language)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></paper-input>
<paper-input
.value=${formatDateTime(this.endDate, this.hass.language)}
label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></paper-input>
</div>
${this.ranges
? html`<div slot="ranges" class="date-range-ranges">
<mwc-list @click=${this._setDateRange}>
${Object.entries(this.ranges).map(
([name, dates]) => html`<mwc-list-item
.activated=${this.startDate.getTime() ===
dates[0].getTime() &&
this.endDate.getTime() === dates[1].getTime()}
.startDate=${dates[0]}
.endDate=${dates[1]}
>
${name}
</mwc-list-item>`
)}
</mwc-list>
</div>`
: ""}
<div slot="footer" class="date-range-footer">
<mwc-button @click=${this._cancelDateRange}
>${this.hass.localize("ui.common.cancel")}</mwc-button
>
<mwc-button @click=${this._applyDateRange}
>${this.hass.localize(
"ui.components.date-range-picker.select"
)}</mwc-button
>
</div>
</date-range-picker>
`;
}
private _compute24hourFormat() {
return (
new Intl.DateTimeFormat(this.hass.language, {
hour: "numeric",
})
.formatToParts(new Date(2020, 0, 1, 13))
.find((part) => part.type === "hour")!.value.length === 2
);
}
private _setDateRange(ev: Event) {
const target = ev.target as any;
const startDate = target.startDate;
const endDate = target.endDate;
const dateRangePicker = this._dateRangePicker;
dateRangePicker.clickRange([startDate, endDate]);
dateRangePicker.clickedApply();
}
private _cancelDateRange() {
this._dateRangePicker.clickCancel();
}
private _applyDateRange() {
this._dateRangePicker.clickedApply();
}
private get _dateRangePicker() {
const dateRangePicker = this.shadowRoot!.querySelector(
"date-range-picker"
) as any;
return dateRangePicker.vueComponent.$children[0];
}
private _handleInputClick() {
// close the date picker, so it will open again on the click event
if (this._dateRangePicker.open) {
this._dateRangePicker.open = false;
}
}
static get styles(): CSSResult {
return css`
ha-svg-icon {
margin-right: 8px;
}
.date-range-inputs {
display: flex;
align-items: center;
}
.date-range-ranges {
border-right: 1px solid var(--divider-color);
}
@media only screen and (max-width: 800px) {
.date-range-ranges {
border-right: none;
border-bottom: 1px solid var(--divider-color);
}
}
.date-range-footer {
display: flex;
justify-content: flex-end;
padding: 8px;
border-top: 1px solid var(--divider-color);
}
paper-input {
display: inline-block;
max-width: 200px;
}
paper-input:last-child {
margin-left: 8px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-date-range-picker": HaDateRangePicker;
}
}

View File

@@ -0,0 +1,24 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-label-badge";
class HaDemoBadge extends PolymerElement {
static get template() {
return html`
<style>
:host {
--ha-label-badge-color: #dac90d;
}
</style>
<ha-label-badge
icon="hass:emoticon"
label="Demo"
description=""
></ha-label-badge>
`;
}
}
customElements.define("ha-demo-badge", HaDemoBadge);

View File

@@ -13,7 +13,7 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
<mwc-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")}
dialogAction="close"
class="header_button"
class="close_button"
>
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
@@ -25,9 +25,6 @@ export class HaDialog extends MwcDialog {
return [
style,
css`
.mdc-dialog {
z-index: var(--dialog-z-index, 7);
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
}
@@ -38,15 +35,10 @@ export class HaDialog extends MwcDialog {
display: block;
height: 20px;
}
.mdc-dialog__content {
padding: var(--dialog-content-padding, 20px 24px);
}
.header_button {
.close_button {
position: absolute;
right: 16px;
top: 12px;
text-decoration: none;
color: inherit;
}
`,
];

View File

@@ -1,33 +0,0 @@
import "@material/mwc-formfield";
import type { Formfield } from "@material/mwc-formfield";
import { style } from "@material/mwc-formfield/mwc-formfield-css";
import { css, CSSResult, customElement } from "lit-element";
import { Constructor } from "../types";
const MwcFormfield = customElements.get("mwc-formfield") as Constructor<
Formfield
>;
@customElement("ha-formfield")
export class HaFormfield extends MwcFormfield {
protected static get styles(): CSSResult[] {
return [
style,
css`
::slotted(ha-switch) {
margin-right: 10px;
}
[dir="rtl"] ::slotted(ha-switch),
::slotted(ha-switch)[dir="rtl"] {
margin-left: 10px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-formfield": HaFormfield;
}
}

View File

@@ -1,37 +1,17 @@
import {
LitElement,
property,
TemplateResult,
html,
customElement,
} from "lit-element";
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-arrow-next")
export class HaIconButtonArrowNext extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property() private _icon = mdiArrowRight;
import { HaIconButton } from "./ha-icon-button";
export class HaIconButtonArrowNext extends HaIconButton {
public connectedCallback() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this._icon =
this.icon =
window.getComputedStyle(this).direction === "ltr"
? mdiArrowRight
: mdiArrowLeft;
? "hass:arrow-right"
: "hass:arrow-left";
}, 100);
}
protected render(): TemplateResult {
return html`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}
}
declare global {
@@ -39,3 +19,5 @@ declare global {
"ha-icon-button-arrow-next": HaIconButtonArrowNext;
}
}
customElements.define("ha-icon-button-arrow-next", HaIconButtonArrowNext);

View File

@@ -1,15 +1,8 @@
import {
LitElement,
property,
TemplateResult,
html,
customElement,
} from "lit-element";
import { LitElement, property, TemplateResult, html } from "lit-element";
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-arrow-prev")
export class HaIconButtonArrowPrev extends LitElement {
@property({ type: Boolean }) public disabled = false;
@@ -39,3 +32,5 @@ declare global {
"ha-icon-button-arrow-prev": HaIconButtonArrowPrev;
}
}
customElements.define("ha-icon-button-arrow-prev", HaIconButtonArrowPrev);

View File

@@ -1,37 +1,17 @@
import {
LitElement,
property,
TemplateResult,
html,
customElement,
} from "lit-element";
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
import "@material/mwc-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-next")
export class HaIconButtonNext extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property() private _icon = mdiChevronRight;
import { HaIconButton } from "./ha-icon-button";
export class HaIconButtonNext extends HaIconButton {
public connectedCallback() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this._icon =
this.icon =
window.getComputedStyle(this).direction === "ltr"
? mdiChevronRight
: mdiChevronLeft;
? "hass:chevron-right"
: "hass:chevron-left";
}, 100);
}
protected render(): TemplateResult {
return html`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}
}
declare global {
@@ -39,3 +19,5 @@ declare global {
"ha-icon-button-next": HaIconButtonNext;
}
}
customElements.define("ha-icon-button-next", HaIconButtonNext);

View File

@@ -1,37 +1,17 @@
import {
LitElement,
property,
TemplateResult,
html,
customElement,
} from "lit-element";
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-prev")
export class HaIconButtonPrev extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property() private _icon = mdiChevronLeft;
import { HaIconButton } from "./ha-icon-button";
export class HaIconButtonPrev extends HaIconButton {
public connectedCallback() {
super.connectedCallback();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
this._icon =
this.icon =
window.getComputedStyle(this).direction === "ltr"
? mdiChevronLeft
: mdiChevronRight;
? "hass:chevron-left"
: "hass:chevron-right";
}, 100);
}
protected render(): TemplateResult {
return html`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}
}
declare global {
@@ -39,3 +19,5 @@ declare global {
"ha-icon-button-prev": HaIconButtonPrev;
}
}
customElements.define("ha-icon-button-prev", HaIconButtonPrev);

View File

@@ -24,21 +24,28 @@ export class HaIconButton extends LitElement {
protected render(): TemplateResult {
return html`
<mwc-icon-button .label=${this.label} .disabled=${this.disabled}>
<mwc-icon-button
.label=${this.label}
?disabled=${this.disabled}
@click=${this._handleClick}
>
<ha-icon .icon=${this.icon}></ha-icon>
</mwc-icon-button>
`;
}
private _handleClick(ev) {
if (this.disabled) {
ev.stopPropagation();
}
}
static get styles(): CSSResult {
return css`
:host {
display: inline-block;
outline: none;
}
:host([disabled]) {
pointer-events: none;
}
mwc-icon-button {
--mdc-theme-on-primary: currentColor;
--mdc-theme-text-disabled-on-light: var(--disabled-text-color);

View File

@@ -13,7 +13,7 @@ class HaMarkdownElement extends UpdatingElement {
protected update(changedProps) {
super.update(changedProps);
if (this.content !== undefined) {
this._render();
this._render();
}
}

View File

@@ -54,7 +54,7 @@ class HaMarkdown extends LitElement {
}
ha-markdown-element code,
pre {
background-color: var(--markdown-code-background-color, none);
background-color: var(--markdown-code-background-color, #f6f8fa);
border-radius: 3px;
}
ha-markdown-element code {

View File

@@ -1,5 +1,3 @@
import "@material/mwc-icon-button";
import { mdiMenu } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@@ -14,7 +12,8 @@ import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { subscribeNotifications } from "../data/persistent_notification";
import { HomeAssistant } from "../types";
import "./ha-svg-icon";
import "./ha-icon-button";
import { mdiMenu } from "@mdi/js";
@customElement("ha-menu-button")
class HaMenuButton extends LitElement {

View File

@@ -1,11 +1,6 @@
import "@material/mwc-icon-button";
import {
mdiBell,
mdiCellphoneSettingsVariant,
mdiMenuOpen,
mdiMenu,
mdiViewDashboard,
} from "@mdi/js";
import { mdiBell, mdiCellphoneSettingsVariant } from "@mdi/js";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "./ha-icon-button";
import "@polymer/paper-item/paper-icon-item";
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
@@ -15,7 +10,6 @@ import {
CSSResult,
eventOptions,
html,
customElement,
LitElement,
property,
PropertyValues,
@@ -35,9 +29,9 @@ import {
getExternalConfig,
} from "../external_app/external_config";
import type { HomeAssistant, PanelInfo } from "../types";
import "./ha-svg-icon";
import "./ha-icon";
import "./ha-menu-button";
import "./ha-svg-icon";
import "./user/ha-user-badge";
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
@@ -109,7 +103,9 @@ const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
return [beforeSpacer, afterSpacer];
};
@customElement("ha-sidebar")
/*
* @appliesMixin LocalizeMixin
*/
class HaSidebar extends LitElement {
@property() public hass!: HomeAssistant;
@@ -157,16 +153,13 @@ class HaSidebar extends LitElement {
<div class="menu">
${!this.narrow
? html`
<mwc-icon-button
.label=${hass.localize("ui.sidebar.sidebar_toggle")}
<ha-icon-button
aria-label=${hass.localize("ui.sidebar.sidebar_toggle")}
.icon=${hass.dockedSidebar === "docked"
? "hass:menu-open"
: "hass:menu"}
@click=${this._toggleSidebar}
>
<ha-svg-icon
.path=${hass.dockedSidebar === "docked"
? mdiMenuOpen
: mdiMenu}
></ha-svg-icon>
</mwc-icon-button>
></ha-icon-button>
`
: ""}
<span class="title">Home Assistant</span>
@@ -181,16 +174,14 @@ class HaSidebar extends LitElement {
>
${this._renderPanel(
defaultPanel.url_path,
defaultPanel.title || hass.localize("panel.states"),
defaultPanel.icon,
!defaultPanel.icon ? mdiViewDashboard : undefined
defaultPanel.icon || "hass:view-dashboard",
defaultPanel.title || hass.localize("panel.states")
)}
${beforeSpacer.map((panel) =>
this._renderPanel(
panel.url_path,
hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon,
undefined
hass.localize(`panel.${panel.title}`) || panel.title
)
)}
<div class="spacer" disabled></div>
@@ -198,9 +189,8 @@ class HaSidebar extends LitElement {
${afterSpacer.map((panel) =>
this._renderPanel(
panel.url_path,
hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon,
undefined
hass.localize(`panel.${panel.title}`) || panel.title
)
)}
${this._externalConfig && this._externalConfig.hasSettingsScreen
@@ -453,12 +443,7 @@ class HaSidebar extends LitElement {
fireEvent(this, "hass-toggle-menu");
}
private _renderPanel(
urlPath: string,
title: string | null,
icon?: string | null,
iconPath?: string | null
) {
private _renderPanel(urlPath, icon, title) {
return html`
<a
aria-role="option"
@@ -469,12 +454,7 @@ class HaSidebar extends LitElement {
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<ha-icon slot="item-icon" .icon="${icon}"></ha-icon>
<span class="item-text">${title}</span>
</paper-icon-item>
</a>
@@ -516,13 +496,13 @@ class HaSidebar extends LitElement {
width: 256px;
}
.menu mwc-icon-button {
.menu ha-icon-button {
color: var(--sidebar-icon-color);
}
:host([expanded]) .menu mwc-icon-button {
:host([expanded]) .menu ha-icon-button {
margin-right: 23px;
}
:host([expanded][_rtl]) .menu mwc-icon-button {
:host([expanded][_rtl]) .menu ha-icon-button {
margin-right: 0px;
margin-left: 23px;
}
@@ -734,7 +714,7 @@ class HaSidebar extends LitElement {
font-weight: 500;
}
:host([_rtl]) .menu mwc-icon-button {
:host([_rtl]) .menu ha-icon-button {
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
@@ -747,3 +727,5 @@ declare global {
"ha-sidebar": HaSidebar;
}
}
customElements.define("ha-sidebar", HaSidebar);

View File

@@ -1,7 +1,15 @@
import { ripple } from "@material/mwc-ripple/ripple-directive";
import "@material/mwc-switch";
import type { Switch } from "@material/mwc-switch";
import { style } from "@material/mwc-switch/mwc-switch-css";
import { css, CSSResult, customElement, property } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
property,
query,
} from "lit-element";
import { forwardHaptic } from "../data/haptics";
import { Constructor } from "../types";
@@ -14,12 +22,18 @@ export class HaSwitch extends MwcSwitch {
// Do not add haptic when a user is required to press save.
@property({ type: Boolean }) public haptic = false;
@query("slot") private _slot!: HTMLSlotElement;
protected firstUpdated() {
super.firstUpdated();
this.style.setProperty(
"--mdc-theme-secondary",
"var(--switch-checked-color)"
);
this.classList.toggle(
"slotted",
Boolean(this._slot.assignedNodes().length)
);
this.addEventListener("change", () => {
if (this.haptic) {
forwardHaptic("light");
@@ -27,10 +41,40 @@ export class HaSwitch extends MwcSwitch {
});
}
protected render() {
return html`
<div class="mdc-switch">
<div class="mdc-switch__track"></div>
<div
class="mdc-switch__thumb-underlay"
.ripple="${ripple({
interactionNode: this,
})}"
>
<div class="mdc-switch__thumb">
<input
type="checkbox"
id="basic-switch"
class="mdc-switch__native-control"
role="switch"
@change="${this._haChangeHandler}"
/>
</div>
</div>
</div>
<label for="basic-switch"><slot></slot></label>
`;
}
protected static get styles(): CSSResult[] {
return [
style,
css`
:host {
display: flex;
flex-direction: row;
align-items: center;
}
.mdc-switch.mdc-switch--checked .mdc-switch__thumb {
background-color: var(--switch-checked-button-color);
border-color: var(--switch-checked-button-color);
@@ -47,9 +91,18 @@ export class HaSwitch extends MwcSwitch {
background-color: var(--switch-unchecked-track-color);
border-color: var(--switch-unchecked-track-color);
}
:host(.slotted) .mdc-switch {
margin-right: 24px;
}
`,
];
}
private _haChangeHandler(e: Event) {
this.mdcFoundation.handleChange(e);
// catch "click" event and sync properties
this.checked = this.formElement.checked;
}
}
declare global {

View File

@@ -0,0 +1,46 @@
/*
Wrapper for paper-textarea.
paper-textarea crashes on iOS when created programmatically. This only impacts
our automation and script editors as they are using Preact. Polymer is using
template elements and does not have this issue.
paper-textarea issue: https://github.com/PolymerElements/paper-input/issues/556
WebKit issue: https://bugs.webkit.org/show_bug.cgi?id=174629
*/
import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
class HaTextarea extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
</style>
<paper-textarea
label="[[label]]"
placeholder="[[placeholder]]"
value="{{value}}"
></paper-textarea>
`;
}
static get properties() {
return {
name: String,
label: String,
placeholder: String,
value: {
type: String,
notify: true,
},
};
}
}
customElements.define("ha-textarea", HaTextarea);

View File

@@ -262,28 +262,6 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
pushData(new Date(state.last_changed), series);
}
});
} else if (domain === "humidifier") {
addColumn(
`${this.hass.localize(
"ui.card.humidifier.target_humidity_entity",
"name",
name
)}`,
true
);
addColumn(
`${this.hass.localize("ui.card.humidifier.on_entity", "name", name)}`,
true,
true
);
states.states.forEach((state) => {
if (!state.attributes) return;
const target = safeParseFloat(state.attributes.humidity);
const series = [target];
series.push(state.state === "on" ? target : null);
pushData(new Date(state.last_changed), series);
});
} else {
// Only disable interpolation for sensors
const isStep = domain === "sensor";

View File

@@ -1,4 +1,4 @@
import "./ha-circular-progress";
import "@polymer/paper-spinner/paper-spinner";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";

View File

@@ -1,11 +1,5 @@
import { HomeAssistant } from "../types";
export interface AuthUrlSearchParams {
client_id?: string;
redirect_uri?: string;
state?: string;
}
export interface AuthProvider {
name: string;
id: string;

View File

@@ -5,7 +5,7 @@ import { HomeAssistant } from "../types";
import { DataEntryFlowProgress, DataEntryFlowStep } from "./data_entry_flow";
import { domainToName } from "./integration";
export const DISCOVERY_SOURCES = ["unignore", "homekit", "ssdp", "zeroconf", "discovery"];
export const DISCOVERY_SOURCES = ["unignore", "homekit", "ssdp", "zeroconf"];
export const createConfigFlow = (hass: HomeAssistant, handler: string) =>
hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", {

View File

@@ -12,11 +12,6 @@ export interface ConfigUpdateValues {
internal_url?: string | null;
}
export interface CheckConfigResult {
result: "valid" | "invalid";
errors: string | null;
}
export const saveCoreConfig = (
hass: HomeAssistant,
values: Partial<ConfigUpdateValues>
@@ -30,6 +25,3 @@ export const detectCoreConfig = (hass: HomeAssistant) =>
hass.callWS<Partial<ConfigUpdateValues>>({
type: "config/core/detect",
});
export const checkCoreConfig = (hass: HomeAssistant) =>
hass.callApi<CheckConfigResult>("POST", "config/core/check_config");

View File

@@ -4,20 +4,6 @@ import { hassioApiResultExtractor, HassioResponse } from "./common";
export type HassioHomeAssistantInfo = any;
export type HassioSupervisorInfo = any;
export type HassioInfo = {
arch: string;
channel: string;
docker: string;
hassos?: string;
homeassistant: string;
hostname: string;
logging: string;
maching: string;
supervisor: string;
supported_arch: string[];
timezone: string;
};
export type HassioPanelInfo = PanelInfo<
| undefined
| {
@@ -52,12 +38,6 @@ export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => {
);
};
export const fetchHassioInfo = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioInfo>>("GET", "hassio/info")
);
};
export const fetchHassioLogs = async (
hass: HomeAssistant,
provider: string

View File

@@ -5,15 +5,13 @@ import { computeStateName } from "../common/entity/compute_state_name";
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
const DOMAINS_USE_LAST_UPDATED = ["climate", "water_heater"];
const LINE_ATTRIBUTES_TO_KEEP = [
"temperature",
"current_temperature",
"target_temp_low",
"target_temp_high",
"hvac_action",
"humidity",
"mode",
];
export interface LineChartState {
@@ -226,8 +224,6 @@ export const computeHistory = (
unit = hass.config.unit_system.temperature;
} else if (computeStateDomain(stateInfo[0]) === "water_heater") {
unit = hass.config.unit_system.temperature;
} else if (computeStateDomain(stateInfo[0]) === "humidifier") {
unit = "%";
}
if (!unit) {

View File

@@ -1,19 +0,0 @@
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
export type HumidifierEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
humidity?: number;
min_humidity?: number;
max_humidity?: number;
mode?: string;
available_modes?: string[];
};
};
export const HUMIDIFIER_SUPPORT_MODES = 1;
export const HUMIDIFIER_DEVICE_CLASS_HUMIDIFIER = "humidifier";
export const HUMIDIFIER_DEVICE_CLASS_DEHUMIDIFIER = "dehumidifier";

View File

@@ -1,67 +1,7 @@
import { HomeAssistant } from "../types";
export interface LogbookEntry {
when: string;
name: string;
message: string;
entity_id?: string;
domain: string;
context_user_id?: string;
}
const DATA_CACHE: {
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
} = {};
export const getLogbookData = (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string
) => {
const ALL_ENTITIES = "*";
if (!entityId) {
entityId = ALL_ENTITIES;
}
const cacheKey = `${startDate}${endDate}`;
if (!DATA_CACHE[cacheKey]) {
DATA_CACHE[cacheKey] = {};
}
if (DATA_CACHE[cacheKey][entityId]) {
return DATA_CACHE[cacheKey][entityId];
}
if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) {
return DATA_CACHE[cacheKey][ALL_ENTITIES].then((entities) =>
entities.filter((entity) => entity.entity_id === entityId)
);
}
DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer(
hass,
startDate,
endDate,
entityId !== ALL_ENTITIES ? entityId : undefined
).then((entries) => entries.reverse());
return DATA_CACHE[cacheKey][entityId];
};
const getLogbookDataFromServer = async (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string
) => {
const url = `logbook/${startDate}?end_time=${endDate}${
entityId ? `&entity=${entityId}` : ""
}`;
return hass.callApi<LogbookEntry[]>("GET", url);
};
export const clearLogbookCache = (startDate, endDate) => {
DATA_CACHE[`${startDate}${endDate}`] = {};
};

View File

@@ -51,7 +51,7 @@ export const onboardCoreConfigStep = (hass: HomeAssistant) =>
export const onboardIntegrationStep = (
hass: HomeAssistant,
params: { client_id: string; redirect_uri: string }
params: { client_id: string }
) =>
hass.callApi<OnboardingIntegrationStepResponse>(
"POST",

View File

@@ -10,9 +10,7 @@ import {
TemplateResult,
} from "lit-element";
import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-circular-progress";
import "../../components/ha-switch";
import "../../components/ha-formfield";
import type { HaSwitch } from "../../components/ha-switch";
import {
getConfigEntrySystemOptions,
@@ -76,7 +74,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
${this._loading
? html`
<div class="init-spinner">
<ha-circular-progress active></ha-circular-progress>
<paper-spinner-lite active></paper-spinner-lite>
</div>
`
: html`
@@ -84,8 +82,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
? html` <div class="error">${this._error}</div> `
: ""}
<div class="form">
<ha-formfield
.label=${html`<p>
<ha-switch
.checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting}
>
<div>
<p>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_label"
)}
@@ -98,15 +101,9 @@ class DialogConfigEntrySystemOptions extends LitElement {
`component.${this._params.entry.domain}.title`
) || this._params.entry.domain
)}
</p>`}
>
<ha-switch
.checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting}
>
</ha-switch>
</ha-formfield>
</p>
</div>
</ha-switch>
</div>
`}
</paper-dialog-scrollable>
@@ -175,6 +172,9 @@ class DialogConfigEntrySystemOptions extends LitElement {
padding-bottom: 24px;
color: var(--primary-text-color);
}
p {
margin: 0;
}
.secondary {
color: var(--secondary-text-color);
}

View File

@@ -1,7 +1,7 @@
import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../../components/ha-icon-button";
import "../../components/ha-circular-progress";
import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-tooltip/paper-tooltip";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
@@ -14,7 +14,8 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import "../../components/ha-dialog";
import "../../components/dialog/ha-paper-dialog";
import type { HaPaperDialog } from "../../components/dialog/ha-paper-dialog";
import "../../components/ha-form/ha-form";
import "../../components/ha-markdown";
import {
@@ -26,6 +27,7 @@ import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
@@ -88,6 +90,7 @@ class DataEntryFlowDialog extends LitElement {
// We only load the handlers once
if (this._handlers === undefined) {
this._loading = true;
this.updateComplete.then(() => this._scheduleCenterDialog());
try {
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
} finally {
@@ -95,6 +98,7 @@ class DataEntryFlowDialog extends LitElement {
}
}
await this.updateComplete;
this._scheduleCenterDialog();
return;
}
@@ -111,6 +115,9 @@ class DataEntryFlowDialog extends LitElement {
this._processStep(step);
this._loading = false;
// When the flow changes, center the dialog.
// Don't do it on each step or else the dialog keeps bouncing.
this._scheduleCenterDialog();
}
protected render(): TemplateResult {
@@ -119,84 +126,80 @@ class DataEntryFlowDialog extends LitElement {
}
return html`
<ha-dialog
open
@closing=${this._close}
scrimClickAction
escapeKeyAction
hideActions
<ha-paper-dialog
with-backdrop
opened
modal
@opened-changed=${this._openedChanged}
>
<div>
${this._loading ||
(this._step === null && this._handlers === undefined)
? html`
<step-flow-loading
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.loading_first_time"
)}
></step-flow-loading>
`
: this._step === undefined
? // When we are going to next step, we render 1 round of empty
// to reset the element.
""
: html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.dismiss"
)}
icon="hass:close"
dialogAction="close"
></ha-icon-button>
${this._step === null
? // Show handler picker
html`
<step-flow-pick-handler
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
></step-flow-pick-handler>
`
: this._step.type === "form"
? html`
<step-flow-form
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-form>
`
: this._step.type === "external"
? html`
<step-flow-external
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-external>
`
: this._step.type === "abort"
? html`
<step-flow-abort
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-abort>
`
: this._devices === undefined || this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry
html` <step-flow-loading></step-flow-loading> `
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.devices=${this._devices}
.areas=${this._areas}
></step-flow-create-entry>
`}
`}
</div>
</ha-dialog>
${this._loading || (this._step === null && this._handlers === undefined)
? html`
<step-flow-loading
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.loading_first_time"
)}
></step-flow-loading>
`
: this._step === undefined
? // When we are going to next step, we render 1 round of empty
// to reset the element.
""
: html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.dismiss"
)}
icon="hass:close"
dialog-dismiss
></ha-icon-button>
${this._step === null
? // Show handler picker
html`
<step-flow-pick-handler
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
></step-flow-pick-handler>
`
: this._step.type === "form"
? html`
<step-flow-form
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-form>
`
: this._step.type === "external"
? html`
<step-flow-external
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-external>
`
: this._step.type === "abort"
? html`
<step-flow-abort
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-abort>
`
: this._devices === undefined || this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry
html` <step-flow-loading></step-flow-loading> `
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.devices=${this._devices}
.areas=${this._areas}
></step-flow-create-entry>
`}
`}
</ha-paper-dialog>
`;
}
@@ -222,6 +225,18 @@ class DataEntryFlowDialog extends LitElement {
this._areas = [];
}
}
if (changedProps.has("_devices") && this._dialog) {
this._scheduleCenterDialog();
}
}
private _scheduleCenterDialog() {
setTimeout(() => this._dialog.center(), 0);
}
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
private async _fetchDevices(configEntryId) {
@@ -295,13 +310,16 @@ class DataEntryFlowDialog extends LitElement {
}
}
private _close(): void {
if (this._step) {
this._flowDone();
} else if (this._step === null) {
// Flow aborted during picking flow
this._step = undefined;
this._params = undefined;
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
// Closed dialog by clicking on the overlay
if (!ev.detail.value) {
if (this._step) {
this._flowDone();
} else if (this._step === null) {
// Flow aborted during picking flow
this._step = undefined;
this._params = undefined;
}
}
}
@@ -309,14 +327,18 @@ class DataEntryFlowDialog extends LitElement {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
ha-paper-dialog {
max-width: 600px;
}
ha-paper-dialog > * {
margin: 0;
display: block;
padding: 0;
}
ha-icon-button {
padding: 16px;
position: absolute;
top: 0;
right: 0;
display: inline-block;
padding: 8px;
float: right;
}
`,
];

View File

@@ -1,5 +1,5 @@
import "@material/mwc-button";
import "../../components/ha-circular-progress";
import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
@@ -76,7 +76,7 @@ class StepFlowForm extends LitElement {
${this._loading
? html`
<div class="submit-spinner">
<ha-circular-progress active></ha-circular-progress>
<paper-spinner active></paper-spinner>
</div>
`
: html`

View File

@@ -1,3 +1,4 @@
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
@@ -7,7 +8,6 @@ import {
property,
TemplateResult,
} from "lit-element";
import "../../components/ha-circular-progress";
@customElement("step-flow-loading")
class StepFlowLoading extends LitElement {
@@ -17,7 +17,7 @@ class StepFlowLoading extends LitElement {
return html`
<div class="init-spinner">
${this.label ? html` <div>${this.label}</div> ` : ""}
<ha-circular-progress active></ha-circular-progress>
<paper-spinner-lite active></paper-spinner-lite>
</div>
`;
}
@@ -28,7 +28,7 @@ class StepFlowLoading extends LitElement {
padding: 50px 100px;
text-align: center;
}
ha-circular-progress {
paper-spinner-lite {
margin-top: 16px;
}
`;

View File

@@ -1,5 +1,6 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-spinner/paper-spinner-lite";
import Fuse from "fuse.js";
import {
css,
@@ -41,8 +42,6 @@ class StepFlowPickHandler extends LitElement {
private _width?: number;
private _height?: number;
private _getHandlers = memoizeOne(
(h: string[], filter?: string, _localize?: LocalizeFunc) => {
const handlers: HandlerObj[] = h.map((handler) => {
@@ -83,10 +82,7 @@ class StepFlowPickHandler extends LitElement {
@value-changed=${this._filterChanged}
></search-input>
<div
style=${styleMap({
width: `${this._width}px`,
height: `${this._height}px`,
})}
style=${styleMap({ width: `${this._width}px` })}
class=${classMap({ advanced: Boolean(this.showAdvanced) })}
>
${handlers.map(
@@ -143,20 +139,13 @@ class StepFlowPickHandler extends LitElement {
protected updated(changedProps) {
super.updated(changedProps);
// Store the width and height so that when we search, box doesn't jump
const div = this.shadowRoot!.querySelector("div")!;
// Store the width so that when we search, box doesn't jump
if (!this._width) {
const width = div.clientWidth;
const width = this.shadowRoot!.querySelector("div")!.clientWidth;
if (width) {
this._width = width;
}
}
if (!this._height) {
const height = div.clientHeight;
if (height) {
this._height = height;
}
}
}
private async _filterChanged(e) {
@@ -177,8 +166,8 @@ class StepFlowPickHandler extends LitElement {
configFlowContentStyles,
css`
img {
width: 40px;
height: 40px;
max-width: 40px;
max-height: 40px;
}
search-input {
display: block;
@@ -191,12 +180,12 @@ class StepFlowPickHandler extends LitElement {
overflow: auto;
max-height: 600px;
}
@media all and (max-height: 900px) {
@media all and (max-height: 1px) {
div {
max-height: calc(100vh - 134px);
max-height: calc(100vh - 205px);
}
div.advanced {
max-height: calc(100vh - 250px);
max-height: calc(100vh - 300px);
}
}
paper-icon-item {

View File

@@ -2,21 +2,8 @@ import { css } from "lit-element";
export const configFlowContentStyles = css`
h2 {
margin: 24px 0 0;
margin-top: 24px;
padding: 0 24px;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-family: var(
--mdc-typography-headline6-font-family,
var(--mdc-typography-font-family, Roboto, sans-serif)
);
font-size: var(--mdc-typography-headline6-font-size, 1.25rem);
line-height: var(--mdc-typography-headline6-line-height, 2rem);
font-weight: var(--mdc-typography-headline6-font-weight, 500);
letter-spacing: var(--mdc-typography-headline6-letter-spacing, 0.0125em);
text-decoration: var(--mdc-typography-headline6-text-decoration, inherit);
text-transform: var(--mdc-typography-headline6-text-transform, inherit);
box-sizing: border-box;
}
.content {

View File

@@ -66,7 +66,7 @@ class DialogDeviceRegistryDetail extends LitElement {
<paper-input
.value=${this._nameByUser}
@value-changed=${this._nameChanged}
.label=${this.hass.localize("ui.panel.config.devices.name")}
.label=${this.hass.localize("ui.dialogs.devices.name")}
.placeholder=${device.name || ""}
.disabled=${this._submitting}
></paper-input>

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import {
css,
@@ -10,7 +11,7 @@ import {
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "../../components/ha-dialog";
import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-switch";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
@@ -40,17 +41,21 @@ class DialogBox extends LitElement {
const confirmPrompt = this._params.confirmation || this._params.prompt;
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
@close=${this._close}
.heading=${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize("ui.dialogs.generic.default_confirmation_title")}
<ha-paper-dialog
with-backdrop
opened
modal
@opened-changed="${this._openedChanged}"
>
<div>
<h2>
${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize(
"ui.dialogs.generic.default_confirmation_title"
)}
</h2>
<paper-dialog-scrollable>
${this._params.text
? html`
<p
@@ -78,21 +83,23 @@ class DialogBox extends LitElement {
></paper-input>
`
: ""}
</div>
${confirmPrompt &&
html`
<mwc-button @click=${this._dismiss} slot="secondaryAction">
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${confirmPrompt &&
html`
<mwc-button @click="${this._dismiss}">
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button>
`}
<mwc-button @click="${this._confirm}">
${this._params.confirmText
? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")}
</mwc-button>
`}
<mwc-button @click=${this._confirm} slot="primaryAction">
${this._params.confirmText
? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")}
</mwc-button>
</ha-dialog>
</div>
</ha-paper-dialog>
`;
}
@@ -120,8 +127,10 @@ class DialogBox extends LitElement {
this._dismiss();
}
private _close(): void {
this._params = undefined;
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
static get styles(): CSSResult[] {
@@ -132,6 +141,15 @@ class DialogBox extends LitElement {
pointer-events: initial !important;
cursor: initial !important;
}
ha-paper-dialog {
min-width: 400px;
max-width: 500px;
}
@media (max-width: 400px) {
ha-paper-dialog {
min-width: initial;
}
}
a {
color: var(--primary-color);
}
@@ -147,10 +165,6 @@ class DialogBox extends LitElement {
.secondary {
color: var(--secondary-text-color);
}
ha-dialog {
/* Place above other dialogs */
--dialog-z-index: 104;
}
`,
];
}

View File

@@ -2,7 +2,7 @@ import "@material/mwc-button";
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/iron-input/iron-input";
import "@polymer/paper-input/paper-input";
import "../../../components/ha-circular-progress";
import "@polymer/paper-spinner/paper-spinner";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -38,7 +38,7 @@ class MoreInfoConfigurator extends PolymerElement {
height: 41px;
}
ha-circular-progress {
paper-spinner {
width: 14px;
height: 14px;
margin-right: 20px;
@@ -75,11 +75,11 @@ class MoreInfoConfigurator extends PolymerElement {
disabled="[[isConfiguring]]"
on-click="submitClicked"
>
<ha-circular-progress
<paper-spinner
active="[[isConfiguring]]"
hidden="[[!isConfiguring]]"
alt="Configuring"
></ha-circular-progress>
></paper-spinner>
[[stateObj.attributes.submit_caption]]
</mwc-button>
</p>

View File

@@ -14,7 +14,6 @@ import "./more-info-default";
import "./more-info-fan";
import "./more-info-group";
import "./more-info-history_graph";
import "./more-info-humidifier";
import "./more-info-input_datetime";
import "./more-info-light";
import "./more-info-lock";

View File

@@ -1,218 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-paper-slider";
import "../../../components/ha-switch";
import {
HumidifierEntity,
HUMIDIFIER_SUPPORT_MODES,
} from "../../../data/humidifier";
import { HomeAssistant } from "../../../types";
class MoreInfoHumidifier extends LitElement {
@property() public hass!: HomeAssistant;
@property() public stateObj?: HumidifierEntity;
private _resizeDebounce?: number;
protected render(): TemplateResult {
if (!this.stateObj) {
return html``;
}
const hass = this.hass;
const stateObj = this.stateObj;
const supportModes = supportsFeature(stateObj, HUMIDIFIER_SUPPORT_MODES);
const rtlDirection = computeRTLDirection(hass);
return html`
<div
class=${classMap({
"has-modes": supportModes,
})}
>
<div class="container-humidity">
<div>${hass.localize("ui.card.humidifier.humidity")}</div>
<div class="single-row">
<div class="target-humidity">
${stateObj.attributes.humidity} %
</div>
<ha-paper-slider
class="humidity"
step="1"
pin
ignore-bar-touch
dir=${rtlDirection}
.min=${stateObj.attributes.min_humidity}
.max=${stateObj.attributes.max_humidity}
.secondaryProgress=${stateObj.attributes.max_humidity}
.value=${stateObj.attributes.humidity}
@change=${this._targetHumiditySliderChanged}
>
</ha-paper-slider>
</div>
</div>
${supportModes
? html`
<div class="container-modes">
<ha-paper-dropdown-menu
label-float
dynamic-align
.label=${hass.localize("ui.card.humidifier.mode")}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
.selected=${stateObj.attributes.mode}
@selected-changed=${this._handleModeChanged}
>
${stateObj.attributes.available_modes!.map(
(mode) => html`
<paper-item item-name=${mode}>
${hass.localize(
`state_attributes.humidifier.mode.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
</div>
`
: ""}
</div>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!changedProps.has("stateObj") || !this.stateObj) {
return;
}
if (this._resizeDebounce) {
clearTimeout(this._resizeDebounce);
}
this._resizeDebounce = window.setTimeout(() => {
fireEvent(this, "iron-resize");
this._resizeDebounce = undefined;
}, 500);
}
private _targetHumiditySliderChanged(ev) {
const newVal = ev.target.value;
this._callServiceHelper(
this.stateObj!.attributes.humidity,
newVal,
"set_humidity",
{ humidity: newVal }
);
}
private _handleModeChanged(ev) {
const newVal = ev.detail.value || null;
this._callServiceHelper(
this.stateObj!.attributes.mode,
newVal,
"set_mode",
{ mode: newVal }
);
}
private async _callServiceHelper(
oldVal: unknown,
newVal: unknown,
service: string,
data: {
entity_id?: string;
[key: string]: unknown;
}
) {
if (oldVal === newVal) {
return;
}
data.entity_id = this.stateObj!.entity_id;
const curState = this.stateObj;
await this.hass.callService("humidifier", service, data);
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
// on. Since the state is not changing, the resync is not called automatic.
await new Promise((resolve) => setTimeout(resolve, 2000));
// No need to resync if we received a new state.
if (this.stateObj !== curState) {
return;
}
this.stateObj = undefined;
await this.updateComplete;
// Only restore if not set yet by a state change
if (this.stateObj === undefined) {
this.stateObj = curState;
}
}
static get styles(): CSSResult {
return css`
:host {
color: var(--primary-text-color);
}
ha-paper-dropdown-menu {
width: 100%;
}
paper-item {
cursor: pointer;
}
ha-paper-slider {
width: 100%;
}
.container-humidity .single-row {
display: flex;
height: 50px;
}
.target-humidity {
width: 90px;
font-size: 200%;
margin: auto;
direction: ltr;
}
.humidity {
--paper-slider-active-color: var(--paper-blue-400);
--paper-slider-secondary-color: var(--paper-blue-400);
}
.single-row {
padding: 8px 0;
}
`;
}
}
customElements.define("more-info-humidifier", MoreInfoHumidifier);

View File

@@ -86,7 +86,7 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class="main-title" main-title="" on-click="enlarge">
[[_computeStateName(stateObj)]]
</div>
<template is="dom-if" if="[[hass.user.is_admin]]">
<template is="dom-if" if="[[_computeConfig(hass)]]">
<ha-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.settings')]]"
icon="hass:settings"
@@ -219,6 +219,10 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
return stateObj ? computeStateName(stateObj) : "";
}
_computeConfig(hass) {
return hass.user.is_admin && isComponentLoaded(hass, "config");
}
_computeEdit(hass, stateObj) {
const domain = this._computeDomain(stateObj);
return (

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