mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-11 10:19:25 +00:00
Compare commits
144 Commits
template-e
...
20201212.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5409752817 | ||
![]() |
a70e6c49a1 | ||
![]() |
3d83d5f4b5 | ||
![]() |
f9dece0743 | ||
![]() |
ac0871d0e8 | ||
![]() |
ffc19e591d | ||
![]() |
c53380ca3d | ||
![]() |
7c74a2026a | ||
![]() |
adaed438d9 | ||
![]() |
baf38305cb | ||
![]() |
8254712521 | ||
![]() |
53214781e3 | ||
![]() |
88cbbbdf65 | ||
![]() |
c10dca9c7b | ||
![]() |
7f2ebb4bde | ||
![]() |
f1abb60e4a | ||
![]() |
e014c7aff6 | ||
![]() |
b79c03433e | ||
![]() |
34eb4d974d | ||
![]() |
3264be3c5e | ||
![]() |
655f4f75fb | ||
![]() |
4383f31696 | ||
![]() |
99eb15d15e | ||
![]() |
2682c6e150 | ||
![]() |
3a5d854e6d | ||
![]() |
1e90c6387c | ||
![]() |
2cca25f4d0 | ||
![]() |
565724d201 | ||
![]() |
3e4955becd | ||
![]() |
7b560c727f | ||
![]() |
35abd9dfdb | ||
![]() |
b7ccf3e0e5 | ||
![]() |
0d9ab8fdd0 | ||
![]() |
303f9290a8 | ||
![]() |
e0c4dc08a1 | ||
![]() |
8c655883fe | ||
![]() |
ba90785115 | ||
![]() |
7b392b626b | ||
![]() |
8e4ceb7d48 | ||
![]() |
2ab1c6e9a9 | ||
![]() |
dbdced0971 | ||
![]() |
5e481880bd | ||
![]() |
faec063f34 | ||
![]() |
bbea38d227 | ||
![]() |
a0ef60de49 | ||
![]() |
3313572606 | ||
![]() |
c4f850cb14 | ||
![]() |
3bdab738c6 | ||
![]() |
faaef31b9f | ||
![]() |
ca7b8b8b4c | ||
![]() |
9ca84e0694 | ||
![]() |
daaf2b1796 | ||
![]() |
25f7cbea5a | ||
![]() |
c485ea9d7b | ||
![]() |
295390c8e9 | ||
![]() |
3ebf816ce2 | ||
![]() |
0e362b851b | ||
![]() |
8d7ba19a08 | ||
![]() |
08f4aa9d10 | ||
![]() |
98175d5c72 | ||
![]() |
7d4cad90bc | ||
![]() |
335354d962 | ||
![]() |
fe31d15d27 | ||
![]() |
7ceb6eb50d | ||
![]() |
4c4db46aa8 | ||
![]() |
b5724ed343 | ||
![]() |
cae94175fe | ||
![]() |
0494a9d410 | ||
![]() |
c261b5c1ce | ||
![]() |
c89e17ac00 | ||
![]() |
50e7410002 | ||
![]() |
c5b0ebf76d | ||
![]() |
1d08978d6c | ||
![]() |
fc78b6c933 | ||
![]() |
480a5718fc | ||
![]() |
f093bd115c | ||
![]() |
8a86beff14 | ||
![]() |
6020890384 | ||
![]() |
124aa947e2 | ||
![]() |
e1add14453 | ||
![]() |
e3293837a8 | ||
![]() |
5cb2743780 | ||
![]() |
6f0c79ec25 | ||
![]() |
7de7d1d926 | ||
![]() |
89175f8e85 | ||
![]() |
fc48c59eb0 | ||
![]() |
51332bc7e7 | ||
![]() |
7403405d12 | ||
![]() |
1d13947e71 | ||
![]() |
f6cb1ffe20 | ||
![]() |
6d92b5651a | ||
![]() |
3ea5bb2a6c | ||
![]() |
1d367eca69 | ||
![]() |
d4bf3a2ec3 | ||
![]() |
0ef8881660 | ||
![]() |
d7d1121f7d | ||
![]() |
7f089c309f | ||
![]() |
4dcc0bb66c | ||
![]() |
0049be7feb | ||
![]() |
39ff641be9 | ||
![]() |
e2fed24995 | ||
![]() |
c0aa353f83 | ||
![]() |
d8521be63d | ||
![]() |
6d4569c89d | ||
![]() |
cd07553b59 | ||
![]() |
641bfcc9f7 | ||
![]() |
6c01371958 | ||
![]() |
7b00260b1a | ||
![]() |
875142373e | ||
![]() |
ba505b15ef | ||
![]() |
17d227b142 | ||
![]() |
e7e192ffe3 | ||
![]() |
c53ec6e12d | ||
![]() |
aad6492a6a | ||
![]() |
fd5b125c2d | ||
![]() |
5acee76c70 | ||
![]() |
10916fa82a | ||
![]() |
f69951a523 | ||
![]() |
38ba85e89d | ||
![]() |
97023921b8 | ||
![]() |
f835810f0a | ||
![]() |
46f5589530 | ||
![]() |
ff9840c8ef | ||
![]() |
0c197558a1 | ||
![]() |
c409ba149d | ||
![]() |
0b896ddfb1 | ||
![]() |
45721eb4fe | ||
![]() |
1289bd03b2 | ||
![]() |
c1ba8ba6b8 | ||
![]() |
4973d8f629 | ||
![]() |
3aff4c96c4 | ||
![]() |
4005bc8985 | ||
![]() |
62e9792c39 | ||
![]() |
33183cc595 | ||
![]() |
394d552856 | ||
![]() |
aa4f0929e0 | ||
![]() |
f99b9215e3 | ||
![]() |
c51d621fee | ||
![]() |
7499892bc2 | ||
![]() |
3fb35871c7 | ||
![]() |
d6d20cd704 | ||
![]() |
9cc6a6b885 | ||
![]() |
ee0be7b6d0 | ||
![]() |
7e6153ba7d |
@@ -1,6 +0,0 @@
|
||||
jshint:
|
||||
enabled: false
|
||||
|
||||
eslint:
|
||||
enabled: true
|
||||
config_file: .eslintrc-hound.json
|
39
build-scripts/README.md
Normal file
39
build-scripts/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Bundling Home Assistant Frontend
|
||||
|
||||
The Home Assistant build pipeline contains various steps to prepare a build.
|
||||
|
||||
- Generating icon files to be included
|
||||
- Generating translation files to be included
|
||||
- Converting TypeScript, CSS and JSON files to JavaScript
|
||||
- Bundling
|
||||
- Minifying the files
|
||||
- Generating the HTML entrypoint files
|
||||
- Generating the service worker
|
||||
- Compressing the files
|
||||
|
||||
## Converting files
|
||||
|
||||
Currently in Home Assistant we use a bundler to convert TypeScript, CSS and JSON files to JavaScript files that the browser understands.
|
||||
|
||||
We currently rely on Webpack but also have experimental Rollup support. Both of these programs bundle the converted files in both production and development.
|
||||
|
||||
For development, bundling is optional. We just want to get the right files in the browser.
|
||||
|
||||
Responsibilities of the converter during development:
|
||||
|
||||
- Convert TypeScript to JavaScript
|
||||
- Convert CSS to JavaScript that sets the content as the default export
|
||||
- Convert JSON to JavaScript that sets the content as the default export
|
||||
- Make sure import, dynamic import and web worker references work
|
||||
- Add extensions where missing
|
||||
- Resolve absolute package imports
|
||||
- Filter out specific imports/packages
|
||||
- Replace constants with values
|
||||
|
||||
In production, the following responsibilities are added:
|
||||
|
||||
- Minify HTML
|
||||
- Bundle multiple imports so that the browser can fetch less files
|
||||
- Generate a second version that is ES5 compatible
|
||||
|
||||
Configuration for all these steps are specified in [bundle.js](bundle.js).
|
@@ -44,7 +44,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
});
|
||||
|
||||
module.exports.terserOptions = (latestBuild) => ({
|
||||
safari10: true,
|
||||
safari10: !latestBuild,
|
||||
ecma: latestBuild ? undefined : 5,
|
||||
output: { comments: false },
|
||||
});
|
||||
@@ -117,7 +117,7 @@ BundleConfig {
|
||||
*/
|
||||
|
||||
module.exports.config = {
|
||||
app({ isProdBuild, latestBuild, isStatsBuild }) {
|
||||
app({ isProdBuild, latestBuild, isStatsBuild, isWDS }) {
|
||||
return {
|
||||
entry: {
|
||||
service_worker: "./src/entrypoints/service_worker.ts",
|
||||
@@ -132,6 +132,7 @@ module.exports.config = {
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isWDS,
|
||||
};
|
||||
},
|
||||
|
||||
|
@@ -6,6 +6,9 @@ module.exports = {
|
||||
useRollup() {
|
||||
return process.env.ROLLUP === "1";
|
||||
},
|
||||
useWDS() {
|
||||
return process.env.WDS === "1";
|
||||
},
|
||||
isProdBuild() {
|
||||
return (
|
||||
process.env.NODE_ENV === "production" || module.exports.isStatsBuild()
|
||||
|
@@ -12,6 +12,7 @@ require("./webpack.js");
|
||||
require("./service-worker.js");
|
||||
require("./entry-html.js");
|
||||
require("./rollup.js");
|
||||
require("./wds.js");
|
||||
|
||||
gulp.task(
|
||||
"develop-app",
|
||||
@@ -28,7 +29,11 @@ gulp.task(
|
||||
"build-translations"
|
||||
),
|
||||
"copy-static-app",
|
||||
env.useRollup() ? "rollup-watch-app" : "webpack-watch-app"
|
||||
env.useWDS()
|
||||
? "wds-watch-app"
|
||||
: env.useRollup()
|
||||
? "rollup-watch-app"
|
||||
: "webpack-watch-app"
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -19,6 +19,7 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
||||
return compiled({
|
||||
...data,
|
||||
useRollup: env.useRollup(),
|
||||
useWDS: env.useWDS(),
|
||||
renderTemplate,
|
||||
});
|
||||
};
|
||||
@@ -90,10 +91,23 @@ gulp.task("gen-pages-prod", (done) => {
|
||||
});
|
||||
|
||||
gulp.task("gen-index-app-dev", (done) => {
|
||||
let latestAppJS, latestCoreJS, latestCustomPanelJS;
|
||||
|
||||
if (env.useWDS()) {
|
||||
latestAppJS = "http://localhost:8000/src/entrypoints/app.ts";
|
||||
latestCoreJS = "http://localhost:8000/src/entrypoints/core.ts";
|
||||
latestCustomPanelJS =
|
||||
"http://localhost:8000/src/entrypoints/custom-panel.ts";
|
||||
} else {
|
||||
latestAppJS = "/frontend_latest/app.js";
|
||||
latestCoreJS = "/frontend_latest/core.js";
|
||||
latestCustomPanelJS = "/frontend_latest/custom-panel.js";
|
||||
}
|
||||
|
||||
const content = renderTemplate("index", {
|
||||
latestAppJS: "/frontend_latest/app.js",
|
||||
latestCoreJS: "/frontend_latest/core.js",
|
||||
latestCustomPanelJS: "/frontend_latest/custom-panel.js",
|
||||
latestAppJS,
|
||||
latestCoreJS,
|
||||
latestCustomPanelJS,
|
||||
|
||||
es5AppJS: "/frontend_es5/app.js",
|
||||
es5CoreJS: "/frontend_es5/core.js",
|
||||
|
@@ -33,21 +33,10 @@ String.prototype.rsplit = function (sep, maxsplit) {
|
||||
: split;
|
||||
};
|
||||
|
||||
// Panel translations which should be split from the core translations. These
|
||||
// should mirror the fragment definitions in polymer.json, so that we load
|
||||
// additional resources at equivalent points.
|
||||
const TRANSLATION_FRAGMENTS = [
|
||||
"config",
|
||||
"history",
|
||||
"logbook",
|
||||
"mailbox",
|
||||
"profile",
|
||||
"shopping-list",
|
||||
"page-authorize",
|
||||
"page-demo",
|
||||
"page-onboarding",
|
||||
"developer-tools",
|
||||
];
|
||||
// Panel translations which should be split from the core translations.
|
||||
const TRANSLATION_FRAGMENTS = Object.keys(
|
||||
require("../../src/translations/en.json").ui.panel
|
||||
);
|
||||
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
|
11
build-scripts/gulp/wds.js
Normal file
11
build-scripts/gulp/wds.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// Tasks to run Rollup
|
||||
const gulp = require("gulp");
|
||||
const { startDevServer } = require("@web/dev-server");
|
||||
|
||||
gulp.task("wds-watch-app", () => {
|
||||
startDevServer({
|
||||
config: {
|
||||
watch: true,
|
||||
},
|
||||
});
|
||||
});
|
@@ -47,7 +47,7 @@ const runDevServer = ({
|
||||
);
|
||||
});
|
||||
|
||||
const handler = (done) => (err, stats) => {
|
||||
const doneHandler = (done) => (err, stats) => {
|
||||
if (err) {
|
||||
log.error(err.stack || err);
|
||||
if (err.details) {
|
||||
@@ -67,11 +67,20 @@ const handler = (done) => (err, stats) => {
|
||||
}
|
||||
};
|
||||
|
||||
const prodBuild = (conf) =>
|
||||
new Promise((resolve) => {
|
||||
webpack(
|
||||
conf,
|
||||
// Resolve promise when done. Because we pass a callback, webpack closes itself
|
||||
doneHandler(resolve)
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task("webpack-watch-app", () => {
|
||||
// we are not calling done, so this command will run forever
|
||||
// This command will run forever because we don't close compiler
|
||||
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
|
||||
{ ignored: /build-translations/ },
|
||||
handler()
|
||||
doneHandler()
|
||||
);
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
@@ -79,15 +88,12 @@ gulp.task("webpack-watch-app", () => {
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-app",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
bothBuilds(createAppConfig, { isProdBuild: true }),
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
gulp.task("webpack-prod-app", () =>
|
||||
prodBuild(
|
||||
bothBuilds(createAppConfig, {
|
||||
isProdBuild: true,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-demo", () => {
|
||||
@@ -98,17 +104,12 @@ gulp.task("webpack-dev-server-demo", () => {
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-demo",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
bothBuilds(createDemoConfig, {
|
||||
isProdBuild: true,
|
||||
}),
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
gulp.task("webpack-prod-demo", () =>
|
||||
prodBuild(
|
||||
bothBuilds(createDemoConfig, {
|
||||
isProdBuild: true,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-cast", () => {
|
||||
@@ -121,41 +122,30 @@ gulp.task("webpack-dev-server-cast", () => {
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-cast",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
bothBuilds(createCastConfig, {
|
||||
isProdBuild: true,
|
||||
}),
|
||||
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
gulp.task("webpack-prod-cast", () =>
|
||||
prodBuild(
|
||||
bothBuilds(createCastConfig, {
|
||||
isProdBuild: true,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-watch-hassio", () => {
|
||||
// we are not calling done, so this command will run forever
|
||||
// This command will run forever because we don't close compiler
|
||||
webpack(
|
||||
createHassioConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
})
|
||||
).watch({}, handler());
|
||||
).watch({}, doneHandler());
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-hassio",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
bothBuilds(createHassioConfig, {
|
||||
isProdBuild: true,
|
||||
}),
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
gulp.task("webpack-prod-hassio", () =>
|
||||
prodBuild(
|
||||
bothBuilds(createHassioConfig, {
|
||||
isProdBuild: true,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-gallery", () => {
|
||||
@@ -167,17 +157,11 @@ gulp.task("webpack-dev-server-gallery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-gallery",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
createGalleryConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
}),
|
||||
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
gulp.task("webpack-prod-gallery", () =>
|
||||
prodBuild(
|
||||
createGalleryConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
@@ -1,5 +1,3 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = function (userOptions = {}) {
|
||||
// Files need to be absolute paths.
|
||||
// This only works if the file has no exports
|
||||
|
@@ -3,7 +3,7 @@ const path = require("path");
|
||||
const commonjs = require("@rollup/plugin-commonjs");
|
||||
const resolve = require("@rollup/plugin-node-resolve");
|
||||
const json = require("@rollup/plugin-json");
|
||||
const babel = require("rollup-plugin-babel");
|
||||
const babel = require("@rollup/plugin-babel").babel;
|
||||
const replace = require("@rollup/plugin-replace");
|
||||
const visualizer = require("rollup-plugin-visualizer");
|
||||
const { string } = require("rollup-plugin-string");
|
||||
@@ -31,6 +31,7 @@ const createRollupConfig = ({
|
||||
isStatsBuild,
|
||||
publicPath,
|
||||
dontHash,
|
||||
isWDS,
|
||||
}) => {
|
||||
return {
|
||||
/**
|
||||
@@ -61,6 +62,7 @@ const createRollupConfig = ({
|
||||
...bundle.babelOptions({ latestBuild }),
|
||||
extensions,
|
||||
exclude: bundle.babelExclude(),
|
||||
babelHelpers: isWDS ? "inline" : "bundled",
|
||||
}),
|
||||
string({
|
||||
// Import certain extensions as strings
|
||||
@@ -69,19 +71,21 @@ const createRollupConfig = ({
|
||||
replace(
|
||||
bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })
|
||||
),
|
||||
manifest({
|
||||
publicPath,
|
||||
}),
|
||||
worker(),
|
||||
dontHashPlugin({ dontHash }),
|
||||
isProdBuild && terser(bundle.terserOptions(latestBuild)),
|
||||
isStatsBuild &&
|
||||
!isWDS &&
|
||||
manifest({
|
||||
publicPath,
|
||||
}),
|
||||
!isWDS && worker(),
|
||||
!isWDS && dontHashPlugin({ dontHash }),
|
||||
!isWDS && isProdBuild && terser(bundle.terserOptions(latestBuild)),
|
||||
!isWDS &&
|
||||
isStatsBuild &&
|
||||
visualizer({
|
||||
// https://github.com/btd/rollup-plugin-visualizer#options
|
||||
open: true,
|
||||
sourcemap: true,
|
||||
}),
|
||||
],
|
||||
].filter(Boolean),
|
||||
},
|
||||
/**
|
||||
* @type { import("rollup").OutputOptions }
|
||||
@@ -108,12 +112,13 @@ const createRollupConfig = ({
|
||||
};
|
||||
};
|
||||
|
||||
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild, isWDS }) => {
|
||||
return createRollupConfig(
|
||||
bundle.config.app({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isWDS,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
@@ -3,22 +3,10 @@ import { Lovelace } from "../../../src/panels/lovelace/types";
|
||||
import { DemoConfig } from "./types";
|
||||
|
||||
export const demoConfigs: Array<() => Promise<DemoConfig>> = [
|
||||
() =>
|
||||
import(/* webpackChunkName: "arsaboo" */ "./arsaboo").then(
|
||||
(mod) => mod.demoArsaboo
|
||||
),
|
||||
() =>
|
||||
import(/* webpackChunkName: "teachingbirds" */ "./teachingbirds").then(
|
||||
(mod) => mod.demoTeachingbirds
|
||||
),
|
||||
() =>
|
||||
import(/* webpackChunkName: "kernehed" */ "./kernehed").then(
|
||||
(mod) => mod.demoKernehed
|
||||
),
|
||||
() =>
|
||||
import(/* webpackChunkName: "jimpower" */ "./jimpower").then(
|
||||
(mod) => mod.demoJimpower
|
||||
),
|
||||
() => import("./arsaboo").then((mod) => mod.demoArsaboo),
|
||||
() => import("./teachingbirds").then((mod) => mod.demoTeachingbirds),
|
||||
() => import("./kernehed").then((mod) => mod.demoKernehed),
|
||||
() => import("./jimpower").then((mod) => mod.demoJimpower),
|
||||
];
|
||||
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
|
@@ -9,5 +9,5 @@ export interface DemoConfig {
|
||||
authorUrl: string;
|
||||
lovelace: (localize: LocalizeFunc) => LovelaceConfig;
|
||||
entities: (localize: LocalizeFunc) => Entity[];
|
||||
theme: () => { [key: string]: string } | null;
|
||||
theme: () => Record<string, string> | null;
|
||||
}
|
||||
|
@@ -7,7 +7,5 @@ import "./ha-demo";
|
||||
|
||||
/* polyfill for paper-dropdown */
|
||||
setTimeout(() => {
|
||||
import(
|
||||
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
|
||||
);
|
||||
import("web-animations-js/web-animations-next-lite.min");
|
||||
}, 1000);
|
||||
|
@@ -21,15 +21,16 @@ class DemoCard extends PolymerElement {
|
||||
}
|
||||
pre {
|
||||
width: 400px;
|
||||
margin: 16px;
|
||||
margin: 0 16px;
|
||||
overflow: auto;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
.root {
|
||||
flex-direction: column;
|
||||
}
|
||||
pre {
|
||||
margin-left: 0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -26,8 +26,9 @@ class DemoMoreInfo extends PolymerElement {
|
||||
|
||||
pre {
|
||||
width: 400px;
|
||||
margin: 16px;
|
||||
margin: 0 16px;
|
||||
overflow: auto;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
@@ -35,7 +36,7 @@ class DemoMoreInfo extends PolymerElement {
|
||||
flex-direction: column;
|
||||
}
|
||||
pre {
|
||||
margin-left: 0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -7,8 +7,8 @@ export const createMediaPlayerEntities = () => [
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
|
||||
supported_features: 195135,
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set
|
||||
supported_features: 64063,
|
||||
entity_picture: "/images/album_cover_2.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 50,
|
||||
@@ -24,8 +24,8 @@ export const createMediaPlayerEntities = () => [
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set
|
||||
supported_features: 64063,
|
||||
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
|
||||
supported_features: 195135,
|
||||
entity_picture: "/images/album_cover.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 0,
|
||||
|
@@ -73,13 +73,7 @@ const CONFIGS = [
|
||||
|
||||
class DemoAlarmPanelEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -88,7 +82,6 @@ class DemoAlarmPanelEntity extends PolymerElement {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
hass: Object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -55,13 +55,7 @@ const CONFIGS = [
|
||||
|
||||
class DemoConditional extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -70,7 +64,6 @@ class DemoConditional extends PolymerElement {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
hass: Object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -20,10 +20,10 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "With Name",
|
||||
heading: "With Name (defined in card)",
|
||||
config: `
|
||||
- type: button
|
||||
name: Bedroom
|
||||
name: Custom Name
|
||||
entity: light.bed_light
|
||||
`,
|
||||
},
|
||||
@@ -32,7 +32,7 @@ const CONFIGS = [
|
||||
config: `
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
icon: mdi:hotel
|
||||
icon: mdi:tools
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -71,13 +71,7 @@ const CONFIGS = [
|
||||
|
||||
class DemoButtonEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -86,7 +80,6 @@ class DemoButtonEntity extends PolymerElement {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
hass: Object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -163,13 +163,7 @@ const CONFIGS = [
|
||||
|
||||
class DemoMap extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -178,7 +172,6 @@ class DemoMap extends PolymerElement {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
hass: Object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -146,17 +146,21 @@ const CONFIGS = [
|
||||
entity: media_player.receiver_off
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Grid Full Size",
|
||||
config: `
|
||||
- type: grid
|
||||
columns: 1
|
||||
cards:
|
||||
- type: media-control
|
||||
entity: media_player.music_paused
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoHuiMediControlCard extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -165,7 +169,6 @@ class DemoHuiMediControlCard extends PolymerElement {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
hass: Object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -57,13 +57,7 @@ const CONFIGS = [
|
||||
|
||||
class DemoHuiMediaPlayerRows extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -72,7 +66,6 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
hass: Object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -20,48 +20,47 @@ class HaGallery extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-positioning ha-style">
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
app-header-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
ha-icon-button.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
app-header-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
ha-icon-button.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.pickers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
}
|
||||
.pickers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.pickers ha-card {
|
||||
width: 400px;
|
||||
display: block;
|
||||
margin: 16px 8px;
|
||||
}
|
||||
.pickers ha-card {
|
||||
width: 400px;
|
||||
display: block;
|
||||
margin: 16px 8px;
|
||||
}
|
||||
|
||||
.pickers ha-card:last-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.pickers ha-card:last-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin: -1em 0;
|
||||
}
|
||||
.intro {
|
||||
margin: -1em 0;
|
||||
}
|
||||
|
||||
p a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
p a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<app-header-layout>
|
||||
@@ -70,32 +69,42 @@ class HaGallery extends PolymerElement {
|
||||
<ha-icon-button
|
||||
icon="hass:arrow-left"
|
||||
on-click="_backTapped"
|
||||
class$='[[_computeHeaderButtonClass(_demo)]]'
|
||||
class$="[[_computeHeaderButtonClass(_demo)]]"
|
||||
></ha-icon-button>
|
||||
<div main-title>[[_withDefault(_demo, "Home Assistant Gallery")]]</div>
|
||||
<div main-title>
|
||||
[[_withDefault(_demo, "Home Assistant Gallery")]]
|
||||
</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<div class='content'>
|
||||
<div id='demo'></div>
|
||||
<template is='dom-if' if='[[!_demo]]'>
|
||||
<div class='pickers'>
|
||||
<ha-card header="Lovelace card demos">
|
||||
<div class='card-content intro'>
|
||||
<div class="content">
|
||||
<div id="demo"></div>
|
||||
<template is="dom-if" if="[[!_demo]]">
|
||||
<div class="pickers">
|
||||
<ha-card header="Lovelace Card Demos">
|
||||
<div class="card-content intro">
|
||||
<p>
|
||||
Lovelace has many different cards. Each card allows the user to tell a different story about what is going on in their house. These cards are very customizable, as no household is the same.
|
||||
Lovelace has many different cards. Each card allows the user
|
||||
to tell a different story about what is going on in their
|
||||
house. These cards are very customizable, as no household is
|
||||
the same.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This gallery helps our developers and designers to see all the different states that each card can be in.
|
||||
This gallery helps our developers and designers to see all
|
||||
the different states that each card can be in.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Check <a href='https://www.home-assistant.io/lovelace'>the official website</a> for instructions on how to get started with Lovelace.</a>.
|
||||
Check
|
||||
<a href="https://www.home-assistant.io/lovelace"
|
||||
>the official website</a
|
||||
>
|
||||
for instructions on how to get started with Lovelace.
|
||||
</p>
|
||||
</div>
|
||||
<template is='dom-repeat' items='[[_lovelaceDemos]]'>
|
||||
<a href='#[[item]]'>
|
||||
<template is="dom-repeat" items="[[_lovelaceDemos]]">
|
||||
<a href="#[[item]]">
|
||||
<paper-item>
|
||||
<paper-item-body>{{ item }}</paper-item-body>
|
||||
<ha-icon icon="hass:chevron-right"></ha-icon>
|
||||
@@ -104,14 +113,14 @@ class HaGallery extends PolymerElement {
|
||||
</template>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="More Info demos">
|
||||
<div class='card-content intro'>
|
||||
<ha-card header="More Info Demos">
|
||||
<div class="card-content intro">
|
||||
<p>
|
||||
More info screens show up when an entity is clicked.
|
||||
</p>
|
||||
</div>
|
||||
<template is='dom-repeat' items='[[_moreInfoDemos]]'>
|
||||
<a href='#[[item]]'>
|
||||
<template is="dom-repeat" items="[[_moreInfoDemos]]">
|
||||
<a href="#[[item]]">
|
||||
<paper-item>
|
||||
<paper-item-body>{{ item }}</paper-item-body>
|
||||
<ha-icon icon="hass:chevron-right"></ha-icon>
|
||||
@@ -120,14 +129,14 @@ class HaGallery extends PolymerElement {
|
||||
</template>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Util demos">
|
||||
<div class='card-content intro'>
|
||||
<ha-card header="Util Demos">
|
||||
<div class="card-content intro">
|
||||
<p>
|
||||
Test pages for our utility functions.
|
||||
</p>
|
||||
</div>
|
||||
<template is='dom-repeat' items='[[_utilDemos]]'>
|
||||
<a href='#[[item]]'>
|
||||
<template is="dom-repeat" items="[[_utilDemos]]">
|
||||
<a href="#[[item]]">
|
||||
<paper-item>
|
||||
<paper-item-body>{{ item }}</paper-item-body>
|
||||
<ha-icon icon="hass:chevron-right"></ha-icon>
|
||||
@@ -139,7 +148,10 @@ class HaGallery extends PolymerElement {
|
||||
</template>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
<notification-manager hass=[[_fakeHass]] id='notifications'></notification-manager>
|
||||
<notification-manager
|
||||
hass="[[_fakeHass]]"
|
||||
id="notifications"
|
||||
></notification-manager>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -27,6 +27,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
@customElement("hassio-upload-snapshot")
|
||||
export class HassioUploadSnapshot extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
@@ -51,6 +53,20 @@ export class HassioUploadSnapshot extends LitElement {
|
||||
private async _uploadFile(ev) {
|
||||
const file = ev.detail.files[0];
|
||||
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
showAlertDialog(this, {
|
||||
title: "Snapshot file is too big",
|
||||
text: html`The maximum allowed filesize is 1GB.<br />
|
||||
<a
|
||||
href="https://www.home-assistant.io/hassio/haos_common_tasks/#restoring-a-snapshot-on-a-new-install"
|
||||
target="_blank"
|
||||
>Have a look here on how to restore it.</a
|
||||
>`,
|
||||
confirmText: "ok",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!["application/x-tar"].includes(file.type)) {
|
||||
showAlertDialog(this, {
|
||||
title: "Unsupported file format",
|
||||
|
@@ -12,7 +12,7 @@ import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { compare } from "../../../src/common/string/compare";
|
||||
import "../../../src/components/ha-card";
|
||||
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-card-content";
|
||||
@@ -22,14 +22,14 @@ import { hassioStyle } from "../resources/hassio-style";
|
||||
class HassioAddons extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addons?: HassioAddonInfo[];
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>Add-ons</h1>
|
||||
<div class="card-group">
|
||||
${!this.addons?.length
|
||||
${!this.supervisor.supervisor.addons?.length
|
||||
? html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
@@ -41,7 +41,7 @@ class HassioAddons extends LitElement {
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: this.addons
|
||||
: this.supervisor.supervisor.addons
|
||||
.sort((a, b) => compare(a.name, b.name))
|
||||
.map(
|
||||
(addon) => html`
|
||||
|
@@ -7,11 +7,7 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||
import {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
@@ -23,16 +19,12 @@ import "./hassio-update";
|
||||
class HassioDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
|
||||
|
||||
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@@ -47,13 +39,11 @@ class HassioDashboard extends LitElement {
|
||||
<div class="content">
|
||||
<hassio-update
|
||||
.hass=${this.hass}
|
||||
.hassInfo=${this.hassInfo}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-update>
|
||||
<hassio-addons
|
||||
.hass=${this.hass}
|
||||
.addons=${this.supervisorInfo.addons}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-addons>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -35,31 +36,20 @@ import { hassioStyle } from "../resources/hassio-style";
|
||||
export class HassioUpdate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo;
|
||||
|
||||
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
private _pendingUpdates = memoizeOne(
|
||||
(
|
||||
core?: HassioHomeAssistantInfo,
|
||||
supervisor?: HassioSupervisorInfo,
|
||||
os?: HassioHassOSInfo
|
||||
): number => {
|
||||
return [core, supervisor, os].filter(
|
||||
(value) => !!value && value?.update_available
|
||||
).length;
|
||||
}
|
||||
);
|
||||
private _pendingUpdates = memoizeOne((supervisor: Supervisor): number => {
|
||||
return Object.keys(supervisor).filter(
|
||||
(value) => supervisor[value].update_available
|
||||
).length;
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const updatesAvailable = this._pendingUpdates(
|
||||
this.hassInfo,
|
||||
this.supervisorInfo,
|
||||
this.hassOsInfo
|
||||
);
|
||||
if (!this.supervisor) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const updatesAvailable = this._pendingUpdates(this.supervisor);
|
||||
if (!updatesAvailable) {
|
||||
return html``;
|
||||
}
|
||||
@@ -74,26 +64,24 @@ export class HassioUpdate extends LitElement {
|
||||
<div class="card-group">
|
||||
${this._renderUpdateCard(
|
||||
"Home Assistant Core",
|
||||
this.hassInfo!,
|
||||
this.supervisor.core,
|
||||
"hassio/homeassistant/update",
|
||||
`https://${
|
||||
this.hassInfo?.version_latest.includes("b") ? "rc" : "www"
|
||||
this.supervisor.core.version_latest.includes("b") ? "rc" : "www"
|
||||
}.home-assistant.io/latest-release-notes/`
|
||||
)}
|
||||
${this._renderUpdateCard(
|
||||
"Supervisor",
|
||||
this.supervisorInfo!,
|
||||
this.supervisor.supervisor,
|
||||
"hassio/supervisor/update",
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${
|
||||
this.supervisorInfo!.version_latest
|
||||
}`
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
|
||||
)}
|
||||
${this.hassOsInfo
|
||||
${this.supervisor.host.features.includes("hassos")
|
||||
? this._renderUpdateCard(
|
||||
"Operating System",
|
||||
this.hassOsInfo,
|
||||
this.supervisor.os,
|
||||
"hassio/os/update",
|
||||
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
|
||||
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
|
@@ -11,10 +11,7 @@ export const showHassioMarkdownDialog = (
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-markdown",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"
|
||||
),
|
||||
dialogImport: () => import("./dialog-hassio-markdown"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-icon-button";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
@@ -16,18 +18,22 @@ import {
|
||||
} from "lit-element";
|
||||
import { cache } from "lit-html/directives/cache";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-chips";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-expansion-panel";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../../src/components/ha-radio";
|
||||
import "../../../../src/components/ha-related-items";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
AccessPoints,
|
||||
accesspointScan,
|
||||
NetworkInterface,
|
||||
updateNetworkInterface,
|
||||
WifiConfiguration,
|
||||
} from "../../../../src/data/hassio/network";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -38,54 +44,51 @@ import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||
|
||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||
|
||||
@customElement("dialog-hassio-network")
|
||||
export class DialogHassioNetwork extends LitElement
|
||||
implements HassDialog<HassioNetworkDialogParams> {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _prosessing = false;
|
||||
|
||||
@internalProperty() private _params?: HassioNetworkDialogParams;
|
||||
|
||||
@internalProperty() private _network!: {
|
||||
interface: string;
|
||||
data: NetworkInterface;
|
||||
}[];
|
||||
@internalProperty() private _accessPoints?: AccessPoints;
|
||||
|
||||
@internalProperty() private _curTabIndex = 0;
|
||||
|
||||
@internalProperty() private _device?: {
|
||||
interface: string;
|
||||
data: NetworkInterface;
|
||||
};
|
||||
|
||||
@internalProperty() private _dirty = false;
|
||||
|
||||
@internalProperty() private _interface?: NetworkInterface;
|
||||
|
||||
@internalProperty() private _interfaces!: NetworkInterface[];
|
||||
|
||||
@internalProperty() private _params?: HassioNetworkDialogParams;
|
||||
|
||||
@internalProperty() private _processing = false;
|
||||
|
||||
@internalProperty() private _scanning = false;
|
||||
|
||||
@internalProperty() private _wifiConfiguration?: WifiConfiguration;
|
||||
|
||||
public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._dirty = false;
|
||||
this._curTabIndex = 0;
|
||||
this._network = Object.keys(params.network?.interfaces)
|
||||
.map((device) => ({
|
||||
interface: device,
|
||||
data: params.network.interfaces[device],
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
return a.data.primary > b.data.primary ? -1 : 1;
|
||||
});
|
||||
this._device = this._network[this._curTabIndex];
|
||||
this._device.data.nameservers = String(this._device.data.nameservers);
|
||||
this._interfaces = params.network.interfaces.sort((a, b) => {
|
||||
return a.primary > b.primary ? -1 : 1;
|
||||
});
|
||||
this._interface = { ...this._interfaces[this._curTabIndex] };
|
||||
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
this._prosessing = false;
|
||||
this._processing = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params || !this._network) {
|
||||
if (!this._params || !this._interface) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -107,11 +110,11 @@ export class DialogHassioNetwork extends LitElement
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</ha-header-bar>
|
||||
${this._network.length > 1
|
||||
${this._interfaces.length > 1
|
||||
? html` <mwc-tab-bar
|
||||
.activeIndex=${this._curTabIndex}
|
||||
@MDCTabBar:activated=${this._handleTabActivated}
|
||||
>${this._network.map(
|
||||
>${this._interfaces.map(
|
||||
(device) =>
|
||||
html`<mwc-tab
|
||||
.id=${device.interface}
|
||||
@@ -129,81 +132,302 @@ export class DialogHassioNetwork extends LitElement
|
||||
|
||||
private _renderTab() {
|
||||
return html` <div class="form container">
|
||||
<ha-formfield label="DHCP">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="dhcp"
|
||||
name="method"
|
||||
?checked=${this._device!.data.method === "dhcp"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Static">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="static"
|
||||
name="method"
|
||||
?checked=${this._device!.data.method === "static"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
${this._device!.data.method !== "dhcp"
|
||||
? html` <paper-input
|
||||
class="flex-auto"
|
||||
id="ip_address"
|
||||
label="IP address/Netmask"
|
||||
.value="${this._device!.data.ip_address}"
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="gateway"
|
||||
label="Gateway address"
|
||||
.value="${this._device!.data.gateway}"
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="nameservers"
|
||||
label="DNS servers"
|
||||
.value="${this._device!.data.nameservers as string}"
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
></paper-input>
|
||||
NB!: If you are changing IP or gateway addresses, you might lose
|
||||
the connection.`
|
||||
${IP_VERSIONS.map((version) =>
|
||||
this._interface![version] ? this._renderIPConfiguration(version) : ""
|
||||
)}
|
||||
${this._interface?.type === "wireless"
|
||||
? html`
|
||||
<ha-expansion-panel header="Wi-Fi" outlined>
|
||||
${this._interface?.wifi?.ssid
|
||||
? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>`
|
||||
: ""}
|
||||
<mwc-button
|
||||
class="scan"
|
||||
@click=${this._scanForAP}
|
||||
.disabled=${this._scanning}
|
||||
>
|
||||
${this._scanning
|
||||
? html`<ha-circular-progress active size="small">
|
||||
</ha-circular-progress>`
|
||||
: "Scan for accesspoints"}
|
||||
</mwc-button>
|
||||
${this._accessPoints &&
|
||||
this._accessPoints.accesspoints &&
|
||||
this._accessPoints.accesspoints.length !== 0
|
||||
? html`
|
||||
<mwc-list>
|
||||
${this._accessPoints.accesspoints
|
||||
.filter((ap) => ap.ssid)
|
||||
.map(
|
||||
(ap) =>
|
||||
html`
|
||||
<mwc-list-item
|
||||
twoline
|
||||
@click=${this._selectAP}
|
||||
.activated=${ap.ssid ===
|
||||
this._wifiConfiguration?.ssid}
|
||||
.ap=${ap}
|
||||
>
|
||||
<span>${ap.ssid}</span>
|
||||
<span slot="secondary">
|
||||
${ap.mac} - Strength: ${ap.signal}
|
||||
</span>
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: ""}
|
||||
${this._wifiConfiguration
|
||||
? html`
|
||||
<div class="radio-row">
|
||||
<ha-formfield label="open">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="open"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth ===
|
||||
undefined ||
|
||||
this._wifiConfiguration.auth === "open"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="wep">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="wep"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth === "wep"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="wpa-psk">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="wpa-psk"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth ===
|
||||
"wpa-psk"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
type="password"
|
||||
id="psk"
|
||||
label="Password"
|
||||
version="wifi"
|
||||
@value-changed=${this
|
||||
._handleInputValueChangedWifi}
|
||||
>
|
||||
</paper-input>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: ""}
|
||||
${this._dirty
|
||||
? html`<div class="warning">
|
||||
If you are changing the Wi-Fi, IP or gateway addresses, you might
|
||||
lose the connection!
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
|
||||
<mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}>
|
||||
${this._prosessing
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: "Update"}
|
||||
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
|
||||
${this._processing
|
||||
? html`<ha-circular-progress active size="small">
|
||||
</ha-circular-progress>`
|
||||
: "Save"}
|
||||
</mwc-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _updateNetwork() {
|
||||
this._prosessing = true;
|
||||
let options: Partial<NetworkInterface> = {
|
||||
method: this._device!.data.method,
|
||||
};
|
||||
if (options.method !== "dhcp") {
|
||||
options = {
|
||||
...options,
|
||||
address: this._device!.data.ip_address,
|
||||
gateway: this._device!.data.gateway,
|
||||
dns: String(this._device!.data.nameservers).split(","),
|
||||
};
|
||||
private _selectAP(event) {
|
||||
this._wifiConfiguration = event.currentTarget.ap;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private async _scanForAP() {
|
||||
if (!this._interface) {
|
||||
return;
|
||||
}
|
||||
this._scanning = true;
|
||||
try {
|
||||
await updateNetworkInterface(this.hass, this._device!.interface, options);
|
||||
this._accessPoints = await accesspointScan(
|
||||
this.hass,
|
||||
this._interface.interface
|
||||
);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to scan for accesspoints",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
this._scanning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderIPConfiguration(version: string) {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
.header=${`IPv${version.charAt(version.length - 1)}`}
|
||||
outlined
|
||||
>
|
||||
<div class="radio-row">
|
||||
<ha-formfield label="DHCP">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="auto"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "auto"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Static">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="static"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "static"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Disabled" class="warning">
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="disabled"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "disabled"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${this._interface![version].method === "static"
|
||||
? html`
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="address"
|
||||
label="IP address/Netmask"
|
||||
.version=${version}
|
||||
.value=${this._toString(this._interface![version].address)}
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
>
|
||||
</paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="gateway"
|
||||
label="Gateway address"
|
||||
.version=${version}
|
||||
.value=${this._interface![version].gateway}
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
>
|
||||
</paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="nameservers"
|
||||
label="DNS servers"
|
||||
.version=${version}
|
||||
.value=${this._toString(this._interface![version].nameservers)}
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
>
|
||||
</paper-input>
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
_toArray(data: string | string[]): string[] {
|
||||
if (Array.isArray(data)) {
|
||||
if (data && typeof data[0] === "string") {
|
||||
data = data[0];
|
||||
}
|
||||
}
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
if (typeof data === "string") {
|
||||
return data.replace(/ /g, "").split(",");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
_toString(data: string | string[]): string {
|
||||
if (!data) {
|
||||
return "";
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
return data.join(", ");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private async _updateNetwork() {
|
||||
this._processing = true;
|
||||
let interfaceOptions: Partial<NetworkInterface> = {};
|
||||
|
||||
IP_VERSIONS.forEach((version) => {
|
||||
interfaceOptions[version] = {
|
||||
method: this._interface![version]?.method || "auto",
|
||||
};
|
||||
if (this._interface![version]?.method === "static") {
|
||||
interfaceOptions[version] = {
|
||||
...interfaceOptions[version],
|
||||
address: this._toArray(this._interface![version]?.address),
|
||||
gateway: this._interface![version]?.gateway,
|
||||
nameservers: this._toArray(this._interface![version]?.nameservers),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (this._wifiConfiguration) {
|
||||
interfaceOptions = {
|
||||
...interfaceOptions,
|
||||
wifi: {
|
||||
ssid: this._wifiConfiguration.ssid,
|
||||
mode: this._wifiConfiguration.mode,
|
||||
auth: this._wifiConfiguration.auth || "open",
|
||||
},
|
||||
};
|
||||
if (interfaceOptions.wifi!.auth !== "open") {
|
||||
interfaceOptions.wifi = {
|
||||
...interfaceOptions.wifi,
|
||||
psk: this._wifiConfiguration.psk,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interfaceOptions.enabled =
|
||||
this._wifiConfiguration !== undefined ||
|
||||
interfaceOptions.ipv4?.method !== "disabled" ||
|
||||
interfaceOptions.ipv6?.method !== "disabled";
|
||||
|
||||
try {
|
||||
await updateNetworkInterface(
|
||||
this.hass,
|
||||
this._interface!.interface,
|
||||
interfaceOptions
|
||||
);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to change network settings",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
this._prosessing = false;
|
||||
this._processing = false;
|
||||
return;
|
||||
}
|
||||
this._params?.loadData();
|
||||
@@ -219,40 +443,73 @@ export class DialogHassioNetwork extends LitElement
|
||||
dismissText: "no",
|
||||
});
|
||||
if (!confirm) {
|
||||
this.requestUpdate("_device");
|
||||
this.requestUpdate("_interface");
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._curTabIndex = ev.detail.index;
|
||||
this._device = this._network[ev.detail.index];
|
||||
this._device.data.nameservers = String(this._device.data.nameservers);
|
||||
this._interface = { ...this._interfaces[ev.detail.index] };
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: CustomEvent): void {
|
||||
const value = (ev.target as HaRadio).value as "dhcp" | "static";
|
||||
const value = (ev.target as any).value as "disabled" | "auto" | "static";
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
|
||||
if (!value || !this._device || this._device!.data.method === value) {
|
||||
if (
|
||||
!value ||
|
||||
!this._interface ||
|
||||
this._interface[version]!.method === value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
|
||||
this._device!.data.method = value;
|
||||
this.requestUpdate("_device");
|
||||
this._interface[version]!.method = value;
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _handleRadioValueChangedAp(ev: CustomEvent): void {
|
||||
const value = ((ev.target as any).value as string) as
|
||||
| "open"
|
||||
| "wep"
|
||||
| "wpa-psk";
|
||||
this._wifiConfiguration!.auth = value;
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_wifiConfiguration");
|
||||
}
|
||||
|
||||
private _handleInputValueChanged(ev: CustomEvent): void {
|
||||
const value: string | null | undefined = (ev.target as PaperInputElement)
|
||||
.value;
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
const id = (ev.target as PaperInputElement).id;
|
||||
|
||||
if (!value || !this._device || this._device.data[id] === value) {
|
||||
if (
|
||||
!value ||
|
||||
!this._interface ||
|
||||
this._toString(this._interface[version]![id]) === this._toString(value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
this._interface[version]![id] = value;
|
||||
}
|
||||
|
||||
this._device.data[id] = value;
|
||||
private _handleInputValueChangedWifi(ev: CustomEvent): void {
|
||||
const value: string | null | undefined = (ev.target as PaperInputElement)
|
||||
.value;
|
||||
const id = (ev.target as PaperInputElement).id;
|
||||
|
||||
if (
|
||||
!value ||
|
||||
!this._wifiConfiguration ||
|
||||
this._wifiConfiguration![id] === value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._dirty = true;
|
||||
this._wifiConfiguration![id] = value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
@@ -299,12 +556,16 @@ export class DialogHassioNetwork extends LitElement
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
mwc-button.scan {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
:host([rtl]) app-toolbar {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
.container {
|
||||
padding: 20px 24px;
|
||||
padding: 0 8px 4px;
|
||||
}
|
||||
.form {
|
||||
margin-bottom: 53px;
|
||||
@@ -322,6 +583,24 @@ export class DialogHassioNetwork extends LitElement
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||
background-color: var(--mdc-theme-surface, #fff);
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
--primary-color: var(--error-color);
|
||||
}
|
||||
div.warning {
|
||||
margin: 12px 4px -12px;
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
paper-input {
|
||||
padding: 0 14px;
|
||||
}
|
||||
mwc-list-item {
|
||||
--mdc-list-side-padding: 10px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -13,10 +13,7 @@ export const showNetworkDialog = (
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-network",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network"
|
||||
),
|
||||
dialogImport: () => import("./dialog-hassio-network"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
|
@@ -4,10 +4,7 @@ import "./dialog-hassio-registries";
|
||||
export const showRegistriesDialog = (element: HTMLElement): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-registries",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries"
|
||||
),
|
||||
dialogImport: () => import("./dialog-hassio-registries"),
|
||||
dialogParams: {},
|
||||
});
|
||||
};
|
||||
|
@@ -13,10 +13,7 @@ export const showRepositoriesDialog = (
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-repositories",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-repositories" */ "./dialog-hassio-repositories"
|
||||
),
|
||||
dialogImport: () => import("./dialog-hassio-repositories"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
|
@@ -109,7 +109,7 @@ class HassioSnapshotDialog extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog open stacked @closing=${this._closeDialog} .heading=${true}>
|
||||
<ha-dialog open @closing=${this._closeDialog} .heading=${true}>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
@@ -191,47 +191,37 @@ class HassioSnapshotDialog extends LitElement {
|
||||
: ""}
|
||||
${this._error ? html` <p class="error">Error: ${this._error}</p> ` : ""}
|
||||
|
||||
<div>Actions:</div>
|
||||
${!this._onboarding
|
||||
? html`<mwc-button
|
||||
@click=${this._downloadClicked}
|
||||
slot="primaryAction"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon>
|
||||
Download Snapshot
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
|
||||
<mwc-button
|
||||
@click=${this._partialRestoreClicked}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
|
||||
Restore Selected
|
||||
</mwc-button>
|
||||
${this._snapshot.type === "full"
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._fullRestoreClicked}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
|
||||
Wipe & restore
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
${!this._onboarding
|
||||
? html`<mwc-button
|
||||
@click=${this._deleteClicked}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiDelete}
|
||||
class="icon warning"
|
||||
></ha-svg-icon>
|
||||
<span class="warning">Delete Snapshot</span>
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
<div class="button-row" slot="primaryAction">
|
||||
<mwc-button @click=${this._partialRestoreClicked}>
|
||||
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
|
||||
Restore Selected
|
||||
</mwc-button>
|
||||
${!this._onboarding
|
||||
? html`
|
||||
<mwc-button @click=${this._deleteClicked}>
|
||||
<ha-svg-icon .path=${mdiDelete} class="icon warning">
|
||||
</ha-svg-icon>
|
||||
<span class="warning">Delete Snapshot</span>
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="button-row" slot="secondaryAction">
|
||||
${this._snapshot.type === "full"
|
||||
? html`
|
||||
<mwc-button @click=${this._fullRestoreClicked}>
|
||||
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
|
||||
Restore Everything
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
${!this._onboarding
|
||||
? html`<mwc-button @click=${this._downloadClicked}>
|
||||
<ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon>
|
||||
Download Snapshot
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -245,6 +235,14 @@ class HassioSnapshotDialog extends LitElement {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
}
|
||||
mwc-button ha-svg-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.button-row {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.details {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@@ -252,10 +250,6 @@ class HassioSnapshotDialog extends LitElement {
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.buttons li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
@@ -12,10 +12,7 @@ export const showHassioSnapshotDialog = (
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-snapshot",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"
|
||||
),
|
||||
dialogImport: () => import("./dialog-hassio-snapshot"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
|
@@ -13,10 +13,7 @@ export const showSnapshotUploadDialog = (
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-snapshot-upload",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-snapshot-upload" */ "./dialog-hassio-snapshot-upload"
|
||||
),
|
||||
dialogImport: () => import("./dialog-hassio-snapshot-upload"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
|
@@ -1,29 +1,22 @@
|
||||
import {
|
||||
html,
|
||||
PropertyValues,
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { html, PropertyValues, customElement, property } from "lit-element";
|
||||
import "./hassio-router";
|
||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import { SupervisorBaseElement } from "./supervisor-base-element";
|
||||
|
||||
@customElement("hassio-main")
|
||||
export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
|
||||
export class HassioMain extends SupervisorBaseElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public panel!: HassioPanelInfo;
|
||||
@property({ attribute: false }) public panel!: HassioPanelInfo;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property() public route?: Route;
|
||||
@property({ attribute: false }) public route?: Route;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
@@ -77,9 +70,13 @@ export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.supervisor || !this.hass) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<hassio-router
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.route=${this.route}
|
||||
.panel=${this.panel}
|
||||
.narrow=${this.narrow}
|
||||
|
@@ -1,10 +1,5 @@
|
||||
import { customElement, property } from "lit-element";
|
||||
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
|
||||
import {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
HassioInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
@@ -21,20 +16,12 @@ import "./system/hassio-system";
|
||||
class HassioPanelRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
@property({ attribute: false }) public hassioInfo!: HassioInfo;
|
||||
|
||||
@property({ attribute: false }) public hostInfo?: HassioHostInfo;
|
||||
|
||||
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
routes: {
|
||||
dashboard: {
|
||||
@@ -54,13 +41,9 @@ class HassioPanelRouter extends HassRouterPage {
|
||||
|
||||
protected updatePageEl(el) {
|
||||
el.hass = this.hass;
|
||||
el.supervisor = this.supervisor;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,18 +1,13 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
css,
|
||||
CSSResult,
|
||||
} from "lit-element";
|
||||
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
|
||||
import {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
HassioInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import "./hassio-panel-router";
|
||||
|
||||
@@ -20,34 +15,19 @@ import "./hassio-panel-router";
|
||||
class HassioPanel extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
|
||||
|
||||
@property({ attribute: false }) public hassioInfo!: HassioInfo;
|
||||
|
||||
@property({ attribute: false }) public hostInfo!: HassioHostInfo;
|
||||
|
||||
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.supervisorInfo) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<hassio-panel-router
|
||||
.route=${this.route}
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.route=${this.route}
|
||||
.narrow=${this.narrow}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
.hassioInfo=${this.hassioInfo}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.hassInfo=${this.hassInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-panel-router>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,24 +1,6 @@
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import {
|
||||
fetchHassioHassOsInfo,
|
||||
fetchHassioHostInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
} from "../../src/data/hassio/host";
|
||||
import {
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
fetchHassioInfo,
|
||||
HassioHomeAssistantInfo,
|
||||
HassioInfo,
|
||||
HassioPanelInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import { customElement, property } from "lit-element";
|
||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
@@ -32,9 +14,11 @@ import "./hassio-panel";
|
||||
class HassioRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public panel!: HassioPanelInfo;
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
@property({ attribute: false }) public panel!: HassioPanelInfo;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
||||
@@ -51,47 +35,22 @@ class HassioRouter extends HassRouterPage {
|
||||
system: "dashboard",
|
||||
addon: {
|
||||
tag: "hassio-addon-dashboard",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
|
||||
),
|
||||
load: () => import("./addon-view/hassio-addon-dashboard"),
|
||||
},
|
||||
ingress: {
|
||||
tag: "hassio-ingress-view",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
|
||||
),
|
||||
load: () => import("./ingress-view/hassio-ingress-view"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@internalProperty() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
@internalProperty() private _hostInfo?: HassioHostInfo;
|
||||
|
||||
@internalProperty() private _hassioInfo?: HassioInfo;
|
||||
|
||||
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
|
||||
|
||||
@internalProperty() private _hassInfo?: HassioHomeAssistantInfo;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
}
|
||||
|
||||
protected updatePageEl(el) {
|
||||
// the tabs page does its own routing so needs full route.
|
||||
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
|
||||
|
||||
el.hass = this.hass;
|
||||
el.supervisor = this.supervisor;
|
||||
el.narrow = this.narrow;
|
||||
el.supervisorInfo = this._supervisorInfo;
|
||||
el.hassioInfo = this._hassioInfo;
|
||||
el.hostInfo = this._hostInfo;
|
||||
el.hassInfo = this._hassInfo;
|
||||
el.hassOsInfo = this._hassOsInfo;
|
||||
el.route = route;
|
||||
|
||||
if (el.localName === "hassio-ingress-view") {
|
||||
@@ -102,45 +61,12 @@ class HassioRouter extends HassRouterPage {
|
||||
private async _fetchData() {
|
||||
if (this.panel.config && this.panel.config.ingress) {
|
||||
this._redirectIngress(this.panel.config.ingress);
|
||||
return;
|
||||
}
|
||||
|
||||
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = 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;
|
||||
|
||||
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
|
||||
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
private _redirectIngress(addonSlug: string) {
|
||||
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
|
||||
}
|
||||
|
||||
private _apiCalled(ev) {
|
||||
if (!ev.detail.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tries = 1;
|
||||
|
||||
const tryUpdate = () => {
|
||||
this._fetchData().catch(() => {
|
||||
tries += 1;
|
||||
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
tryUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -26,7 +26,6 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
@@ -41,7 +40,7 @@ import {
|
||||
HassioSnapshot,
|
||||
reloadHassioSnapshots,
|
||||
} from "../../../src/data/hassio/snapshot";
|
||||
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
@@ -67,7 +66,7 @@ class HassioSnapshots extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@internalProperty() private _snapshotName = "";
|
||||
|
||||
@@ -266,7 +265,7 @@ class HassioSnapshots extends LitElement {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("supervisorInfo")) {
|
||||
this._addonList = this.supervisorInfo.addons
|
||||
this._addonList = this.supervisor.supervisor.addons
|
||||
.map((addon) => ({
|
||||
slug: addon.slug,
|
||||
name: addon.name,
|
||||
@@ -372,7 +371,6 @@ class HassioSnapshots extends LitElement {
|
||||
await createHassioPartialSnapshot(this.hass, data);
|
||||
}
|
||||
this._updateSnapshots();
|
||||
fireEvent(this, "hass-api-called", { success: true, response: null });
|
||||
} catch (err) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
|
69
hassio/src/supervisor-base-element.ts
Normal file
69
hassio/src/supervisor-base-element.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { LitElement, property, PropertyValues } from "lit-element";
|
||||
import {
|
||||
fetchHassioHassOsInfo,
|
||||
fetchHassioHostInfo,
|
||||
} from "../../src/data/hassio/host";
|
||||
import { fetchNetworkInfo } from "../../src/data/hassio/network";
|
||||
import { fetchHassioResolution } from "../../src/data/hassio/resolution";
|
||||
import {
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"supervisor-update": Partial<Supervisor>;
|
||||
}
|
||||
}
|
||||
|
||||
export class SupervisorBaseElement extends urlSyncMixin(
|
||||
ProvideHassLitMixin(LitElement)
|
||||
) {
|
||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||
|
||||
protected _updateSupervisor(obj: Partial<Supervisor>): void {
|
||||
this.supervisor = { ...this.supervisor!, ...obj };
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this._initSupervisor();
|
||||
this.addEventListener("supervisor-update", (ev) =>
|
||||
this._updateSupervisor(ev.detail)
|
||||
);
|
||||
}
|
||||
|
||||
private async _initSupervisor(): Promise<void> {
|
||||
const [
|
||||
supervisor,
|
||||
host,
|
||||
core,
|
||||
info,
|
||||
os,
|
||||
network,
|
||||
resolution,
|
||||
] = await Promise.all([
|
||||
fetchHassioSupervisorInfo(this.hass),
|
||||
fetchHassioHostInfo(this.hass),
|
||||
fetchHassioHomeAssistantInfo(this.hass),
|
||||
fetchHassioInfo(this.hass),
|
||||
fetchHassioHassOsInfo(this.hass),
|
||||
fetchNetworkInfo(this.hass),
|
||||
fetchHassioResolution(this.hass),
|
||||
]);
|
||||
|
||||
this.supervisor = {
|
||||
supervisor,
|
||||
host,
|
||||
core,
|
||||
info,
|
||||
os,
|
||||
network,
|
||||
resolution,
|
||||
};
|
||||
}
|
||||
}
|
@@ -8,12 +8,12 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
@@ -27,8 +27,6 @@ import {
|
||||
changeHostOptions,
|
||||
configSyncOS,
|
||||
fetchHassioHostInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo as HassioHostInfoType,
|
||||
rebootHost,
|
||||
shutdownHost,
|
||||
updateOS,
|
||||
@@ -37,7 +35,7 @@ import {
|
||||
fetchNetworkInfo,
|
||||
NetworkInfo,
|
||||
} from "../../../src/data/hassio/network";
|
||||
import { HassioInfo } from "../../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -53,28 +51,22 @@ import { hassioStyle } from "../resources/hassio-style";
|
||||
class HassioHostInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public hostInfo!: HassioHostInfoType;
|
||||
|
||||
@property({ attribute: false }) public hassioInfo!: HassioInfo;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
@internalProperty() public _networkInfo?: NetworkInfo;
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const primaryIpAddress = this.hostInfo.features.includes("network")
|
||||
? this._primaryIpAddress(this._networkInfo!)
|
||||
const primaryIpAddress = this.supervisor.host.features.includes("network")
|
||||
? this._primaryIpAddress(this.supervisor.network!)
|
||||
: "";
|
||||
return html`
|
||||
<ha-card header="Host System">
|
||||
<div class="card-content">
|
||||
${this.hostInfo.features.includes("hostname")
|
||||
${this.supervisor.host.features.includes("hostname")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Hostname
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hostInfo.hostname}
|
||||
${this.supervisor.host.hostname}
|
||||
</span>
|
||||
<mwc-button
|
||||
title="Change the hostname"
|
||||
@@ -84,7 +76,7 @@ class HassioHostInfo extends LitElement {
|
||||
</mwc-button>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${this.hostInfo.features.includes("network")
|
||||
${this.supervisor.host.features.includes("network")
|
||||
? html` <ha-settings-row>
|
||||
<span slot="heading">
|
||||
IP Address
|
||||
@@ -106,10 +98,9 @@ class HassioHostInfo extends LitElement {
|
||||
Operating System
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hostInfo.operating_system}
|
||||
${this.supervisor.host.operating_system}
|
||||
</span>
|
||||
${this.hostInfo.features.includes("hassos") &&
|
||||
this.hassOsInfo.update_available
|
||||
${this.supervisor.os.update_available
|
||||
? html`
|
||||
<ha-progress-button
|
||||
title="Update the host OS"
|
||||
@@ -120,29 +111,29 @@ class HassioHostInfo extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
${!this.hostInfo.features.includes("hassos")
|
||||
${!this.supervisor.host.features.includes("hassos")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Docker version
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hassioInfo.docker}
|
||||
${this.supervisor.info.docker}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${this.hostInfo.deployment
|
||||
${this.supervisor.host.deployment
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Deployment
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hostInfo.deployment}
|
||||
${this.supervisor.host.deployment}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${this.hostInfo.features.includes("reboot")
|
||||
${this.supervisor.host.features.includes("reboot")
|
||||
? html`
|
||||
<ha-progress-button
|
||||
title="Reboot the host OS"
|
||||
@@ -153,7 +144,7 @@ class HassioHostInfo extends LitElement {
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
${this.hostInfo.features.includes("shutdown")
|
||||
${this.supervisor.host.features.includes("shutdown")
|
||||
? html`
|
||||
<ha-progress-button
|
||||
title="Shutdown the host OS"
|
||||
@@ -175,7 +166,7 @@ class HassioHostInfo extends LitElement {
|
||||
<mwc-list-item title="Show a list of hardware">
|
||||
Hardware
|
||||
</mwc-list-item>
|
||||
${this.hostInfo.features.includes("hassos")
|
||||
${this.supervisor.host.features.includes("hassos")
|
||||
? html`<mwc-list-item
|
||||
title="Load HassOS configs or updates from USB"
|
||||
>
|
||||
@@ -193,12 +184,10 @@ class HassioHostInfo extends LitElement {
|
||||
}
|
||||
|
||||
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
|
||||
if (!network_info) {
|
||||
if (!network_info || !network_info.interfaces) {
|
||||
return "";
|
||||
}
|
||||
return Object.keys(network_info?.interfaces)
|
||||
.map((device) => network_info.interfaces[device])
|
||||
.find((device) => device.primary)?.ip_address;
|
||||
return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0];
|
||||
});
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
@@ -316,13 +305,13 @@ class HassioHostInfo extends LitElement {
|
||||
|
||||
private async _changeNetworkClicked(): Promise<void> {
|
||||
showNetworkDialog(this, {
|
||||
network: this._networkInfo!,
|
||||
network: this.supervisor.network!,
|
||||
loadData: () => this._loadData(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _changeHostnameClicked(): Promise<void> {
|
||||
const curHostname: string = this.hostInfo.hostname;
|
||||
const curHostname: string = this.supervisor.host.hostname;
|
||||
const hostname = await showPromptDialog(this, {
|
||||
title: "Change Hostname",
|
||||
inputLabel: "Please enter a new hostname:",
|
||||
@@ -333,7 +322,8 @@ class HassioHostInfo extends LitElement {
|
||||
if (hostname && hostname !== curHostname) {
|
||||
try {
|
||||
await changeHostOptions(this.hass, { hostname });
|
||||
this.hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
const host = await fetchHassioHostInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { host });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Setting hostname failed",
|
||||
@@ -346,7 +336,8 @@ class HassioHostInfo extends LitElement {
|
||||
private async _importFromUSB(): Promise<void> {
|
||||
try {
|
||||
await configSyncOS(this.hass);
|
||||
this.hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
const host = await fetchHassioHostInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { host });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to import from USB",
|
||||
@@ -356,7 +347,8 @@ class HassioHostInfo extends LitElement {
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._networkInfo = await fetchNetworkInfo(this.hass);
|
||||
const network = await fetchNetworkInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { network });
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@@ -13,16 +13,15 @@ import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-switch";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
|
||||
import { fetchHassioResolution } from "../../../src/data/hassio/resolution";
|
||||
import {
|
||||
fetchHassioSupervisorInfo,
|
||||
HassioSupervisorInfo as HassioSupervisorInfoType,
|
||||
reloadSupervisor,
|
||||
restartSupervisor,
|
||||
setSupervisorOption,
|
||||
SupervisorOptions,
|
||||
updateSupervisor,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -32,7 +31,7 @@ import { HomeAssistant } from "../../../src/types";
|
||||
import { documentationUrl } from "../../../src/util/documentation-url";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
const ISSUES = {
|
||||
const UNSUPPORTED_REASON = {
|
||||
container: {
|
||||
title: "Containers known to cause issues",
|
||||
url: "/more-info/unsupported/container",
|
||||
@@ -46,6 +45,10 @@ const ISSUES = {
|
||||
title: "Docker Version",
|
||||
url: "/more-info/unsupported/docker_version",
|
||||
},
|
||||
job_conditions: {
|
||||
title: "Ignored job conditions",
|
||||
url: "/more-info/unsupported/job_conditions",
|
||||
},
|
||||
lxc: { title: "LXC", url: "/more-info/unsupported/lxc" },
|
||||
network_manager: {
|
||||
title: "Network Manager",
|
||||
@@ -59,14 +62,30 @@ const ISSUES = {
|
||||
systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" },
|
||||
};
|
||||
|
||||
const UNHEALTHY_REASON = {
|
||||
privileged: {
|
||||
title: "Supervisor is not privileged",
|
||||
url: "/more-info/unsupported/privileged",
|
||||
},
|
||||
supervisor: {
|
||||
title: "Supervisor was not able to update",
|
||||
url: "/more-info/unhealthy/supervisor",
|
||||
},
|
||||
setup: {
|
||||
title: "Setup of the Supervisor failed",
|
||||
url: "/more-info/unhealthy/setup",
|
||||
},
|
||||
docker: {
|
||||
title: "The Docker environment is not working properly",
|
||||
url: "/more-info/unhealthy/docker",
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public supervisorInfo!: HassioSupervisorInfoType;
|
||||
|
||||
@property() public hostInfo!: HassioHostInfoType;
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
@@ -77,7 +96,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
Version
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisorInfo.version}
|
||||
${this.supervisor.supervisor.version}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
@@ -85,9 +104,9 @@ class HassioSupervisorInfo extends LitElement {
|
||||
Newest Version
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisorInfo.version_latest}
|
||||
${this.supervisor.supervisor.version_latest}
|
||||
</span>
|
||||
${this.supervisorInfo.update_available
|
||||
${this.supervisor.supervisor.update_available
|
||||
? html`
|
||||
<ha-progress-button
|
||||
title="Update the supervisor"
|
||||
@@ -103,9 +122,9 @@ class HassioSupervisorInfo extends LitElement {
|
||||
Channel
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisorInfo.channel}
|
||||
${this.supervisor.supervisor.channel}
|
||||
</span>
|
||||
${this.supervisorInfo.channel === "beta"
|
||||
${this.supervisor.supervisor.channel === "beta"
|
||||
? html`
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
@@ -114,7 +133,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
Leave beta channel
|
||||
</ha-progress-button>
|
||||
`
|
||||
: this.supervisorInfo.channel === "stable"
|
||||
: this.supervisor.supervisor.channel === "stable"
|
||||
? html`
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
@@ -126,7 +145,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
|
||||
${this.supervisorInfo?.supported
|
||||
${this.supervisor.supervisor.supported
|
||||
? html` <ha-settings-row three-line>
|
||||
<span slot="heading">
|
||||
Share Diagnostics
|
||||
@@ -143,7 +162,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</div>
|
||||
<ha-switch
|
||||
haptic
|
||||
.checked=${this.supervisorInfo.diagnostics}
|
||||
.checked=${this.supervisor.supervisor.diagnostics}
|
||||
@change=${this._toggleDiagnostics}
|
||||
></ha-switch>
|
||||
</ha-settings-row>`
|
||||
@@ -157,14 +176,33 @@ class HassioSupervisorInfo extends LitElement {
|
||||
Learn more
|
||||
</button>
|
||||
</div>`}
|
||||
${!this.supervisor.supervisor.healthy
|
||||
? html`<div class="error">
|
||||
Your installtion is running in an unhealthy state.
|
||||
<button
|
||||
class="link"
|
||||
title="Learn more about why your system is marked as unhealthy"
|
||||
@click=${this._unhealthyDialog}
|
||||
>
|
||||
Learn more
|
||||
</button>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
@click=${this._supervisorReload}
|
||||
title="Reload parts of the supervisor"
|
||||
title="Reload parts of the Supervisor"
|
||||
>
|
||||
Reload
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
class="warning"
|
||||
@click=${this._supervisorRestart}
|
||||
title="Restart the Supervisor"
|
||||
>
|
||||
Restart
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -174,7 +212,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
if (this.supervisorInfo.channel === "stable") {
|
||||
if (this.supervisor.supervisor.channel === "stable") {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "WARNING",
|
||||
text: html` Beta releases are for testers and early adopters and can
|
||||
@@ -203,18 +241,19 @@ class HassioSupervisorInfo extends LitElement {
|
||||
|
||||
try {
|
||||
const data: Partial<SupervisorOptions> = {
|
||||
channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable",
|
||||
channel:
|
||||
this.supervisor.supervisor.channel === "stable" ? "beta" : "stable",
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await reloadSupervisor(this.hass);
|
||||
fireEvent(this, "hass-api-called", { success: true, response: null });
|
||||
await this._reloadSupervisor();
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to set supervisor option",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _supervisorReload(ev: CustomEvent): Promise<void> {
|
||||
@@ -222,15 +261,37 @@ class HassioSupervisorInfo extends LitElement {
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await reloadSupervisor(this.hass);
|
||||
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
await this._reloadSupervisor();
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to reload the supervisor",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _reloadSupervisor(): Promise<void> {
|
||||
await reloadSupervisor(this.hass);
|
||||
const supervisor = await fetchHassioSupervisorInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { supervisor });
|
||||
}
|
||||
|
||||
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await restartSupervisor(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to restart the supervisor",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
|
||||
@@ -239,7 +300,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Update Supervisor",
|
||||
text: `Are you sure you want to update supervisor to version ${this.supervisorInfo.version_latest}?`,
|
||||
text: `Are you sure you want to update supervisor to version ${this.supervisor.supervisor.version_latest}?`,
|
||||
confirmText: "update",
|
||||
dismissText: "cancel",
|
||||
});
|
||||
@@ -256,8 +317,9 @@ class HassioSupervisorInfo extends LitElement {
|
||||
title: "Failed to update the supervisor",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _diagnosticsInformationDialog(): Promise<void> {
|
||||
@@ -276,22 +338,53 @@ class HassioSupervisorInfo extends LitElement {
|
||||
}
|
||||
|
||||
private async _unsupportedDialog(): Promise<void> {
|
||||
const resolution = await fetchHassioResolution(this.hass);
|
||||
await showAlertDialog(this, {
|
||||
title: "You are running an unsupported installation",
|
||||
text: html`Below is a list of issues found with your installation, click
|
||||
on the links to learn how you can resolve the issues. <br /><br />
|
||||
<ul>
|
||||
${resolution.unsupported.map(
|
||||
${this.supervisor.resolution.unsupported.map(
|
||||
(issue) => html`
|
||||
<li>
|
||||
${ISSUES[issue]
|
||||
${UNSUPPORTED_REASON[issue]
|
||||
? html`<a
|
||||
href="${documentationUrl(this.hass, ISSUES[issue].url)}"
|
||||
href="${documentationUrl(
|
||||
this.hass,
|
||||
UNSUPPORTED_REASON[issue].url
|
||||
)}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${ISSUES[issue].title}
|
||||
${UNSUPPORTED_REASON[issue].title}
|
||||
</a>`
|
||||
: issue}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _unhealthyDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: "Your installation is unhealthy",
|
||||
text: html`Running an unhealthy installation will cause issues. Below is a
|
||||
list of issues found with your installation, click on the links to learn
|
||||
how you can resolve the issues. <br /><br />
|
||||
<ul>
|
||||
${this.supervisor.resolution.unhealthy.map(
|
||||
(issue) => html`
|
||||
<li>
|
||||
${UNHEALTHY_REASON[issue]
|
||||
? html`<a
|
||||
href="${documentationUrl(
|
||||
this.hass,
|
||||
UNHEALTHY_REASON[issue].url
|
||||
)}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${UNHEALTHY_REASON[issue].title}
|
||||
</a>`
|
||||
: issue}
|
||||
</li>
|
||||
@@ -304,7 +397,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
private async _toggleDiagnostics(): Promise<void> {
|
||||
try {
|
||||
const data: SupervisorOptions = {
|
||||
diagnostics: !this.supervisorInfo?.diagnostics,
|
||||
diagnostics: !this.supervisor.supervisor?.diagnostics,
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
} catch (err) {
|
||||
|
@@ -19,6 +19,7 @@ import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import { fetchHassioStats, HassioStats } from "../../../src/data/hassio/common";
|
||||
import { HassioHostInfo } from "../../../src/data/hassio/host";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
@@ -32,7 +33,7 @@ import { hassioStyle } from "../resources/hassio-style";
|
||||
class HassioSystemMetrics extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public hostInfo!: HassioHostInfo;
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@internalProperty() private _supervisorMetrics?: HassioStats;
|
||||
|
||||
@@ -64,8 +65,8 @@ class HassioSystemMetrics extends LitElement {
|
||||
},
|
||||
{
|
||||
description: "Used Space",
|
||||
value: this._getUsedSpace(this.hostInfo),
|
||||
tooltip: `${this.hostInfo.disk_used} GB/${this.hostInfo.disk_total} GB`,
|
||||
value: this._getUsedSpace(this.supervisor.host),
|
||||
tooltip: `${this.supervisor.host.disk_used} GB/${this.supervisor.host.disk_total} GB`,
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -7,14 +7,7 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import {
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
} from "../../../src/data/hassio/host";
|
||||
import {
|
||||
HassioInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
@@ -29,18 +22,12 @@ import "./hassio-system-metrics";
|
||||
class HassioSystem extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property() public supervisorInfo!: HassioSupervisorInfo;
|
||||
|
||||
@property({ attribute: false }) public hassioInfo!: HassioInfo;
|
||||
|
||||
@property() public hostInfo!: HassioHostInfo;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@@ -56,18 +43,15 @@ class HassioSystem extends LitElement {
|
||||
<div class="card-group">
|
||||
<hassio-supervisor-info
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-supervisor-info>
|
||||
<hassio-host-info
|
||||
.hass=${this.hass}
|
||||
.hassioInfo=${this.hassioInfo}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-host-info>
|
||||
<hassio-system-metrics
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-system-metrics>
|
||||
</div>
|
||||
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
|
||||
|
13
package.json
13
package.json
@@ -83,6 +83,9 @@
|
||||
"@types/sortablejs": "^1.10.6",
|
||||
"@vaadin/vaadin-combo-box": "^5.0.10",
|
||||
"@vaadin/vaadin-date-picker": "^4.0.7",
|
||||
"@vibrant/color": "^3.2.1-alpha.1",
|
||||
"@vibrant/core": "^3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||
"@vue/web-component-wrapper": "^1.2.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||
"chart.js": "~2.8.0",
|
||||
@@ -109,7 +112,7 @@
|
||||
"marked": "^1.1.1",
|
||||
"mdn-polyfills": "^5.16.0",
|
||||
"memoize-one": "^5.0.2",
|
||||
"node-vibrant": "^3.1.6",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "^0.3.1",
|
||||
"punycode": "^2.1.1",
|
||||
"qrcode": "^1.4.4",
|
||||
@@ -120,6 +123,8 @@
|
||||
"superstruct": "^0.10.12",
|
||||
"tinykeys": "^1.1.1",
|
||||
"unfetch": "^4.1.0",
|
||||
"vis-data": "^7.1.1",
|
||||
"vis-network": "^8.5.4",
|
||||
"vue": "^2.6.11",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
"web-animations-js": "^2.3.2",
|
||||
@@ -141,6 +146,9 @@
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@koa/cors": "^3.1.0",
|
||||
"@open-wc/dev-server-hmr": "^0.0.2",
|
||||
"@rollup/plugin-babel": "^5.2.1",
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
"@rollup/plugin-json": "^4.0.3",
|
||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||
@@ -159,6 +167,8 @@
|
||||
"@types/webspeechapi": "^0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
||||
"@typescript-eslint/parser": "^4.4.0",
|
||||
"@web/dev-server": "^0.0.24",
|
||||
"@web/dev-server-rollup": "^0.2.11",
|
||||
"babel-loader": "^8.1.0",
|
||||
"chai": "^4.2.0",
|
||||
"cpx": "^1.5.0",
|
||||
@@ -195,7 +205,6 @@
|
||||
"raw-loader": "^2.0.0",
|
||||
"require-dir": "^1.2.0",
|
||||
"rollup": "^2.8.2",
|
||||
"rollup-plugin-babel": "^4.4.0",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"rollup-plugin-visualizer": "^4.0.4",
|
||||
|
40
polymer.json
40
polymer.json
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"entrypoint": "index.html",
|
||||
"shell": "src/entrypoints/app.js",
|
||||
"fragments": [
|
||||
"src/panels/config/ha-panel-config.js",
|
||||
"src/panels/dev-event/ha-panel-dev-event.js",
|
||||
"src/panels/dev-info/ha-panel-dev-info.js",
|
||||
"src/panels/dev-mqtt/ha-panel-dev-mqtt.js",
|
||||
"src/panels/dev-service/ha-panel-dev-service.js",
|
||||
"src/panels/dev-state/ha-panel-dev-state.js",
|
||||
"src/panels/dev-template/ha-panel-dev-template.js",
|
||||
"src/panels/history/ha-panel-history.js",
|
||||
"src/panels/iframe/ha-panel-iframe.js",
|
||||
"src/panels/logbook/ha-panel-logbook.js",
|
||||
"src/panels/map/ha-panel-map.js",
|
||||
"src/panels/mailbox/ha-panel-mailbox.js",
|
||||
"hassio/src/entrypoint.js"
|
||||
],
|
||||
"sources": ["src/**/*", "!src/translations/*"],
|
||||
"lint": {
|
||||
"rules": ["polymer-3"],
|
||||
"ignoreWarnings": ["could-not-resolve-reference", "could-not-load"],
|
||||
"filesToIgnore": [
|
||||
"**/*.html",
|
||||
"**/src/panels/config/js/**/*.js",
|
||||
"**/ha-iconset-svg.js",
|
||||
"**/ha-script-editor.js",
|
||||
"**/ha-automation-editor.js",
|
||||
"**/ha-big-calendar.js"
|
||||
]
|
||||
},
|
||||
"builds": [
|
||||
{
|
||||
"preset": "es5-bundled"
|
||||
},
|
||||
{
|
||||
"preset": "es6-bundled"
|
||||
}
|
||||
]
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 30 KiB |
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20201111.0",
|
||||
version="20201212.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -18,7 +18,7 @@ import "./ha-auth-flow";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import punycode from "punycode";
|
||||
|
||||
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
|
||||
import("./ha-pick-auth-provider");
|
||||
|
||||
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
@property() public clientId?: string;
|
||||
|
@@ -22,3 +22,8 @@ export const rgbContrast = (
|
||||
|
||||
return (lum2 + 0.05) / (lum1 + 0.05);
|
||||
};
|
||||
|
||||
export const getRGBContrastRatio = (
|
||||
rgb1: [number, number, number],
|
||||
rgb2: [number, number, number]
|
||||
) => Math.round((rgbContrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Theme } from "../../data/ws-themes";
|
||||
import { darkStyles, derivedStyles } from "../../resources/styles";
|
||||
import { HomeAssistant, Theme } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
hex2rgb,
|
||||
lab2hex,
|
||||
@@ -13,10 +14,10 @@ import { rgbContrast } from "../color/rgb";
|
||||
|
||||
interface ProcessedTheme {
|
||||
keys: { [key: string]: "" };
|
||||
styles: { [key: string]: string };
|
||||
styles: Record<string, string>;
|
||||
}
|
||||
|
||||
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
|
||||
let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
|
||||
|
||||
/**
|
||||
* Apply a theme to an element by setting the CSS variables on it.
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { directive, NodePart, Part } from "lit-html";
|
||||
|
||||
export const dynamicElement = directive(
|
||||
(tag: string, properties?: { [key: string]: any }) => (part: Part): void => {
|
||||
(tag: string, properties?: Record<string, any>) => (part: Part): void => {
|
||||
if (!(part instanceof NodePart)) {
|
||||
throw new Error(
|
||||
"dynamicElementDirective can only be used in content bindings"
|
||||
|
@@ -13,13 +13,12 @@ export const setupLeafletMap = async (
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
const Leaflet = ((await import(
|
||||
/* webpackChunkName: "leaflet" */ "leaflet"
|
||||
)) as any).default as LeafletModuleType;
|
||||
const Leaflet = ((await import("leaflet")) as any)
|
||||
.default as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||
|
||||
if (draw) {
|
||||
await import(/* webpackChunkName: "leaflet-draw" */ "leaflet-draw");
|
||||
await import("leaflet-draw");
|
||||
}
|
||||
|
||||
const map = Leaflet.map(mapElement);
|
||||
|
6
src/common/ensure-array.ts
Normal file
6
src/common/ensure-array.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const ensureArray = (value?: any) => {
|
||||
if (!value || Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return [value];
|
||||
};
|
@@ -5,7 +5,7 @@ import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { numberFormat } from "../string/number-format";
|
||||
import { formatNumber } from "../string/format_number";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -20,7 +20,7 @@ export const computeStateDisplay = (
|
||||
}
|
||||
|
||||
if (stateObj.attributes.unit_of_measurement) {
|
||||
return `${numberFormat(compareState, language)} ${
|
||||
return `${formatNumber(compareState, language)} ${
|
||||
stateObj.attributes.unit_of_measurement
|
||||
}`;
|
||||
}
|
||||
@@ -67,6 +67,10 @@ export const computeStateDisplay = (
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "counter") {
|
||||
return formatNumber(compareState, language);
|
||||
}
|
||||
|
||||
return (
|
||||
// Return device class translation
|
||||
(stateObj.attributes.device_class &&
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { GroupEntity } from "../../types";
|
||||
import type { GroupEntity } from "../../data/group";
|
||||
import { DEFAULT_VIEW_ENTITY_ID } from "../const";
|
||||
|
||||
// Return an ordered array of available views
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { GroupEntity } from "../../types";
|
||||
import { GroupEntity } from "../../data/group";
|
||||
|
||||
export const getGroupEntities = (
|
||||
entities: HassEntities,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { GroupEntity } from "../../types";
|
||||
import { GroupEntity } from "../../data/group";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { getGroupEntities } from "./get_group_entities";
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { GroupEntity } from "../../types";
|
||||
import { GroupEntity } from "../../data/group";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
// Split a collection into a list of groups and a 'rest' list of ungrouped
|
||||
|
132
src/common/image/extract_color.ts
Normal file
132
src/common/image/extract_color.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import Vibrant from "node-vibrant/lib/browser";
|
||||
import MMCQ from "@vibrant/quantizer-mmcq";
|
||||
import { BasicPipeline } from "@vibrant/core/lib/pipeline";
|
||||
import { Swatch, Vec3 } from "@vibrant/color";
|
||||
import { getRGBContrastRatio } from "../color/rgb";
|
||||
|
||||
const CONTRAST_RATIO = 4.5;
|
||||
|
||||
// How much the total diff between 2 RGB colors can be
|
||||
// to be considered similar.
|
||||
const COLOR_SIMILARITY_THRESHOLD = 150;
|
||||
|
||||
// For debug purposes, is being tree shaken.
|
||||
const DEBUG_COLOR = __DEV__ && false;
|
||||
|
||||
const logColor = (
|
||||
color: Swatch,
|
||||
label = `${color.hex} - ${color.population}`
|
||||
) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`%c${label}`,
|
||||
`color: ${color.bodyTextColor}; background-color: ${color.hex}`
|
||||
);
|
||||
|
||||
const customGenerator = (colors: Swatch[]) => {
|
||||
colors.sort((colorA, colorB) => colorB.population - colorA.population);
|
||||
|
||||
const backgroundColor = colors[0];
|
||||
let foregroundColor: Vec3 | undefined;
|
||||
|
||||
const contrastRatios = new Map<string, number>();
|
||||
const approvedContrastRatio = (hex: string, rgb: Swatch["rgb"]) => {
|
||||
if (!contrastRatios.has(hex)) {
|
||||
contrastRatios.set(hex, getRGBContrastRatio(backgroundColor.rgb, rgb));
|
||||
}
|
||||
|
||||
return contrastRatios.get(hex)! > CONTRAST_RATIO;
|
||||
};
|
||||
|
||||
// We take each next color and find one that has better contrast.
|
||||
for (let i = 1; i < colors.length && foregroundColor === undefined; i++) {
|
||||
// If this color matches, score, take it.
|
||||
if (approvedContrastRatio(colors[i].hex, colors[i].rgb)) {
|
||||
if (DEBUG_COLOR) {
|
||||
logColor(colors[i], "PICKED");
|
||||
}
|
||||
foregroundColor = colors[i].rgb;
|
||||
break;
|
||||
}
|
||||
|
||||
// This color has the wrong contrast ratio, but it is the right color.
|
||||
// Let's find similar colors that might have the right contrast ratio
|
||||
|
||||
const currentColor = colors[i];
|
||||
if (DEBUG_COLOR) {
|
||||
logColor(colors[i], "Finding similar color with better contrast");
|
||||
}
|
||||
|
||||
for (let j = i + 1; j < colors.length; j++) {
|
||||
const compareColor = colors[j];
|
||||
|
||||
// difference. 0 is same, 765 max difference
|
||||
const diffScore =
|
||||
Math.abs(currentColor.rgb[0] - compareColor.rgb[0]) +
|
||||
Math.abs(currentColor.rgb[1] - compareColor.rgb[1]) +
|
||||
Math.abs(currentColor.rgb[2] - compareColor.rgb[2]);
|
||||
|
||||
if (DEBUG_COLOR) {
|
||||
logColor(colors[j], `${colors[j].hex} - ${diffScore}`);
|
||||
}
|
||||
|
||||
if (diffScore > COLOR_SIMILARITY_THRESHOLD) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (approvedContrastRatio(compareColor.hex, compareColor.rgb)) {
|
||||
if (DEBUG_COLOR) {
|
||||
logColor(compareColor, "PICKED");
|
||||
}
|
||||
foregroundColor = compareColor.rgb;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foregroundColor === undefined) {
|
||||
foregroundColor =
|
||||
// @ts-expect-error
|
||||
backgroundColor.getYiq() < 200 ? [255, 255, 255] : [0, 0, 0];
|
||||
}
|
||||
|
||||
if (DEBUG_COLOR) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
"%cPicked colors",
|
||||
`color: ${foregroundColor}; background-color: ${backgroundColor.hex}; font-weight: bold; padding: 16px;`
|
||||
);
|
||||
colors.forEach((color) => logColor(color));
|
||||
// eslint-disable-next-line no-console
|
||||
console.log();
|
||||
}
|
||||
|
||||
return {
|
||||
foreground: new Swatch(foregroundColor, 0),
|
||||
background: backgroundColor,
|
||||
};
|
||||
};
|
||||
|
||||
Vibrant.use(
|
||||
new BasicPipeline().filter
|
||||
.register(
|
||||
"default",
|
||||
(r: number, g: number, b: number, a: number) =>
|
||||
a >= 125 && !(r > 250 && g > 250 && b > 250)
|
||||
)
|
||||
.quantizer.register("mmcq", MMCQ)
|
||||
// Our generator has different output
|
||||
// @ts-expect-error
|
||||
.generator.register("default", customGenerator)
|
||||
);
|
||||
|
||||
export const extractColors = (url: string, downsampleColors = 16) =>
|
||||
new Vibrant(url, {
|
||||
colorCount: downsampleColors,
|
||||
})
|
||||
.getPalette()
|
||||
.then(({ foreground, background }) => {
|
||||
return { background: background!, foreground: foreground! };
|
||||
});
|
54
src/common/string/format_number.ts
Normal file
54
src/common/string/format_number.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
|
||||
*
|
||||
* @param num The number to format
|
||||
* @param language The language to use when formatting the number
|
||||
*/
|
||||
export const formatNumber = (
|
||||
num: string | number,
|
||||
language: string,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string => {
|
||||
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
|
||||
Number.isNaN =
|
||||
Number.isNaN ||
|
||||
function isNaN(input) {
|
||||
return typeof input === "number" && isNaN(input);
|
||||
};
|
||||
|
||||
if (!Number.isNaN(Number(num)) && Intl) {
|
||||
return new Intl.NumberFormat(
|
||||
language,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).format(Number(num));
|
||||
}
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates default options for Intl.NumberFormat
|
||||
* @param num The number to be formatted
|
||||
* @param options The Intl.NumberFormatOptions that should be included in the returned options
|
||||
*/
|
||||
const getDefaultFormatOptions = (
|
||||
num: string | number,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): Intl.NumberFormatOptions => {
|
||||
const defaultOptions: Intl.NumberFormatOptions = options || {};
|
||||
|
||||
if (typeof num !== "string") {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
// Keep decimal trailing zeros if they are present in a string numeric value
|
||||
if (
|
||||
!options ||
|
||||
(!options.minimumFractionDigits && !options.maximumFractionDigits)
|
||||
) {
|
||||
const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0;
|
||||
defaultOptions.minimumFractionDigits = digits;
|
||||
defaultOptions.maximumFractionDigits = digits;
|
||||
}
|
||||
|
||||
return defaultOptions;
|
||||
};
|
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
|
||||
*
|
||||
* @param num The number to format
|
||||
* @param language The language to use when formatting the number
|
||||
*/
|
||||
export const numberFormat = (
|
||||
num: string | number,
|
||||
language: string
|
||||
): string => {
|
||||
// Polyfill for Number.isNaN, which is more reliable that the global isNaN()
|
||||
Number.isNaN =
|
||||
Number.isNaN ||
|
||||
function isNaN(input) {
|
||||
return typeof input === "number" && isNaN(input);
|
||||
};
|
||||
|
||||
if (!Number.isNaN(Number(num)) && Intl) {
|
||||
return new Intl.NumberFormat(language).format(Number(num));
|
||||
}
|
||||
return num.toString();
|
||||
};
|
@@ -102,7 +102,7 @@ export const computeLocalize = async (
|
||||
export const localizeKey = (
|
||||
localize: LocalizeFunc,
|
||||
key: string,
|
||||
placeholders?: { [key: string]: string }
|
||||
placeholders?: Record<string, string>
|
||||
) => {
|
||||
const args: [string, ...string[]] = [key];
|
||||
if (placeholders) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
export const extractSearchParamsObject = (): { [key: string]: string } => {
|
||||
export const extractSearchParamsObject = (): Record<string, string> => {
|
||||
const query = {};
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
for (const [key, value] of searchParams.entries()) {
|
||||
|
@@ -1,8 +1,12 @@
|
||||
export const copyToClipboard = (str) => {
|
||||
const el = document.createElement("textarea");
|
||||
el.value = str;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(el);
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(str);
|
||||
} else {
|
||||
const el = document.createElement("textarea");
|
||||
el.value = str;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
};
|
||||
|
@@ -98,6 +98,12 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public hasFab = false;
|
||||
|
||||
/**
|
||||
* Add an extra rows at the bottom of the datatabel
|
||||
* @type {TemplateResult}
|
||||
*/
|
||||
@property({ attribute: false }) public appendRow?;
|
||||
|
||||
@property({ type: Boolean, attribute: "auto-height" })
|
||||
public autoHeight = false;
|
||||
|
||||
@@ -126,6 +132,8 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@query("slot[name='header']") private _header!: HTMLSlotElement;
|
||||
|
||||
private _items: DataTableRowData[] = [];
|
||||
|
||||
private _checkableRowsCount?: number;
|
||||
|
||||
private _checkedRows: string[] = [];
|
||||
@@ -318,10 +326,13 @@ export class HaDataTable extends LitElement {
|
||||
@scroll=${this._saveScrollPos}
|
||||
>
|
||||
${scroll({
|
||||
items: !this.hasFab
|
||||
? this._filteredData
|
||||
: [...this._filteredData, ...[{ empty: true }]],
|
||||
items: this._items,
|
||||
renderItem: (row: DataTableRowData, index) => {
|
||||
if (row.append) {
|
||||
return html`
|
||||
<div class="mdc-data-table__row">${row.content}</div>
|
||||
`;
|
||||
}
|
||||
if (row.empty) {
|
||||
return html` <div class="mdc-data-table__row"></div> `;
|
||||
}
|
||||
@@ -447,6 +458,20 @@ export class HaDataTable extends LitElement {
|
||||
if (this.curRequest !== curRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.appendRow || this.hasFab) {
|
||||
this._items = [...data];
|
||||
|
||||
if (this.appendRow) {
|
||||
this._items.push({ append: true, content: this.appendRow });
|
||||
}
|
||||
|
||||
if (this.hasFab) {
|
||||
this._items.push({ empty: true });
|
||||
}
|
||||
} else {
|
||||
this._items = data;
|
||||
}
|
||||
this._filteredData = data;
|
||||
}
|
||||
|
||||
|
@@ -139,7 +139,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _filteredDevices: DeviceRegistryEntry[] = [];
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
private _getAreasWithDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
@@ -277,7 +277,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
const areas = this._getDevices(
|
||||
const areas = this._getAreasWithDevices(
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._entities,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -35,6 +38,7 @@ import {
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { mdiClose, mdiMenuUp, mdiMenuDown } from "@mdi/js";
|
||||
|
||||
interface Device {
|
||||
name: string;
|
||||
@@ -42,6 +46,10 @@ interface Device {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type HaDevicePickerDeviceFilterFunc = (
|
||||
device: DeviceRegistryEntry
|
||||
) => boolean;
|
||||
|
||||
const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
@@ -102,9 +110,15 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ type: Boolean })
|
||||
private _opened?: boolean;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||
|
||||
private _init = false;
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
@@ -112,21 +126,31 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
entities: EntityRegistryEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"]
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"]
|
||||
): Device[] => {
|
||||
if (!devices.length) {
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
id: "",
|
||||
area: "",
|
||||
name: this.hass.localize("ui.components.device-picker.no_devices"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
|
||||
if (includeDomains || excludeDomains || includeDeviceClasses) {
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
@@ -134,7 +158,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
let inputDevices = [...devices];
|
||||
let inputDevices = devices.filter(
|
||||
(device) => device.id === this.value || !device.disabled_by
|
||||
);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
@@ -180,6 +206,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices.filter(
|
||||
(device) =>
|
||||
// We always want to include the device of the current value
|
||||
device.id === this.value || deviceFilter!(device)
|
||||
);
|
||||
}
|
||||
|
||||
const outputDevices = inputDevices.map((device) => {
|
||||
return {
|
||||
id: device.id,
|
||||
@@ -193,6 +227,15 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
};
|
||||
});
|
||||
if (!outputDevices.length) {
|
||||
return [
|
||||
{
|
||||
id: "",
|
||||
area: "",
|
||||
name: this.hass.localize("ui.components.device-picker.no_match"),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (outputDevices.length === 1) {
|
||||
return outputDevices;
|
||||
}
|
||||
@@ -200,6 +243,18 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
);
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot?.querySelector("paper-input")?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
@@ -214,24 +269,33 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.devices && this.areas && this.entities) ||
|
||||
(changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
(this._comboBox as any).items = this._getDevices(
|
||||
this.devices!,
|
||||
this.areas!,
|
||||
this.entities!,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.devices || !this.areas || !this.entities) {
|
||||
return html``;
|
||||
}
|
||||
const devices = this._getDevices(
|
||||
this.devices,
|
||||
this.areas,
|
||||
this.entities,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses
|
||||
);
|
||||
return html`
|
||||
<vaadin-combo-box-light
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="name"
|
||||
.items=${devices}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@@ -249,34 +313,30 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
${this.value
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.device-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
Clear
|
||||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${devices.length > 0
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.device-picker.show_devices"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
>
|
||||
Toggle
|
||||
</ha-icon-button>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.device-picker.show_devices"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
`;
|
||||
@@ -313,7 +373,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > ha-icon-button {
|
||||
paper-input > mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
padding: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
|
@@ -230,9 +230,7 @@ class HaChartBase extends mixinBehaviors(
|
||||
}
|
||||
|
||||
if (scriptsLoaded === null) {
|
||||
scriptsLoaded = import(
|
||||
/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js"
|
||||
);
|
||||
scriptsLoaded = import("../../resources/ha-chart-scripts.js");
|
||||
}
|
||||
scriptsLoaded.then((ChartModule) => {
|
||||
this.ChartClass = ChartModule.default;
|
||||
|
@@ -101,6 +101,18 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot?.querySelector("paper-input")?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private _initedStates = false;
|
||||
|
||||
private _states: HassEntity[] = [];
|
||||
@@ -153,6 +165,24 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
if (!states.length) {
|
||||
return [
|
||||
{
|
||||
entity_id: "",
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null },
|
||||
attributes: {
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
),
|
||||
icon: "mdi:magnify",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
);
|
||||
@@ -203,7 +233,6 @@ export class HaEntityPicker extends LitElement {
|
||||
.label=${this.label === undefined
|
||||
? this.hass.localize("ui.components.entity.entity-picker.entity")
|
||||
: this.label}
|
||||
.value=${this._value}
|
||||
.disabled=${this.disabled}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
|
@@ -21,6 +21,7 @@ import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-label-badge";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { formatNumber } from "../../common/string/format_number";
|
||||
|
||||
@customElement("ha-state-label-badge")
|
||||
export class HaStateLabelBadge extends LitElement {
|
||||
@@ -115,7 +116,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
: state.state === UNKNOWN
|
||||
? "-"
|
||||
: state.attributes.unit_of_measurement
|
||||
? state.state
|
||||
? formatNumber(state.state, this.hass!.language)
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
state,
|
||||
|
@@ -94,7 +94,6 @@ class StateInfo extends LitElement {
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
@apply --paper-font-body1;
|
||||
min-width: 120px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -118,9 +117,10 @@ class StateInfo extends LitElement {
|
||||
}
|
||||
|
||||
.name {
|
||||
@apply --paper-font-common-nowrap;
|
||||
color: var(--primary-text-color);
|
||||
line-height: 40px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.name[in-dialog],
|
||||
@@ -131,8 +131,10 @@ class StateInfo extends LitElement {
|
||||
.time-ago,
|
||||
.extra-info,
|
||||
.extra-info > * {
|
||||
@apply --paper-font-common-nowrap;
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.row {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
@@ -14,6 +15,8 @@ import {
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
query,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
@@ -28,6 +31,19 @@ import {
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
DeviceEntityLookup,
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../data/entity_registry";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
@@ -68,31 +84,252 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public _areas?: AreaRegistryEntry[];
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd?: boolean;
|
||||
|
||||
/**
|
||||
* Show only areas with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no areas with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only areas with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||
|
||||
@internalProperty() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@internalProperty() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@internalProperty() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@internalProperty() private _opened?: boolean;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||
|
||||
private _init = false;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = this.noAdd
|
||||
? areas
|
||||
: [
|
||||
...areas,
|
||||
{
|
||||
area_id: "add_new",
|
||||
name: this.hass.localize("ui.components.area-picker.add_new"),
|
||||
},
|
||||
];
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.updateComplete.then(() => {
|
||||
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot?.querySelector("paper-input")?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private _getAreas = memoizeOne(
|
||||
(
|
||||
areas: AreaRegistryEntry[],
|
||||
devices: DeviceRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
noAdd: this["noAdd"]
|
||||
): AreaRegistryEntry[] => {
|
||||
if (!areas.length) {
|
||||
return [
|
||||
{
|
||||
area_id: "",
|
||||
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryEntry[] | undefined;
|
||||
|
||||
if (includeDomains || excludeDomains || includeDeviceClasses) {
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
} else if (deviceFilter) {
|
||||
inputDevices = devices;
|
||||
} else if (entityFilter) {
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
}
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
entityFilter!(entity)
|
||||
);
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
|
||||
}
|
||||
|
||||
if (!outputAreas.length) {
|
||||
outputAreas = [
|
||||
{
|
||||
area_id: "",
|
||||
name: this.hass.localize("ui.components.area-picker.no_match"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return noAdd
|
||||
? outputAreas
|
||||
: [
|
||||
...outputAreas,
|
||||
{
|
||||
area_id: "add_new",
|
||||
name: this.hass.localize("ui.components.area-picker.add_new"),
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this._devices && this._areas && this._entities) ||
|
||||
(changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
(this._comboBox as any).items = this._getAreas(
|
||||
this._areas!,
|
||||
this._devices!,
|
||||
this._entities!,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._areas) {
|
||||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
@@ -100,7 +337,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
item-value-path="area_id"
|
||||
item-id-path="area_id"
|
||||
item-label-path="name"
|
||||
.items=${this._areas}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@@ -110,6 +346,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.area-picker.area")
|
||||
: this.label}
|
||||
.placeholder=${this.placeholder
|
||||
? this._area(this.placeholder)?.name
|
||||
: undefined}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
@@ -118,39 +357,39 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
${this.value
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.area-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
${this.hass.localize("ui.components.area-picker.clear")}
|
||||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${this._areas.length > 0
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.area-picker.show_areas"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
>
|
||||
${this.hass.localize("ui.components.area-picker.toggle")}
|
||||
</ha-icon-button>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize("ui.components.area-picker.toggle")}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
`;
|
||||
}
|
||||
|
||||
private _area = memoizeOne((areaId: string):
|
||||
| AreaRegistryEntry
|
||||
| undefined => {
|
||||
return this._areas?.find((area) => area.area_id === areaId);
|
||||
});
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._setValue("");
|
||||
@@ -215,7 +454,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > ha-icon-button {
|
||||
paper-input > mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
padding: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
|
@@ -107,7 +107,7 @@ class HaAttributes extends LitElement {
|
||||
(!Array.isArray(value) && value instanceof Object)
|
||||
) {
|
||||
if (!jsYamlPromise) {
|
||||
jsYamlPromise = import(/* webpackChunkName: "js-yaml" */ "js-yaml");
|
||||
jsYamlPromise = import("js-yaml");
|
||||
}
|
||||
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
|
||||
return html` <pre>${until(yaml, "")}</pre> `;
|
||||
|
125
src/components/ha-blueprint-picker.ts
Normal file
125
src/components/ha-blueprint-picker.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { compare } from "../common/string/compare";
|
||||
import { Blueprint, Blueprints, fetchBlueprints } from "../data/blueprint";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-blueprint-picker")
|
||||
class HaBluePrintPicker extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value = "";
|
||||
|
||||
@property() public domain = "automation";
|
||||
|
||||
@property() public blueprints?: Blueprints;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => {
|
||||
if (!blueprints) {
|
||||
return [];
|
||||
}
|
||||
const result = Object.entries(blueprints)
|
||||
.filter(([_path, blueprint]) => !("error" in blueprint))
|
||||
.map(([path, blueprint]) => ({
|
||||
...(blueprint as Blueprint).metadata,
|
||||
path,
|
||||
}));
|
||||
return result.sort((a, b) => compare(a.name, b.name));
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<paper-dropdown-menu-light
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.blueprint-picker.label")}
|
||||
.disabled=${this.disabled}
|
||||
horizontal-align="left"
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${this.value}
|
||||
attr-for-selected="data-blueprint-path"
|
||||
@iron-select=${this._blueprintChanged}
|
||||
>
|
||||
<paper-item data-blueprint-path="">
|
||||
${this.hass.localize(
|
||||
"ui.components.blueprint-picker.select_blueprint"
|
||||
)}
|
||||
</paper-item>
|
||||
${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => html`
|
||||
<paper-item data-blueprint-path=${blueprint.path}>
|
||||
${blueprint.name}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu-light>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
if (this.blueprints === undefined) {
|
||||
fetchBlueprints(this.hass!, this.domain).then((blueprints) => {
|
||||
this.blueprints = blueprints;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _blueprintChanged(ev) {
|
||||
const newValue = ev.detail.item.dataset.blueprintPath;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
this.value = ev.detail.value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
paper-dropdown-menu-light {
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
display: block;
|
||||
}
|
||||
paper-listbox {
|
||||
min-width: 200px;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-blueprint-picker": HaBluePrintPicker;
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@ import {
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { ToggleButton } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
@customElement("ha-button-toggle-group")
|
||||
export class HaButtonToggleGroup extends LitElement {
|
||||
@@ -21,17 +22,22 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div>
|
||||
${this.buttons.map(
|
||||
(button) => html`
|
||||
<mwc-icon-button
|
||||
.label=${button.label}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
${this.buttons.map((button) =>
|
||||
button.iconPath
|
||||
? html`<mwc-icon-button
|
||||
.label=${button.label}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: html`<mwc-button
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>${button.label}</mwc-button
|
||||
>`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
@@ -49,13 +55,15 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||
}
|
||||
mwc-icon-button {
|
||||
mwc-icon-button,
|
||||
mwc-button {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-right-width: 0px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
mwc-icon-button::before {
|
||||
mwc-icon-button::before,
|
||||
mwc-button::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@@ -67,17 +75,21 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
content: "";
|
||||
transition: opacity 15ms linear, background-color 15ms linear;
|
||||
}
|
||||
mwc-icon-button[active]::before {
|
||||
mwc-icon-button[active]::before,
|
||||
mwc-button[active]::before {
|
||||
opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
|
||||
}
|
||||
mwc-icon-button:first-child {
|
||||
mwc-icon-button:first-child,
|
||||
mwc-button:first-child {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
mwc-icon-button:last-child {
|
||||
mwc-icon-button:last-child,
|
||||
mwc-button:last-child {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
mwc-icon-button:only-child {
|
||||
mwc-icon-button:only-child,
|
||||
mwc-button:only-child {
|
||||
border-radius: 4px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
@@ -13,11 +13,12 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import {
|
||||
CameraEntity,
|
||||
CAMERA_SUPPORT_STREAM,
|
||||
computeMJPEGStreamUrl,
|
||||
fetchStreamUrl,
|
||||
} from "../data/camera";
|
||||
import { CameraEntity, HomeAssistant } from "../types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-hls-player";
|
||||
|
||||
@customElement("ha-camera-stream")
|
||||
|
@@ -11,6 +11,7 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { formatNumber } from "../common/string/format_number";
|
||||
|
||||
@customElement("ha-climate-state")
|
||||
class HaClimateState extends LitElement {
|
||||
@@ -51,11 +52,17 @@ class HaClimateState extends LitElement {
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_temperature != null) {
|
||||
return `${this.stateObj.attributes.current_temperature} ${this.hass.config.unit_system.temperature}`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_temperature,
|
||||
this.hass!.language
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_humidity != null) {
|
||||
return `${this.stateObj.attributes.current_humidity} %`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_humidity,
|
||||
this.hass!.language
|
||||
)} %`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -70,21 +77,39 @@ class HaClimateState extends LitElement {
|
||||
this.stateObj.attributes.target_temp_low != null &&
|
||||
this.stateObj.attributes.target_temp_high != null
|
||||
) {
|
||||
return `${this.stateObj.attributes.target_temp_low}-${this.stateObj.attributes.target_temp_high} ${this.hass.config.unit_system.temperature}`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.target_temp_low,
|
||||
this.hass!.language
|
||||
)}-${formatNumber(
|
||||
this.stateObj.attributes.target_temp_high,
|
||||
this.hass!.language
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.temperature != null) {
|
||||
return `${this.stateObj.attributes.temperature} ${this.hass.config.unit_system.temperature}`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.temperature,
|
||||
this.hass!.language
|
||||
)} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (
|
||||
this.stateObj.attributes.target_humidity_low != null &&
|
||||
this.stateObj.attributes.target_humidity_high != null
|
||||
) {
|
||||
return `${this.stateObj.attributes.target_humidity_low}-${this.stateObj.attributes.target_humidity_high}%`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.target_humidity_low,
|
||||
this.hass!.language
|
||||
)}-${formatNumber(
|
||||
this.stateObj.attributes.target_humidity_high,
|
||||
this.hass!.language
|
||||
)}%`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
return `${this.stateObj.attributes.humidity} %`;
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.humidity,
|
||||
this.hass!.language
|
||||
)} %`;
|
||||
}
|
||||
|
||||
return "";
|
||||
|
@@ -41,7 +41,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
|
||||
return html` <ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsStop,
|
||||
invisible: !this._entityObj.supportsOpenTilt,
|
||||
})}
|
||||
label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.open_tilt_cover"
|
||||
@@ -61,10 +61,10 @@ class HaCoverTiltControls extends LitElement {
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsStop,
|
||||
invisible: !this._entityObj.supportsCloseTilt,
|
||||
})}
|
||||
label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.open_tilt_cover"
|
||||
"ui.dialogs.more_info_control.close_tilt_cover"
|
||||
)}
|
||||
icon="hass:arrow-bottom-left"
|
||||
@click=${this._onCloseTiltTap}
|
||||
|
@@ -19,12 +19,14 @@ class HaExpansionPanel extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) outlined = false;
|
||||
|
||||
@property() header?: string;
|
||||
|
||||
@query(".container") private _container!: HTMLDivElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="summary" @click=${this._toggleContainer}>
|
||||
<slot name="title"></slot>
|
||||
<slot name="header">${this.header}</slot>
|
||||
<ha-svg-icon
|
||||
.path=${mdiChevronDown}
|
||||
class="summary-icon ${classMap({ expanded: this.expanded })}"
|
||||
@@ -76,7 +78,7 @@ class HaExpansionPanel extends LitElement {
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
padding: 0px 16px;
|
||||
padding: var(--expansion-panel-summary-padding, 0);
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
20
src/components/ha-fab.ts
Normal file
20
src/components/ha-fab.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Fab } from "@material/mwc-fab";
|
||||
import "@material/mwc-fab";
|
||||
import { customElement } from "lit-element";
|
||||
import { Constructor } from "../types";
|
||||
|
||||
const MwcFab = customElements.get("mwc-fab") as Constructor<Fab>;
|
||||
|
||||
@customElement("ha-fab")
|
||||
export class HaFab extends MwcFab {
|
||||
protected firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-fab": HaFab;
|
||||
}
|
||||
}
|
@@ -54,7 +54,7 @@ export interface HaFormSelectSchema extends HaFormBaseSchema {
|
||||
|
||||
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
|
||||
type: "multi_select";
|
||||
options?: { [key: string]: string } | string[] | Array<[string, string]>;
|
||||
options?: Record<string, string> | string[] | Array<[string, string]>;
|
||||
}
|
||||
|
||||
export interface HaFormFloatSchema extends HaFormBaseSchema {
|
||||
|
@@ -12,6 +12,7 @@ import { afterNextRender } from "../common/util/render-status";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
import { getValueInPercentage, normalize } from "../util/calculate";
|
||||
import { formatNumber } from "../common/string/format_number";
|
||||
|
||||
const getAngle = (value: number, min: number, max: number) => {
|
||||
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
|
||||
@@ -29,6 +30,8 @@ export class Gauge extends LitElement {
|
||||
|
||||
@property({ type: Number }) public value = 0;
|
||||
|
||||
@property({ type: String }) public language = "";
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@internalProperty() private _angle = 0;
|
||||
@@ -88,7 +91,7 @@ export class Gauge extends LitElement {
|
||||
</svg>
|
||||
<svg class="text">
|
||||
<text class="value-text">
|
||||
${this.value} ${this.label}
|
||||
${formatNumber(this.value, this.language)} ${this.label}
|
||||
</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
@@ -107,9 +107,7 @@ class HaHLSPlayer extends LitElement {
|
||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
|
||||
const hls = ((await import(
|
||||
/* webpackChunkName: "hls.js" */ "hls.js"
|
||||
)) as any).default as HLSModule;
|
||||
const hls = ((await import("hls.js")) as any).default as HLSModule;
|
||||
let hlsSupported = hls.isSupported();
|
||||
|
||||
if (!hlsSupported) {
|
||||
@@ -129,7 +127,7 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
// Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url
|
||||
// See https://tools.ietf.org/html/rfc8216 for HLS spec details
|
||||
const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(?<isHevc>hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(?<streamUrl>.+)/g;
|
||||
const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(.+)/g;
|
||||
const match = playlistRegexp.exec(masterPlaylist);
|
||||
const matchTwice = playlistRegexp.exec(masterPlaylist);
|
||||
|
||||
@@ -138,17 +136,13 @@ class HaHLSPlayer extends LitElement {
|
||||
let playlist_url: string;
|
||||
if (match !== null && matchTwice === null) {
|
||||
// Only send the regular playlist url if we match exactly once
|
||||
playlist_url = new URL(match.groups!.streamUrl, this.url).href;
|
||||
playlist_url = new URL(match[2], this.url).href;
|
||||
} else {
|
||||
playlist_url = this.url;
|
||||
}
|
||||
|
||||
// If codec is HEVC and ExoPlayer is supported, use ExoPlayer.
|
||||
if (
|
||||
this._useExoPlayer &&
|
||||
match !== null &&
|
||||
match.groups!.isHevc !== undefined
|
||||
) {
|
||||
if (this._useExoPlayer && match !== null && match[1] !== undefined) {
|
||||
this._renderHLSExoPlayer(playlist_url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, playlist_url);
|
||||
|
@@ -60,8 +60,9 @@ export class HaIconInput extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-icon {
|
||||
position: relative;
|
||||
bottom: 4px;
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ checkCacheVersion();
|
||||
|
||||
const debouncedWriteCache = debounce(() => writeCache(chunks), 2000);
|
||||
|
||||
const cachedIcons: { [key: string]: string } = {};
|
||||
const cachedIcons: Record<string, string> = {};
|
||||
|
||||
@customElement("ha-icon")
|
||||
export class HaIcon extends LitElement {
|
||||
|
@@ -58,6 +58,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
||||
getConfigEntries(this.hass).then((configEntries) => {
|
||||
this._entries = configEntries;
|
||||
});
|
||||
this.hass.loadBackendTranslation("title");
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
|
@@ -1,66 +0,0 @@
|
||||
import { dom } from "@polymer/polymer/lib/legacy/polymer.dom";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import relativeTime from "../common/datetime/relative_time";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaRelativeTime extends LocalizeMixin(PolymerElement) {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
datetime: {
|
||||
type: String,
|
||||
observer: "datetimeChanged",
|
||||
},
|
||||
|
||||
datetimeObj: {
|
||||
type: Object,
|
||||
observer: "datetimeObjChanged",
|
||||
},
|
||||
|
||||
parsedDateTime: Object,
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.updateRelative = this.updateRelative.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// update every 60 seconds
|
||||
this.updateInterval = setInterval(this.updateRelative, 60000);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
|
||||
datetimeChanged(newVal) {
|
||||
this.parsedDateTime = newVal ? new Date(newVal) : null;
|
||||
|
||||
this.updateRelative();
|
||||
}
|
||||
|
||||
datetimeObjChanged(newVal) {
|
||||
this.parsedDateTime = newVal;
|
||||
|
||||
this.updateRelative();
|
||||
}
|
||||
|
||||
updateRelative() {
|
||||
const root = dom(this);
|
||||
if (!this.parsedDateTime) {
|
||||
root.innerHTML = this.localize("ui.components.relative_time.never");
|
||||
} else {
|
||||
root.innerHTML = relativeTime(this.parsedDateTime, this.localize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-relative-time", HaRelativeTime);
|
72
src/components/ha-relative-time.ts
Normal file
72
src/components/ha-relative-time.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
customElement,
|
||||
UpdatingElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
|
||||
import relativeTime from "../common/datetime/relative_time";
|
||||
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-relative-time")
|
||||
class HaRelativeTime extends UpdatingElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public datetime?: string | Date;
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._clearInterval();
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.datetime) {
|
||||
this._startInterval();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._updateRelative();
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues) {
|
||||
super.update(changedProps);
|
||||
this._updateRelative();
|
||||
}
|
||||
|
||||
private _clearInterval(): void {
|
||||
if (this._interval) {
|
||||
window.clearInterval(this._interval);
|
||||
this._interval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _startInterval(): void {
|
||||
this._clearInterval();
|
||||
|
||||
// update every 60 seconds
|
||||
this._interval = window.setInterval(() => this._updateRelative(), 60000);
|
||||
}
|
||||
|
||||
private _updateRelative(): void {
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
|
||||
} else {
|
||||
this.innerHTML = relativeTime(
|
||||
new Date(this.datetime),
|
||||
this.hass.localize
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-relative-time": HaRelativeTime;
|
||||
}
|
||||
}
|
45
src/components/ha-selector/ha-selector-action.ts
Normal file
45
src/components/ha-selector/ha-selector-action.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { ActionSelector } from "../../data/selector";
|
||||
import { Action } from "../../data/script";
|
||||
import "../../panels/config/automation/action/ha-automation-action";
|
||||
|
||||
@customElement("ha-selector-action")
|
||||
export class HaActionSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: ActionSelector;
|
||||
|
||||
@property() public value?: Action;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-automation-action
|
||||
.actions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-automation-action {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-action": HaActionSelector;
|
||||
}
|
||||
}
|
102
src/components/ha-selector/ha-selector-area.ts
Normal file
102
src/components/ha-selector/ha-selector-area.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AreaSelector } from "../../data/selector";
|
||||
import "../ha-area-picker";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
|
||||
@customElement("ha-selector-area")
|
||||
export class HaAreaSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: AreaSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@internalProperty() public _configEntries?: ConfigEntry[];
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (
|
||||
oldSelector !== this.selector &&
|
||||
this.selector.area.device?.integration
|
||||
) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
.entityFilter=${(entity) => this._filterEntities(entity)}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
></ha-area-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities(entity: EntityRegistryEntry): boolean {
|
||||
if (this.selector.area.entity?.integration) {
|
||||
if (entity.platform !== this.selector.area.entity.integration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
if (
|
||||
this.selector.area.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.area.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.area.device?.model &&
|
||||
device.model !== this.selector.area.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.area.device?.integration) {
|
||||
if (
|
||||
this._configEntries &&
|
||||
!this._configEntries.some((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === this.selector.area.device?.integration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-area": HaAreaSelector;
|
||||
}
|
||||
}
|
54
src/components/ha-selector/ha-selector-boolean.ts
Normal file
54
src/components/ha-selector/ha-selector-boolean.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-formfield";
|
||||
import "../ha-switch";
|
||||
|
||||
@customElement("ha-selector-boolean")
|
||||
export class HaBooleanSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public value?: number;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
protected render() {
|
||||
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
|
||||
<ha-switch
|
||||
.checked=${this.value}
|
||||
@change=${this._handleChange}
|
||||
></ha-switch>
|
||||
</ha-formfield>`;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
const value = ev.target.checked;
|
||||
if (this.value === value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-formfield {
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
--mdc-typography-body2-font-size: 1em;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-boolean": HaBooleanSelector;
|
||||
}
|
||||
}
|
88
src/components/ha-selector/ha-selector-device.ts
Normal file
88
src/components/ha-selector/ha-selector-device.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../device/ha-device-picker";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import { DeviceSelector } from "../../data/selector";
|
||||
|
||||
@customElement("ha-selector-device")
|
||||
export class HaDeviceSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: DeviceSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@internalProperty() public _configEntries?: ConfigEntry[];
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (oldSelector !== this.selector && this.selector.device.integration) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
.includeDeviceClasses=${this.selector.device.entity?.device_class
|
||||
? [this.selector.device.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.device.entity?.domain
|
||||
? [this.selector.device.entity.domain]
|
||||
: undefined}
|
||||
allow-custom-entity
|
||||
></ha-device-picker>`;
|
||||
}
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
if (
|
||||
this.selector.device.manufacturer &&
|
||||
device.manufacturer !== this.selector.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.device.model &&
|
||||
device.model !== this.selector.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.device.integration) {
|
||||
if (
|
||||
this._configEntries &&
|
||||
!this._configEntries.some((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === this.selector.device.integration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-device": HaDeviceSelector;
|
||||
}
|
||||
}
|
84
src/components/ha-selector/ha-selector-entity.ts
Normal file
84
src/components/ha-selector/ha-selector-entity.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-picker";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { subscribeEntityRegistry } from "../../data/entity_registry";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { EntitySelector } from "../../data/selector";
|
||||
|
||||
@customElement("ha-selector-entity")
|
||||
export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: EntitySelector;
|
||||
|
||||
@internalProperty() private _entityPlaformLookup?: Record<string, string>;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.entityFilter=${(entity) => this._filterEntities(entity)}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`;
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
const entityLookup = {};
|
||||
for (const confEnt of entities) {
|
||||
if (!confEnt.platform) {
|
||||
continue;
|
||||
}
|
||||
entityLookup[confEnt.entity_id] = confEnt.platform;
|
||||
}
|
||||
this._entityPlaformLookup = entityLookup;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _filterEntities(entity: HassEntity): boolean {
|
||||
if (this.selector.entity.domain) {
|
||||
if (computeStateDomain(entity) !== this.selector.entity.domain) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.selector.entity.device_class) {
|
||||
if (
|
||||
!entity.attributes.device_class ||
|
||||
entity.attributes.device_class !== this.selector.entity.device_class
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.selector.entity.integration) {
|
||||
if (
|
||||
!this._entityPlaformLookup ||
|
||||
this._entityPlaformLookup[entity.entity_id] !==
|
||||
this.selector.entity.integration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-entity": HaEntitySelector;
|
||||
}
|
||||
}
|
104
src/components/ha-selector/ha-selector-number.ts
Normal file
104
src/components/ha-selector/ha-selector-number.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { NumberSelector } from "../../data/selector";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "../ha-slider";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
@customElement("ha-selector-number")
|
||||
export class HaNumberSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: NumberSelector;
|
||||
|
||||
@property() public value?: number;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
protected render() {
|
||||
return html`${this.label}
|
||||
${this.selector.number.mode === "slider"
|
||||
? html`<ha-slider
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this._value}
|
||||
.step=${this.selector.number.step}
|
||||
pin
|
||||
ignore-bar-touch
|
||||
@change=${this._handleSliderChange}
|
||||
>
|
||||
</ha-slider>`
|
||||
: ""}
|
||||
<paper-input
|
||||
pattern="[0-9]+([\\.][0-9]+)?"
|
||||
.label=${this.selector.number.mode === "slider"
|
||||
? undefined
|
||||
: this.label}
|
||||
.noLabelFloat=${this.selector.number.mode === "slider"}
|
||||
class=${classMap({ single: this.selector.number.mode === "box" })}
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this._value}
|
||||
.step=${this.selector.number.step}
|
||||
type="number"
|
||||
auto-validate
|
||||
@value-changed=${this._handleInputChange}
|
||||
>
|
||||
${this.selector.number.unit_of_measurement
|
||||
? html`<div slot="suffix">
|
||||
${this.selector.number.unit_of_measurement}
|
||||
</div>`
|
||||
: ""}
|
||||
</paper-input>`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || 0;
|
||||
}
|
||||
|
||||
private _handleInputChange(ev) {
|
||||
const value = ev.detail.value;
|
||||
if (this._value === value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _handleSliderChange(ev) {
|
||||
const value = ev.target.value;
|
||||
if (this._value === value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
ha-slider {
|
||||
flex: 1;
|
||||
}
|
||||
.single {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-number": HaNumberSelector;
|
||||
}
|
||||
}
|
153
src/components/ha-selector/ha-selector-target.ts
Normal file
153
src/components/ha-selector/ha-selector-target.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { TargetSelector } from "../../data/selector";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import "../ha-target-picker";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { Target } from "../../data/target";
|
||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||
import "@material/mwc-tab/mwc-tab";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
|
||||
@customElement("ha-selector-target")
|
||||
export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: TargetSelector;
|
||||
|
||||
@property() public value?: Target;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@internalProperty() private _entityPlaformLookup?: Record<string, string>;
|
||||
|
||||
@internalProperty() private _configEntries?: ConfigEntry[];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
const entityLookup = {};
|
||||
for (const confEnt of entities) {
|
||||
if (!confEnt.platform) {
|
||||
continue;
|
||||
}
|
||||
entityLookup[confEnt.entity_id] = confEnt.platform;
|
||||
}
|
||||
this._entityPlaformLookup = entityLookup;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (
|
||||
oldSelector !== this.selector &&
|
||||
this.selector.target.device?.integration
|
||||
) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
.entityRegFilter=${(entity: EntityRegistryEntry) =>
|
||||
this._filterRegEntities(entity)}
|
||||
.entityFilter=${(entity: HassEntity) => this._filterEntities(entity)}
|
||||
.includeDeviceClasses=${this.selector.target.entity?.device_class
|
||||
? [this.selector.target.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.target.entity?.domain
|
||||
? [this.selector.target.entity.domain]
|
||||
: undefined}
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities(entity: HassEntity): boolean {
|
||||
if (this.selector.target.entity?.integration) {
|
||||
if (
|
||||
!this._entityPlaformLookup ||
|
||||
this._entityPlaformLookup[entity.entity_id] !==
|
||||
this.selector.target.entity.integration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _filterRegEntities(entity: EntityRegistryEntry): boolean {
|
||||
if (this.selector.target.entity?.integration) {
|
||||
if (entity.platform !== this.selector.target.entity.integration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
if (
|
||||
this.selector.target.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.target.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.target.device?.model &&
|
||||
device.model !== this.selector.target.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.target.device?.integration) {
|
||||
if (
|
||||
!this._configEntries?.some((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) => entry.domain === this.selector.target.device?.integration
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-target-picker {
|
||||
margin: 0 -8px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-target": HaTargetSelector;
|
||||
}
|
||||
}
|
59
src/components/ha-selector/ha-selector-time.ts
Normal file
59
src/components/ha-selector/ha-selector-time.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { customElement, html, LitElement, property } from "lit-element";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { TimeSelector } from "../../data/selector";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../paper-time-input";
|
||||
|
||||
const test = new Date().toLocaleString();
|
||||
const useAMPM = test.includes("AM") || test.includes("PM");
|
||||
|
||||
@customElement("ha-selector-time")
|
||||
export class HaTimeSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: TimeSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
protected render() {
|
||||
const parts = this.value?.split(":") || [];
|
||||
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
|
||||
|
||||
return html`
|
||||
<paper-time-input
|
||||
.label=${this.label}
|
||||
.hour=${useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours}
|
||||
.min=${parts[1] ?? "00"}
|
||||
.sec=${parts[2] ?? "00"}
|
||||
.format=${useAMPM ? 12 : 24}
|
||||
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
|
||||
@change=${this._timeChanged}
|
||||
@am-pm-changed=${this._timeChanged}
|
||||
hide-label
|
||||
enable-second
|
||||
></paper-time-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _timeChanged(ev) {
|
||||
let value = ev.target.value;
|
||||
if (useAMPM) {
|
||||
let hours = Number(ev.target.hour);
|
||||
if (ev.target.amPm === "PM") {
|
||||
hours += 12;
|
||||
}
|
||||
value = `${hours}:${ev.target.min}:${ev.target.sec}`;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-time": HaTimeSelector;
|
||||
}
|
||||
}
|
54
src/components/ha-selector/ha-selector.ts
Normal file
54
src/components/ha-selector/ha-selector.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { customElement, html, LitElement, property } from "lit-element";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
import "./ha-selector-entity";
|
||||
import "./ha-selector-device";
|
||||
import "./ha-selector-area";
|
||||
import "./ha-selector-target";
|
||||
import "./ha-selector-number";
|
||||
import "./ha-selector-boolean";
|
||||
import "./ha-selector-time";
|
||||
import "./ha-selector-action";
|
||||
import { Selector } from "../../data/selector";
|
||||
|
||||
@customElement("ha-selector")
|
||||
export class HaSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: Selector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
public focus() {
|
||||
const input = this.shadowRoot!.getElementById("selector");
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
(input as HTMLElement).focus();
|
||||
}
|
||||
|
||||
private get _type() {
|
||||
return Object.keys(this.selector)[0];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${dynamicElement(`ha-selector-${this._type}`, {
|
||||
hass: this.hass,
|
||||
selector: this.selector,
|
||||
value: this.value,
|
||||
label: this.label,
|
||||
id: "selector",
|
||||
})}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector": HaSelector;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user