Merge pull request #4117 from home-assistant/dev

20191023.0
This commit is contained in:
Bram Kragten 2019-10-23 21:36:21 +02:00 committed by GitHub
commit ad8f049570
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
246 changed files with 12813 additions and 3746 deletions

View File

@ -1,10 +1,24 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: ""
---
<!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- Provide as many details as possible. Do not delete any text from this template!
-->
**Checklist:**
- [ ] I updated to the latest version available
- [ ] I cleared the cache of my browser
**Home Assistant release with the issue:**
<!--
- Frontend -> Developer tools -> Info
- Or use this command: hass --version
@ -13,22 +27,25 @@
**Last working Home Assistant release (if known):**
**UI (States or Lovelace UI?):**
<!--
- Frontend -> Developer tools -> Info
-->
**Browser and Operating System:**
<!--
Provide details about what browser (and version) you are seeing the issue in. And also which operating system this is on. If possible try to replicate the issue in other browsers and include your findings here.
-->
**Description of problem:**
<!--
Explain what the issue is, and how things should look/behave. If possible provide a screenshot with a description.
-->
**Javascript errors shown in the web inspector (if applicable):**
```
```

View File

@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: feature request
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -8,19 +8,20 @@ install: yarn install
script:
- npm run build
- hassio/script/build_hassio
# Because else eslint fails because hassio has cleaned that build
- ./node_modules/.bin/gulp gen-icons-app
- npm run test
# - xvfb-run wct --module-resolution=node --npm
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
services:
- docker
before_deploy:
- 'docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21'
- "docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21"
deploy:
provider: script
script: script/travis_deploy
'on':
"on":
branch: master
dist: trusty
addons:
sauce_connect: true

View File

@ -3,7 +3,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
throw Error("latestBuild not defined for babel loader config");
}
return {
test: /\.m?js$/,
test: /\.m?js$|\.tsx?$/,
use: {
loader: "babel-loader",
options: {
@ -12,6 +12,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
require("@babel/preset-env").default,
{ modules: false },
],
[
require("@babel/preset-typescript").default,
{
jsxPragma: "h",
},
],
].filter(Boolean),
plugins: [
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
@ -21,6 +27,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
],
// Only support the syntax, Webpack will handle it.
"@babel/syntax-dynamic-import",
[
"@babel/transform-react-jsx",
{
pragma: "h",
},
],
[
require("@babel/plugin-proposal-decorators").default,
{ decoratorsBeforeExport: true },

6
build-scripts/env.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
isProdBuild: process.env.NODE_ENV === "production",
isStatsBuild: process.env.STATS === "1",
isTravis: process.env.TRAVIS === "true",
isNetlify: process.env.NETLIFY === "true",
};

View File

@ -1,10 +1,13 @@
// Run HA develop mode
const gulp = require("gulp");
const envVars = require("../env");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./compress.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
@ -18,7 +21,7 @@ gulp.task(
"clean",
gulp.parallel(
"gen-service-worker-dev",
"gen-icons",
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
"gen-pages-dev",
"gen-index-app-dev",
gulp.series("create-test-translation", "build-translations")
@ -35,13 +38,11 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean",
gulp.parallel("gen-icons", "build-translations"),
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
"copy-static",
gulp.parallel(
"webpack-prod-app",
// Do not compress static files in CI, it's SLOW.
...(process.env.CI === "true" ? [] : ["compress-static"])
),
"webpack-prod-app",
...// Don't compress running tests
(envVars.isTravis ? [] : ["compress-app"]),
gulp.parallel(
"gen-pages-prod",
"gen-index-app-prod",

View File

@ -1,4 +1,3 @@
// Run cast develop mode
const gulp = require("gulp");
require("./clean.js");
@ -16,7 +15,12 @@ gulp.task(
process.env.NODE_ENV = "development";
},
"clean-cast",
gulp.parallel("gen-icons", "gen-index-cast-dev", "build-translations"),
gulp.parallel(
"gen-icons-app",
"gen-icons-mdi",
"gen-index-cast-dev",
"build-translations"
),
"copy-static-cast",
"webpack-dev-server-cast"
)
@ -29,7 +33,7 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean-cast",
gulp.parallel("gen-icons", "build-translations"),
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
"copy-static-cast",
"webpack-prod-cast",
"gen-index-cast-prod"

View File

@ -9,15 +9,31 @@ gulp.task(
return del([config.root, config.build_dir]);
})
);
gulp.task(
"clean-demo",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.demo_root, config.build_dir]);
})
);
gulp.task(
"clean-cast",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.cast_root, config.build_dir]);
})
);
gulp.task(
"clean-hassio",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.hassio_root, config.build_dir]);
})
);
gulp.task(
"clean-gallery",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.gallery_root, config.build_dir]);
})
);

View File

@ -0,0 +1,38 @@
// Tasks to compress
const gulp = require("gulp");
const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream");
const path = require("path");
const paths = require("../paths");
gulp.task("compress-app", function compressApp() {
const jsLatest = gulp
.src(path.resolve(paths.output, "**/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(paths.output));
const jsEs5 = gulp
.src(path.resolve(paths.output_es5, "**/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(paths.output_es5));
const polyfills = gulp
.src(path.resolve(paths.static, "polyfills/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(path.resolve(paths.static, "polyfills")));
const translations = gulp
.src(path.resolve(paths.static, "translations/*.json"))
.pipe(zopfli())
.pipe(gulp.dest(path.resolve(paths.static, "translations")));
return merge(jsLatest, jsEs5, polyfills, translations);
});
gulp.task("compress-hassio", function compressApp() {
return gulp
.src(path.resolve(paths.hassio_root, "**/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(paths.hassio_root));
});

View File

@ -17,7 +17,8 @@ gulp.task(
},
"clean-demo",
gulp.parallel(
"gen-icons",
"gen-icons-app",
"gen-icons-mdi",
"gen-icons-demo",
"gen-index-demo-dev",
"build-translations"
@ -34,7 +35,12 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean-demo",
gulp.parallel("gen-icons", "gen-icons-demo", "build-translations"),
gulp.parallel(
"gen-icons-app",
"gen-icons-mdi",
"gen-icons-demo",
"build-translations"
),
"copy-static-demo",
"webpack-prod-demo",
"gen-index-demo-prod"

View File

@ -11,12 +11,6 @@ const config = require("../paths.js");
const templatePath = (tpl) =>
path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`);
const demoTemplatePath = (tpl) =>
path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`);
const castTemplatePath = (tpl) =>
path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`);
const readFile = (pth) => fs.readFileSync(pth).toString();
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
@ -25,10 +19,19 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
};
const renderDemoTemplate = (pth, data = {}) =>
renderTemplate(pth, data, demoTemplatePath);
renderTemplate(pth, data, (tpl) =>
path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`)
);
const renderCastTemplate = (pth, data = {}) =>
renderTemplate(pth, data, castTemplatePath);
renderTemplate(pth, data, (tpl) =>
path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`)
);
const renderGalleryTemplate = (pth, data = {}) =>
renderTemplate(pth, data, (tpl) =>
path.resolve(config.gallery_dir, "src/html/", `${tpl}.html.template`)
);
const minifyHtml = (content) =>
minify(content, {
@ -209,8 +212,33 @@ gulp.task("gen-index-demo-prod", (done) => {
es5Compatibility: es5Manifest["compatibility.js"],
es5DemoJS: es5Manifest["main.js"],
});
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
const minified = minifyHtml(content);
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), minified);
done();
});
gulp.task("gen-index-gallery-dev", (done) => {
// In dev mode we don't mangle names, so we hardcode urls. That way we can
// run webpack as last in watch mode, which blocks output.
const content = renderGalleryTemplate("index", {
latestGalleryJS: "./entrypoint.js",
});
fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), content);
done();
});
gulp.task("gen-index-gallery-prod", (done) => {
const latestManifest = require(path.resolve(
config.gallery_output,
"manifest.json"
));
const content = renderGalleryTemplate("index", {
latestGalleryJS: latestManifest["entrypoint.js"],
});
const minified = minifyHtml(content);
fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), minified);
done();
});

View File

@ -0,0 +1,38 @@
// Run demo develop mode
const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
gulp.task(
"develop-gallery",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-gallery",
gulp.parallel("gen-icons-app", "gen-icons-app", "build-translations"),
"copy-static-gallery",
"gen-index-gallery-dev",
"webpack-dev-server-gallery"
)
);
gulp.task(
"build-gallery",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-gallery",
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
"copy-static-gallery",
"webpack-prod-gallery",
"gen-index-gallery-prod"
)
);

View File

@ -4,8 +4,6 @@ const gulp = require("gulp");
const path = require("path");
const cpx = require("cpx");
const fs = require("fs-extra");
const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream");
const paths = require("../paths");
const npmPath = (...parts) =>
@ -67,20 +65,6 @@ function copyMapPanel(staticDir) {
);
}
function compressStatic(staticDir) {
const staticPath = genStaticPath(staticDir);
const polyfills = gulp
.src(staticPath("polyfills/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("polyfills")));
const translations = gulp
.src(staticPath("translations/*.json"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("translations")));
return merge(polyfills, translations);
}
gulp.task("copy-static", (done) => {
const staticDir = paths.static;
const staticPath = genStaticPath(paths.static);
@ -100,8 +84,6 @@ gulp.task("copy-static", (done) => {
done();
});
gulp.task("compress-static", () => compressStatic(paths.static));
gulp.task("copy-static-demo", (done) => {
// Copy app static files
fs.copySync(
@ -129,3 +111,15 @@ gulp.task("copy-static-cast", (done) => {
copyTranslations(paths.cast_static);
done();
});
gulp.task("copy-static-gallery", (done) => {
// Copy app static files
fs.copySync(polyPath("public/static"), paths.gallery_static);
// Copy gallery static files
fs.copySync(path.resolve(paths.gallery_dir, "public"), paths.gallery_root);
copyMapPanel(paths.gallery_static);
copyFonts(paths.gallery_static);
copyTranslations(paths.gallery_static);
done();
});

View File

@ -57,18 +57,6 @@ function generateIconset(iconsetName, iconNames) {
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
}
// Generate the full MDI iconset
function genMDIIcons() {
const meta = JSON.parse(
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
);
const iconNames = meta.map((iconInfo) => iconInfo.name);
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
}
// Helper function to map recursively over files in a folder and it's subfolders
function mapFiles(startPath, filter, mapFunc) {
const files = fs.readdirSync(startPath);
@ -101,24 +89,27 @@ function findIcons(searchPath, iconsetName) {
return icons;
}
function genHassIcons() {
gulp.task("gen-icons-mdi", (done) => {
const meta = JSON.parse(
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
);
const iconNames = meta.map((iconInfo) => iconInfo.name);
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
done();
});
gulp.task("gen-icons-app", (done) => {
const iconNames = findIcons("./src", "hass");
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
}
gulp.task("gen-icons-mdi", (done) => {
genMDIIcons();
done();
});
gulp.task("gen-icons-hass", (done) => {
genHassIcons();
done();
});
gulp.task("gen-icons", gulp.series("gen-icons-hass", "gen-icons-mdi"));
gulp.task("gen-icons-demo", (done) => {
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
@ -129,8 +120,21 @@ gulp.task("gen-icons-demo", (done) => {
done();
});
module.exports = {
findIcons,
generateIconset,
genMDIIcons,
};
gulp.task("gen-icons-hassio", (done) => {
const iconNames = findIcons(
path.resolve(paths.hassio_dir, "./src"),
"hassio"
);
// Find hassio icons inside HA main repo.
for (const item of findIcons(
path.resolve(paths.polymer_dir, "./src"),
"hassio"
)) {
iconNames.add(item);
}
fs.writeFileSync(
path.resolve(paths.hassio_dir, "hassio-icons.html"),
generateIconset("hassio", iconNames)
);
done();
});

View File

@ -0,0 +1,34 @@
const gulp = require("gulp");
const envVars = require("../env");
require("./clean.js");
require("./gen-icons.js");
require("./webpack.js");
require("./compress.js");
gulp.task(
"develop-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-hassio",
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
"webpack-watch-hassio"
)
);
gulp.task(
"build-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-hassio",
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
"webpack-prod-hassio",
...// Don't compress running tests
(envVars.isTravis ? [] : ["compress-hassio"])
)
);

View File

@ -1,6 +1,5 @@
// Tasks to run webpack.
const gulp = require("gulp");
const path = require("path");
const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server");
const log = require("fancy-log");
@ -9,8 +8,33 @@ const {
createAppConfig,
createDemoConfig,
createCastConfig,
createHassioConfig,
createGalleryConfig,
} = require("../webpack");
const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: true }),
createConfigFunc({ ...params, latestBuild: false }),
];
const runDevServer = ({
compiler,
contentBase,
port,
listenHost = "localhost",
}) =>
new WebpackDevServer(compiler, {
open: true,
watchContentBase: true,
contentBase,
}).listen(port, listenHost, function(err) {
if (err) {
throw err;
}
// Server listening
log("[webpack-dev-server]", `http://localhost:${port}`);
});
const handler = (done) => (err, stats) => {
if (err) {
console.log(err.stack || err);
@ -32,20 +56,11 @@ const handler = (done) => (err, stats) => {
};
gulp.task("webpack-watch-app", () => {
const compiler = webpack([
createAppConfig({
isProdBuild: false,
latestBuild: true,
isStatsBuild: false,
}),
createAppConfig({
isProdBuild: false,
latestBuild: false,
isStatsBuild: false,
}),
]);
compiler.watch({}, handler());
// we are not calling done, so this command will run forever
webpack(bothBuilds(createAppConfig, { isProdBuild: false })).watch(
{},
handler()
);
});
gulp.task(
@ -53,47 +68,17 @@ gulp.task(
() =>
new Promise((resolve) =>
webpack(
[
createAppConfig({
isProdBuild: true,
latestBuild: true,
isStatsBuild: false,
}),
createAppConfig({
isProdBuild: true,
latestBuild: false,
isStatsBuild: false,
}),
],
bothBuilds(createAppConfig, { isProdBuild: true }),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-demo", () => {
const compiler = webpack([
createDemoConfig({
isProdBuild: false,
latestBuild: false,
isStatsBuild: false,
}),
createDemoConfig({
isProdBuild: false,
latestBuild: true,
isStatsBuild: false,
}),
]);
new WebpackDevServer(compiler, {
open: true,
watchContentBase: true,
contentBase: path.resolve(paths.demo_dir, "dist"),
}).listen(8090, "localhost", function(err) {
if (err) {
throw err;
}
// Server listening
log("[webpack-dev-server]", "http://localhost:8090");
runDevServer({
compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
contentBase: paths.demo_root,
port: 8090,
});
});
@ -102,51 +87,22 @@ gulp.task(
() =>
new Promise((resolve) =>
webpack(
[
createDemoConfig({
isProdBuild: true,
latestBuild: false,
isStatsBuild: false,
}),
createDemoConfig({
isProdBuild: true,
latestBuild: true,
isStatsBuild: false,
}),
],
bothBuilds(createDemoConfig, {
isProdBuild: true,
}),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-cast", () => {
const compiler = webpack([
createCastConfig({
isProdBuild: false,
latestBuild: false,
}),
createCastConfig({
isProdBuild: false,
latestBuild: true,
}),
]);
new WebpackDevServer(compiler, {
open: true,
watchContentBase: true,
contentBase: path.resolve(paths.cast_dir, "dist"),
}).listen(
8080,
runDevServer({
compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
contentBase: paths.cast_root,
port: 8080,
// Accessible from the network, because that's how Cast hits it.
"0.0.0.0",
function(err) {
if (err) {
throw err;
}
// Server listening
log("[webpack-dev-server]", "http://localhost:8080");
}
);
listenHost: "0.0.0.0",
});
});
gulp.task(
@ -154,16 +110,59 @@ gulp.task(
() =>
new Promise((resolve) =>
webpack(
[
createCastConfig({
isProdBuild: true,
latestBuild: false,
}),
createCastConfig({
isProdBuild: true,
latestBuild: true,
}),
],
bothBuilds(createCastConfig, {
isProdBuild: true,
}),
handler(resolve)
)
)
);
gulp.task("webpack-watch-hassio", () => {
// we are not calling done, so this command will run forever
webpack(
createHassioConfig({
isProdBuild: false,
latestBuild: false,
})
).watch({}, handler());
});
gulp.task(
"webpack-prod-hassio",
() =>
new Promise((resolve) =>
webpack(
createHassioConfig({
isProdBuild: true,
latestBuild: false,
}),
handler(resolve)
)
)
);
gulp.task("webpack-dev-server-gallery", () => {
runDevServer({
compiler: webpack(
createGalleryConfig({ latestBuild: true, isProdBuild: false })
),
contentBase: paths.gallery_root,
port: 8100,
});
});
gulp.task(
"webpack-prod-gallery",
() =>
new Promise((resolve) =>
webpack(
createGalleryConfig({
isProdBuild: true,
latestBuild: true,
}),
handler(resolve)
)
)

View File

@ -20,4 +20,13 @@ module.exports = {
cast_static: path.resolve(__dirname, "../cast/dist/static"),
cast_output: path.resolve(__dirname, "../cast/dist/frontend_latest"),
cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"),
gallery_dir: path.resolve(__dirname, "../gallery"),
gallery_root: path.resolve(__dirname, "../gallery/dist"),
gallery_output: path.resolve(__dirname, "../gallery/dist/frontend_latest"),
gallery_static: path.resolve(__dirname, "../gallery/dist/static"),
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_root: path.resolve(__dirname, "../hassio/build"),
hassio_publicPath: "/api/hassio/app",
};

View File

@ -3,8 +3,6 @@ const fs = require("fs");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require("@gfx/zopfli");
const ManifestPlugin = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const { babelLoaderConfig } = require("./babel.js");
@ -17,288 +15,246 @@ if (!version) {
}
version = version[0];
const genMode = (isProdBuild) => (isProdBuild ? "production" : "development");
const genDevTool = (isProdBuild) =>
isProdBuild ? "source-map" : "inline-cheap-module-source-map";
const genFilename = (isProdBuild, dontHash = new Set()) => ({ chunk }) => {
if (!isProdBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`;
}
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
};
const genChunkFilename = (isProdBuild, isStatsBuild) =>
isProdBuild && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const resolve = {
extensions: [".ts", ".js", ".json", ".tsx"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
};
const tsLoader = (latestBuild) => ({
test: /\.ts|tsx$/,
exclude: [path.resolve(paths.polymer_dir, "node_modules")],
use: [
{
loader: "ts-loader",
options: {
compilerOptions: latestBuild
? { noEmit: false }
: { target: "es5", noEmit: false },
},
},
],
});
const cssLoader = {
test: /\.css$/,
use: "raw-loader",
};
const htmlLoader = {
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
};
const plugins = [
// Ignore moment.js locales
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Color.js is bloated, it contains all color definitions for all material color sets.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/paper-styles\/color\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore roboto pointing at CDN. We use local font-roboto-local.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/font-roboto\/roboto\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore mwc icons pointing at CDN.
new webpack.NormalModuleReplacementPlugin(
/@material\/mwc-icon\/mwc-icon-font\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
];
const optimization = (latestBuild) => ({
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
sourceMap: true,
terserOptions: {
safari10: true,
ecma: latestBuild ? undefined : 5,
},
}),
],
});
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
const isCI = process.env.CI === "true";
// Create an object mapping browser urls to their paths during build
const translationMetadata = require("../build-translations/translationMetadata.json");
const workBoxTranslationsTemplatedURLs = {};
const englishFP = translationMetadata.translations.en.fingerprints;
Object.keys(englishFP).forEach((key) => {
workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFP[key]}`
] = `build-translations/output/${key}.json`;
});
const entry = {
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts",
core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.ts",
"custom-panel": "./src/entrypoints/custom-panel.ts",
"hass-icons": "./src/entrypoints/hass-icons.ts",
};
const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
const createWebpackConfig = ({
entry,
outputRoot,
defineOverlay,
isProdBuild,
latestBuild,
isStatsBuild,
}) => {
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild ? "source-map" : "inline-cheap-module-source-map",
entry,
module: {
rules,
rules: [
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
},
optimization: {
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
sourceMap: true,
terserOptions: {
safari10: true,
ecma: latestBuild ? undefined : 5,
},
}),
],
},
optimization: optimization(latestBuild),
plugins: [
new ManifestPlugin(),
new webpack.DefinePlugin({
__DEV__: JSON.stringify(!isProdBuild),
__DEMO__: false,
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(version),
__DEMO__: false,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
...defineOverlay,
}),
...plugins,
isProdBuild &&
!isCI &&
!isStatsBuild &&
new CompressionPlugin({
cache: true,
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
},
}),
latestBuild &&
new WorkboxPlugin.InjectManifest({
swSrc: "./src/entrypoints/service-worker-hass.js",
swDest: "service_worker.js",
importWorkboxFrom: "local",
include: [/\.js$/],
templatedURLs: {
...workBoxTranslationsTemplatedURLs,
"/static/icons/favicon-192x192.png":
"public/icons/favicon-192x192.png",
"/static/fonts/roboto/Roboto-Light.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
"/static/fonts/roboto/Roboto-Medium.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
"/static/fonts/roboto/Roboto-Regular.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
"/static/fonts/roboto/Roboto-Bold.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
},
}),
// Ignore moment.js locales
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Color.js is bloated, it contains all color definitions for all material color sets.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/paper-styles\/color\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore roboto pointing at CDN. We use local font-roboto-local.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/font-roboto\/roboto\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore mwc icons pointing at CDN.
new webpack.NormalModuleReplacementPlugin(
/@material\/mwc-icon\/mwc-icon-font\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json", ".tsx"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
},
output: {
filename: genFilename(isProdBuild),
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
path: latestBuild ? paths.output : paths.output_es5,
filename: ({ chunk }) => {
const dontHash = new Set();
if (!isProdBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`;
}
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
},
chunkFilename:
isProdBuild && !isStatsBuild
? "chunk.[chunkhash].js"
: "[name].chunk.js",
path: path.resolve(
outputRoot,
latestBuild ? "frontend_latest" : "frontend_es5"
),
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
// For workerize loader
globalObject: "self",
},
resolve,
};
};
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
const config = createWebpackConfig({
entry: {
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts",
core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.ts",
"custom-panel": "./src/entrypoints/custom-panel.ts",
"hass-icons": "./src/entrypoints/hass-icons.ts",
},
outputRoot: paths.root,
isProdBuild,
latestBuild,
isStatsBuild,
});
if (latestBuild) {
// Create an object mapping browser urls to their paths during build
const translationMetadata = require("../build-translations/translationMetadata.json");
const workBoxTranslationsTemplatedURLs = {};
const englishFP = translationMetadata.translations.en.fingerprints;
Object.keys(englishFP).forEach((key) => {
workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFP[key]}`
] = `build-translations/output/${key}.json`;
});
config.plugins.push(
new WorkboxPlugin.InjectManifest({
swSrc: "./src/entrypoints/service-worker-hass.js",
swDest: "service_worker.js",
importWorkboxFrom: "local",
include: [/\.js$/],
templatedURLs: {
...workBoxTranslationsTemplatedURLs,
"/static/icons/favicon-192x192.png":
"public/icons/favicon-192x192.png",
"/static/fonts/roboto/Roboto-Light.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
"/static/fonts/roboto/Roboto-Medium.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
"/static/fonts/roboto/Roboto-Regular.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
"/static/fonts/roboto/Roboto-Bold.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
},
})
);
}
return config;
};
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
return createWebpackConfig({
entry: {
main: "./demo/src/entrypoint.ts",
compatibility: "./src/entrypoints/compatibility.ts",
},
module: {
rules,
},
optimization: optimization(latestBuild),
plugins: [
new ManifestPlugin(),
new webpack.DefinePlugin({
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(`DEMO-${version}`),
__DEMO__: true,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
...plugins,
].filter(Boolean),
resolve,
output: {
filename: genFilename(isProdBuild),
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
path: path.resolve(
paths.demo_root,
latestBuild ? "frontend_latest" : "frontend_es5"
main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
compatibility: path.resolve(
paths.polymer_dir,
"src/entrypoints/compatibility.ts"
),
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
// For workerize loader
globalObject: "self",
},
};
outputRoot: paths.demo_root,
defineOverlay: {
__VERSION__: JSON.stringify(`DEMO-${version}`),
__DEMO__: true,
},
isProdBuild,
latestBuild,
isStatsBuild,
});
};
const createCastConfig = ({ isProdBuild, latestBuild }) => {
const isStatsBuild = false;
const entry = {
launcher: "./cast/src/launcher/entrypoint.ts",
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
};
if (latestBuild) {
entry.receiver = "./cast/src/receiver/entrypoint.ts";
entry.receiver = path.resolve(paths.cast_dir, "src/receiver/entrypoint.ts");
}
const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
return createWebpackConfig({
entry,
module: {
rules,
outputRoot: paths.cast_root,
isProdBuild,
latestBuild,
});
};
const createHassioConfig = ({ isProdBuild, latestBuild }) => {
if (latestBuild) {
throw new Error("Hass.io does not support latest build!");
}
const config = createWebpackConfig({
entry: {
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.js"),
},
optimization: optimization(latestBuild),
plugins: [
new ManifestPlugin(),
new webpack.DefinePlugin({
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(version),
__DEMO__: false,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
...plugins,
].filter(Boolean),
resolve,
output: {
filename: genFilename(isProdBuild),
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
path: path.resolve(
paths.cast_root,
latestBuild ? "frontend_latest" : "frontend_es5"
),
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
// For workerize loader
globalObject: "self",
outputRoot: "",
isProdBuild,
latestBuild,
});
config.output.path = paths.hassio_root;
config.output.publicPath = paths.hassio_publicPath;
return config;
};
const createGalleryConfig = ({ isProdBuild, latestBuild }) => {
if (!latestBuild) {
throw new Error("Gallery only supports latest build!");
}
const config = createWebpackConfig({
entry: {
entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"),
},
};
outputRoot: paths.gallery_root,
isProdBuild,
latestBuild,
});
return config;
};
module.exports = {
resolve,
plugins,
optimization,
createAppConfig,
createDemoConfig,
createCastConfig,
createHassioConfig,
createGalleryConfig,
};

11
cast/webpack.config.js Normal file
View File

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

View File

@ -217,6 +217,18 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
icon: "hademo:currency-usd",
},
},
"sensor.study_temp": {
entity_id: "sensor.study_temp",
state: "20.9",
attributes: {
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: localize(
"ui.panel.page-demo.config.arsaboo.names.temperature_study"
),
icon: "hademo:thermometer",
},
},
"cover.garagedoor": {
entity_id: "cover.garagedoor",
state: "closed",

View File

@ -446,6 +446,11 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
"script.tv_off",
],
},
{
type: "sensor",
entity: "sensor.study_temp",
graph: "line",
},
{
type: "entities",
title: "Doorbell",

View File

@ -23,27 +23,24 @@ export const demoThemeJimpower = () => ({
"paper-listbox-background-color": "#2E333A",
"table-row-background-color": "#353840",
"paper-grey-50": "var(--primary-text-color)",
"paper-toggle-button-checked-button-color": "var(--accent-color)",
"switch-checked-color": "var(--accent-color)",
"paper-dialog-background-color": "#434954",
"secondary-text-color": "#5294E2",
"google-red-500": "#E45E65",
"divider-color": "rgba(0, 0, 0, .12)",
"paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
"google-green-500": "#39E949",
"paper-toggle-button-unchecked-button-color": "var(--disabled-text-color)",
"switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green",
"paper-listbox-color": "var(--primary-color)",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
"paper-toggle-button-checked-ink-color": "var(--accent-color)",
"paper-card-background-color": "#434954",
"label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-color)",
"paper-toggle-button-unchecked-bar-color": "var(--disabled-text-color)",
"switch-unchecked-track-color": "var(--disabled-text-color)",
"dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#F9C536",
"accent-color": "#E45E65",
"paper-toggle-button-checked-bar-color": "var(--accent-color)",
"table-row-alternative-background-color": "#3E424B",
});

View File

@ -24,27 +24,24 @@ export const demoThemeKernehed = () => ({
"paper-listbox-background-color": "#141414",
"table-row-background-color": "#292929",
"paper-grey-50": "var(--primary-text-color)",
"paper-toggle-button-checked-button-color": "var(--accent-color)",
"switch-checked-color": "var(--accent-color)",
"paper-dialog-background-color": "#292929",
"secondary-text-color": "#b58e31",
"google-red-500": "#b58e31",
"divider-color": "rgba(0, 0, 0, .12)",
"paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
"google-green-500": "#2980b9",
"paper-toggle-button-unchecked-button-color": "var(--disabled-text-color)",
"switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green",
"paper-listbox-color": "#777777",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
"paper-toggle-button-checked-ink-color": "var(--accent-color)",
"paper-card-background-color": "#292929",
"label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-color)",
"paper-toggle-button-unchecked-bar-color": "var(--disabled-text-color)",
"switch-unchecked-track-color": "var(--disabled-text-color)",
"dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#b58e31",
"accent-color": "#2980b9",
"paper-toggle-button-checked-bar-color": "var(--accent-color)",
"table-row-alternative-background-color": "#292929",
});

View File

@ -12,8 +12,7 @@ export const demoThemeTeachingbirds = () => ({
"paper-slider-knob-color": "var(--primary-color)",
"paper-listbox-color": "#FFFFFF",
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
"paper-toggle-button-checked-ink-color": "var(--dark-primary-color)",
"paper-toggle-button-unchecked-bar-color": "var(--primary-text-color)",
"switch-unchecked-track-color": "var(--primary-text-color)",
"paper-card-background-color": "#4e4e4e",
"label-badge-text-color": "var(--text-primary-color)",
"primary-background-color": "#303030",
@ -22,7 +21,7 @@ export const demoThemeTeachingbirds = () => ({
"secondary-background-color": "#2b2b2b",
"paper-slider-knob-start-color": "var(--primary-color)",
"paper-item-icon-active-color": "#d8bf50",
"paper-toggle-button-checked-button-color": "var(--primary-color)",
"switch-checked-color": "var(--primary-color)",
"secondary-text-color": "#389638",
"disabled-text-color": "#545454",
"paper-item-icon_-_color": "var(--primary-text-color)",

View File

@ -1,10 +1,4 @@
import {
LitElement,
html,
CSSResult,
css,
PropertyDeclarations,
} from "lit-element";
import { LitElement, html, CSSResult, css, property } from "lit-element";
import { until } from "lit-html/directives/until";
import "@material/mwc-button";
import "@polymer/paper-spinner/paper-spinner-lite";
@ -20,19 +14,11 @@ import {
} from "../configs/demo-configs";
export class HADemoCard extends LitElement implements LovelaceCard {
public lovelace?: Lovelace;
public hass!: MockHomeAssistant;
private _switching?: boolean;
@property() public lovelace?: Lovelace;
@property() public hass!: MockHomeAssistant;
@property() private _switching?: boolean;
private _hidden = localStorage.hide_demo_card;
static get properties(): PropertyDeclarations {
return {
lovelace: {},
hass: {},
_switching: {},
};
}
public getCardSize() {
return this._hidden ? 0 : 2;
}

View File

@ -1,10 +1,9 @@
const { createDemoConfig } = require("../build-scripts/webpack.js");
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
// This file exists because we haven't migrated the stats script yet
// File just used for stats builds
const isProdBuild = process.env.NODE_ENV === "production";
const isStatsBuild = process.env.STATS === "1";
const latestBuild = false;
const latestBuild = true;
module.exports = createDemoConfig({
isProdBuild,

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#2157BC">
<title>HAGallery</title>
<script src='./main.js' async></script>
<style>
body {
font-family: Roboto, Noto, sans-serif;
margin: 0;
padding: 0;
}
</style>
</head>
<body></body>
</html>

View File

@ -4,14 +4,6 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(dirname "$0")/../.."
OUTPUT_DIR=dist
rm -rf $OUTPUT_DIR
cd ..
./node_modules/.bin/gulp build-translations gen-icons
cd gallery
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js
./node_modules/.bin/gulp build-gallery

View File

@ -4,10 +4,6 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(dirname "$0")/../.."
cd ..
./node_modules/.bin/gulp build-translations gen-icons
cd gallery
../node_modules/.bin/webpack-dev-server
./node_modules/.bin/gulp develop-gallery

View File

@ -1,6 +1,6 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import JsYaml from "js-yaml";
import { safeLoad } from "js-yaml";
import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";
@ -62,7 +62,7 @@ class DemoCard extends PolymerElement {
card.removeChild(card.lastChild);
}
const el = createCardElement(JsYaml.safeLoad(config.config)[0]);
const el = createCardElement(safeLoad(config.config)[0]);
el.hass = this.hass;
card.appendChild(el);
}

View File

@ -26,7 +26,9 @@ class DemoCards extends PolymerElement {
</style>
<app-toolbar>
<div class="filters">
<ha-switch checked="{{_showConfig}}">Show config</ha-switch>
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
Show config
</ha-switch>
</div>
</app-toolbar>
<div class="cards">
@ -51,6 +53,10 @@ class DemoCards extends PolymerElement {
},
};
}
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
}
customElements.define("demo-cards", DemoCards);

View File

@ -12,7 +12,7 @@ export class DemoUtilLongPress extends LitElement {
() => html`
<ha-card>
<mwc-button
@ha-click="${this._handleTap}"
@ha-click="${this._handleClick}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
>
@ -28,7 +28,7 @@ export class DemoUtilLongPress extends LitElement {
`;
}
private _handleTap(ev: Event) {
private _handleClick(ev: Event) {
this._addValue(ev, "tap");
}

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#2157BC" />
<title>HAGallery</title>
<script type="module" src="<%= latestGalleryJS %>"></script>
<style>
body {
font-family: Roboto, Noto, sans-serif;
margin: 0;
padding: 0;
}
</style>
</head>
<body></body>
</html>

View File

@ -1,6 +1,6 @@
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const webpackBase = require("../build-scripts/webpack.js");
const { createGalleryConfig } = require("../build-scripts/webpack.js");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
const isProd = process.env.NODE_ENV === "production";
@ -9,80 +9,64 @@ const buildPath = path.resolve(__dirname, "dist");
const publicPath = isProd ? "./" : "http://localhost:8080/";
const latestBuild = true;
const rules = [
{
exclude: [path.resolve(__dirname, "../node_modules")],
test: /\.ts$/,
use: [
{
loader: "ts-loader",
options: {
compilerOptions: latestBuild
? { noEmit: false }
: {
target: "es5",
noEmit: false,
},
module.exports = createGalleryConfig({
latestBuild: true,
});
const bla = () => {
const oldExports = {
mode: isProd ? "production" : "development",
// Disabled in prod while we make Home Assistant able to serve the right files.
// Was source-map
devtool: isProd ? "none" : "inline-source-map",
entry: "./src/entrypoint.js",
module: {
rules: [
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
},
],
},
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
},
},
];
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
module.exports = {
mode: isProd ? "production" : "development",
// Disabled in prod while we make Home Assistant able to serve the right files.
// Was source-map
devtool: isProd ? "none" : "inline-source-map",
entry: "./src/entrypoint.js",
module: {
rules,
},
optimization: webpackBase.optimization(latestBuild),
plugins: [
new CopyWebpackPlugin([
"public",
{ from: "../public", to: "static" },
{ from: "../build-translations/output", to: "static/translations" },
{
from: "../node_modules/leaflet/dist/leaflet.css",
to: "static/images/leaflet/",
},
{
from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
to: "static/fonts/roboto/",
},
{
from: "../node_modules/leaflet/dist/images",
to: "static/images/leaflet/",
},
]),
].filter(Boolean),
resolve: webpackBase.resolve,
output: {
filename: "[name].js",
chunkFilename: chunkFilename,
path: buildPath,
publicPath,
},
devServer: {
contentBase: "./public",
},
optimization: webpackBase.optimization(latestBuild),
plugins: [
new CopyWebpackPlugin([
"public",
{ from: "../public", to: "static" },
{ from: "../build-translations/output", to: "static/translations" },
{
from: "../node_modules/leaflet/dist/leaflet.css",
to: "static/images/leaflet/",
},
{
from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
to: "static/fonts/roboto/",
},
{
from: "../node_modules/leaflet/dist/images",
to: "static/images/leaflet/",
},
]),
].filter(Boolean),
resolve: webpackBase.resolve,
output: {
filename: "[name].js",
chunkFilename: chunkFilename,
path: buildPath,
publicPath,
},
devServer: {
contentBase: "./public",
},
};
};

View File

@ -4,11 +4,6 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(dirname "$0")/../.."
OUTPUT_DIR=build
rm -rf $OUTPUT_DIR
node script/gen-icons.js
NODE_ENV=production CI=false ../node_modules/.bin/webpack -p --config webpack.config.js
./node_modules/.bin/gulp build-hassio

View File

@ -4,11 +4,6 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(dirname "$0")/../.."
OUTPUT_DIR=build
rm -rf $OUTPUT_DIR
mkdir $OUTPUT_DIR
node script/gen-icons.js
../node_modules/.bin/webpack --watch --progress
./node_modules/.bin/gulp develop-hassio

View File

@ -1,20 +0,0 @@
#!/usr/bin/env node
const fs = require("fs");
const {
findIcons,
generateIconset,
genMDIIcons,
} = require("../../build-scripts/gulp/gen-icons.js");
function genHassioIcons() {
const iconNames = findIcons("./src", "hassio");
for (const item of findIcons("../src", "hassio")) {
iconNames.add(item);
}
fs.writeFileSync("./hassio-icons.html", generateIconset("hassio", iconNames));
}
genMDIIcons();
genHassioIcons();

View File

@ -7,6 +7,7 @@ import {
property,
customElement,
} from "lit-element";
import "@polymer/iron-icon/iron-icon";
import { HomeAssistant } from "../../../src/types";
import {
@ -33,12 +34,15 @@ export class HassioUpdate extends LitElement {
@property() public error?: string;
protected render(): TemplateResult | void {
if (
this.hassInfo.version === this.hassInfo.last_version &&
this.supervisorInfo.version === this.supervisorInfo.last_version &&
(!this.hassOsInfo ||
this.hassOsInfo.version === this.hassOsInfo.version_latest)
) {
const updatesAvailable: number = [
this.hassInfo,
this.supervisorInfo,
this.hassOsInfo,
].filter((value) => {
return !!value && value.version !== value.last_version;
}).length;
if (!updatesAvailable) {
return html``;
}
@ -50,6 +54,11 @@ export class HassioUpdate extends LitElement {
`
: ""}
<div class="card-group">
<div class="title">
${updatesAvailable > 1
? "Updates Available 🎉"
: "Update Available 🎉"}
</div>
${this._renderUpdateCard(
"Home Assistant",
this.hassInfo.version,
@ -57,7 +66,8 @@ export class HassioUpdate extends LitElement {
"hassio/homeassistant/update",
`https://${
this.hassInfo.last_version.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`
}.home-assistant.io/latest-release-notes/`,
"hassio:home-assistant"
)}
${this._renderUpdateCard(
"Hass.io Supervisor",
@ -89,18 +99,31 @@ export class HassioUpdate extends LitElement {
curVersion: string,
lastVersion: string,
apiPath: string,
releaseNotesUrl: string
releaseNotesUrl: string,
icon?: string
): TemplateResult {
if (lastVersion === curVersion) {
return html``;
}
return html`
<paper-card heading="${name} update available! 🎉">
<paper-card>
<div class="card-content">
${name} ${lastVersion} is available and you are currently running
${name} ${curVersion}.
${icon
? html`
<div class="icon">
<iron-icon .icon="${icon}" />
</div>
`
: ""}
<div class="update-heading">${name} ${lastVersion}</div>
<div class="warning">
You are currently running version ${curVersion}
</div>
</div>
<div class="card-actions">
<a href="${releaseNotesUrl}" target="_blank">
<mwc-button>Release notes</mwc-button>
</a>
<ha-call-api-button
.hass=${this.hass}
.path=${apiPath}
@ -108,9 +131,6 @@ export class HassioUpdate extends LitElement {
>
Update
</ha-call-api-button>
<a href="${releaseNotesUrl}" target="_blank">
<mwc-button>Release notes</mwc-button>
</a>
</div>
</paper-card>
`;
@ -140,6 +160,23 @@ export class HassioUpdate extends LitElement {
display: inline-block;
margin-bottom: 32px;
}
.icon {
--iron-icon-height: 48px;
--iron-icon-width: 48px;
float: right;
margin: 0 0 2px 10px;
}
.update-heading {
font-size: var(--paper-font-subhead_-_font-size);
font-weight: 500;
margin-bottom: 0.5em;
}
.warning {
color: var(--secondary-text-color);
}
.card-actions {
text-align: right;
}
.errors {
color: var(--google-red-500);
padding: 16px;

66
hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts Normal file → Executable file
View File

@ -3,6 +3,7 @@ import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -94,13 +95,23 @@ class HassioSnapshotDialog extends PolymerElement {
.details {
color: var(--secondary-text-color);
}
.download {
color: var(--primary-color);
}
.warning,
.error {
color: var(--google-red-500);
}
.buttons {
display: flex;
flex-direction: column;
}
.buttons li {
list-style-type: none;
}
.buttons .icon {
margin-right: 16px;
}
.no-margin-top {
margin-top: 0;
}
</style>
<ha-paper-dialog
id="dialog"
@ -132,7 +143,7 @@ class HassioSnapshotDialog extends PolymerElement {
</template>
<template is="dom-if" if="[[_addons.length]]">
<div>Add-ons:</div>
<paper-dialog-scrollable>
<paper-dialog-scrollable class="no-margin-top">
<template is="dom-repeat" items="[[_addons]]" sort="_sortAddons">
<paper-checkbox checked="{{item.checked}}">
[[item.name]] <span class="details">([[item.version]])</span>
@ -151,28 +162,35 @@ class HassioSnapshotDialog extends PolymerElement {
<template is="dom-if" if="[[error]]">
<p class="error">Error: [[error]]</p>
</template>
<div class="buttons">
<paper-icon-button
icon="hassio:delete"
on-click="_deleteClicked"
class="warning"
title="Delete snapshot"
></paper-icon-button>
<paper-icon-button
on-click="_downloadClicked"
icon="hassio:download"
class="download"
title="Download snapshot"
></paper-icon-button>
<mwc-button on-click="_partialRestoreClicked"
>Restore selected</mwc-button
>
<div>Actions:</div>
<ul class="buttons">
<li>
<mwc-button on-click="_downloadClicked">
<iron-icon icon="hassio:download" class="icon"></iron-icon>
Download Snapshot
</mwc-button>
</li>
<li>
<mwc-button on-click="_partialRestoreClicked">
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Restore Selected
</mwc-button>
</li>
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
<mwc-button on-click="_fullRestoreClicked"
>Wipe &amp; restore</mwc-button
>
<li>
<mwc-button on-click="_fullRestoreClicked">
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Wipe &amp; restore
</mwc-button>
</li>
</template>
</div>
<li>
<mwc-button on-click="_deleteClicked">
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>
</li>
</ul>
</ha-paper-dialog>
`;
}

View File

@ -3,7 +3,7 @@ import { PolymerElement } from "@polymer/polymer";
import "@polymer/paper-icon-button";
import "../../src/resources/ha-style";
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import {
HassRouterPage,

View File

@ -1,85 +1,11 @@
const webpack = require("webpack");
const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require("@gfx/zopfli");
const { createHassioConfig } = require("../build-scripts/webpack.js");
const { isProdBuild } = require("../build-scripts/env.js");
const config = require("./config.js");
const webpackBase = require("../build-scripts/webpack.js");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
// File just used for stats builds
const isProdBuild = process.env.NODE_ENV === "production";
const isCI = process.env.CI === "true";
const chunkFilename = isProdBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const latestBuild = false;
const rules = [
{
exclude: [config.nodeDir],
test: /\.ts$/,
use: [
{
loader: "ts-loader",
options: {
compilerOptions: latestBuild
? { noEmit: false }
: {
target: "es5",
noEmit: false,
},
},
},
],
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
];
if (!latestBuild) {
rules.push(babelLoaderConfig({ latestBuild }));
}
module.exports = {
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild ? "source-map" : "inline-source-map",
entry: {
entrypoint: "./src/entrypoint.js",
},
module: {
rules,
},
optimization: webpackBase.optimization(latestBuild),
plugins: [
new webpack.DefinePlugin({
__DEV__: JSON.stringify(!isProdBuild),
__DEMO__: false,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
isProdBuild &&
!isCI &&
new CompressionPlugin({
cache: true,
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
},
}),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json"],
},
output: {
filename: "[name].js",
chunkFilename,
path: config.buildDir,
publicPath: `${config.publicPath}/`,
},
};
module.exports = createHassioConfig({
isProdBuild,
latestBuild,
});

View File

@ -84,7 +84,6 @@
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^4.4.0",
"intl-messageformat": "^2.2.0",
"jquery": "^3.4.0",
"js-yaml": "^3.13.1",
"leaflet": "^1.4.0",
"lit-element": "^2.2.1",
@ -98,7 +97,6 @@
"react-big-calendar": "^0.20.4",
"regenerator-runtime": "^0.13.2",
"roboto-fontface": "^0.10.0",
"round-slider": "^1.3.3",
"superstruct": "^0.6.1",
"tslib": "^1.10.0",
"unfetch": "^4.1.0",
@ -112,19 +110,20 @@
"@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-proposal-object-rest-spread": "^7.4.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.4.0",
"@gfx/zopfli": "^1.0.11",
"@babel/plugin-transform-react-jsx": "^7.3.0",
"@babel/preset-env": "^7.4.2",
"@babel/preset-typescript": "^7.4.0",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1",
"@types/codemirror": "^0.0.78",
"@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6",
"babel-loader": "^8.0.5",
"chai": "^4.2.0",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^5.0.2",
"del": "^4.0.0",
"eslint": "^6.3.0",
@ -160,7 +159,6 @@
"require-dir": "^1.2.0",
"sinon": "^7.3.1",
"terser-webpack-plugin": "^1.2.3",
"ts-loader": "^6.1.1",
"ts-mocha": "^6.0.0",
"tslint": "^5.14.0",
"tslint-config-prettier": "^1.18.0",

View File

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

View File

@ -7,7 +7,7 @@ import {
css,
} from "lit-element";
import "@material/mwc-button";
import "../components/ha-form";
import "../components/ha-form/ha-form";
import "../components/ha-markdown";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { AuthProvider } from "../data/auth";

View File

@ -2,10 +2,10 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import {
LitElement,
html,
PropertyDeclarations,
PropertyValues,
CSSResult,
css,
property,
} from "lit-element";
import "./ha-auth-flow";
import { AuthProvider, fetchAuthProviders } from "../data/auth";
@ -20,11 +20,11 @@ interface QueryParams {
}
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
public clientId?: string;
public redirectUri?: string;
public oauth2State?: string;
private _authProvider?: AuthProvider;
private _authProviders?: AuthProvider[];
@property() public clientId?: string;
@property() public redirectUri?: string;
@property() public oauth2State?: string;
@property() private _authProvider?: AuthProvider;
@property() private _authProviders?: AuthProvider[];
constructor() {
super();
@ -48,16 +48,6 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
}
}
static get properties(): PropertyDeclarations {
return {
_authProvider: {},
_authProviders: {},
clientId: {},
redirectUri: {},
oauth2State: {},
};
}
protected render() {
if (!this._authProviders) {
return html`

View File

@ -1,31 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/entity/ha-state-label-badge";
class HaBadgesCard extends PolymerElement {
static get template() {
return html`
<style>
ha-state-label-badge {
display: inline-block;
margin-bottom: var(--ha-state-label-badge-margin-bottom, 16px);
}
</style>
<template is="dom-repeat" items="[[states]]">
<ha-state-label-badge
hass="[[hass]]"
state="[[item]]"
></ha-state-label-badge>
</template>
`;
}
static get properties() {
return {
hass: Object,
states: Array,
};
}
}
customElements.define("ha-badges-card", HaBadgesCard);

View File

@ -0,0 +1,45 @@
import { TemplateResult, html } from "lit-html";
import { customElement, LitElement, property } from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import "../components/entity/ha-state-label-badge";
import { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-badges-card")
class HaBadgesCard extends LitElement {
@property() public hass?: HomeAssistant;
@property() public states?: HassEntity[];
protected render(): TemplateResult | void {
if (!this.hass || !this.states) {
return html``;
}
return html`
${this.states.map(
(state) => html`
<ha-state-label-badge
.hass=${this.hass}
.state=${state}
@click=${this._handleClick}
></ha-state-label-badge>
`
)}
`;
}
private _handleClick(ev: Event) {
const entityId = ((ev.target as any).state as HassEntity).entity_id;
fireEvent(this, "hass-more-info", {
entityId,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-badges-card": HaBadgesCard;
}
}

View File

@ -35,6 +35,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"camera",
"climate",
"configurator",
"counter",
"cover",
"fan",
"group",

View File

@ -21,12 +21,12 @@ const hexToRgb = (hex: string): string | null => {
* localTheme: selected theme.
* updateMeta: boolean if we should update the theme-color meta element.
*/
export default function applyThemesOnElement(
export const applyThemesOnElement = (
element,
themes,
localTheme,
updateMeta = false
) {
) => {
if (!element._themes) {
element._themes = {};
}
@ -76,4 +76,4 @@ export default function applyThemesOnElement(
styles["--primary-color"] || meta.getAttribute("default-content");
meta.setAttribute("content", themeColor);
}
}
};

View File

@ -14,6 +14,7 @@ const fixedIcons = {
climate: "hass:thermostat",
configurator: "hass:settings",
conversation: "hass:text-to-speech",
counter: "hass:counter",
device_tracker: "hass:account",
fan: "hass:fan",
google_assistant: "hass:google-assistant",

View File

@ -3,6 +3,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-progress-button";
import { EventsMixin } from "../../mixins/events-mixin";
import { showConfirmationDialog } from "../../dialogs/confirmation/show-dialog-confirmation";
/*
* @appliesMixin EventsMixin
@ -49,10 +50,7 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
};
}
buttonTapped() {
if (this.confirmation && !window.confirm(this.confirmation)) {
return;
}
callService() {
this.progress = true;
var el = this;
var eventData = {
@ -79,6 +77,17 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
el.fire("hass-service-called", eventData);
});
}
buttonTapped() {
if (this.confirmation) {
showConfirmationDialog(this, {
text: this.confirmation,
confirm: () => this.callService(),
});
} else {
this.callService();
}
}
}
customElements.define("ha-call-service-button", HaCallServiceButton);

View File

@ -427,7 +427,7 @@ export class HaDataTable extends BaseElement {
}
.mdc-data-table {
background-color: var(--card-background-color);
background-color: var(--data-table-background-color);
border-radius: 4px;
border-width: 1px;
border-style: solid;

View File

@ -1,7 +1,7 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "@polymer/paper-listbox/paper-listbox";
import memoizeOne from "memoize-one";
import {
@ -23,52 +23,165 @@ import {
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { computeStateName } from "../../common/entity/compute_state_name";
interface Device {
name: string;
area: string;
id: string;
}
const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px 0;
padding: 0;
}
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div secondary>[[item.area]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name!;
root.querySelector("[secondary]")!.textContent = model.item.area!;
};
@customElement("ha-device-picker")
class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public hass?: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public devices?: DeviceRegistryEntry[];
@property() public areas?: AreaRegistryEntry[];
@property() public entities?: EntityRegistryEntry[];
@property({ type: Boolean }) private _opened?: boolean;
private _sortedDevices = memoizeOne((devices?: DeviceRegistryEntry[]) => {
if (!devices || devices.length === 1) {
return devices || [];
private _getDevices = memoizeOne(
(
devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[],
entities: EntityRegistryEntry[]
): Device[] => {
if (!devices.length) {
return [];
}
const deviceEntityLookup: DeviceEntityLookup = {};
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);
}
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
const outputDevices = devices.map((device) => {
return {
id: device.id,
name:
device.name_by_user ||
device.name ||
this._fallbackDeviceName(device.id, deviceEntityLookup) ||
"No name",
area: device.area_id ? areaLookup[device.area_id].name : "No area",
};
});
if (outputDevices.length === 1) {
return outputDevices;
}
return outputDevices.sort((a, b) => compare(a.name || "", b.name || ""));
}
const sorted = [...devices];
sorted.sort((a, b) => compare(a.name || "", b.name || ""));
return sorted;
});
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass!.connection!, (devices) => {
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this.devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this.areas = areas;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this.entities = entities;
}),
];
}
protected render(): TemplateResult | void {
if (!this.devices || !this.areas || !this.entities) {
return;
}
const devices = this._getDevices(this.devices, this.areas, this.entities);
return html`
<paper-dropdown-menu-light .label=${this.label}>
<paper-listbox
slot="dropdown-content"
.selected=${this._value}
attr-for-selected="data-device-id"
@iron-select=${this._deviceChanged}
<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}
@value-changed=${this._deviceChanged}
>
<paper-input
.label=${this.label}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
<paper-item data-device-id="">
No device
</paper-item>
${this._sortedDevices(this.devices).map(
(device) => html`
<paper-item data-device-id=${device.id}>
${device.name_by_user || device.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this.value
? html`
<paper-icon-button
aria-label="Clear"
slot="suffix"
class="clear-button"
icon="hass:close"
no-ripple
>
Clear
</paper-icon-button>
`
: ""}
${devices.length > 0
? html`
<paper-icon-button
aria-label="Show devices"
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</paper-icon-button>
`
: ""}
</paper-input>
</vaadin-combo-box-light>
`;
}
@ -76,8 +189,12 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
return this.value || "";
}
private _deviceChanged(ev) {
const newValue = ev.detail.item.dataset.deviceId;
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _deviceChanged(ev: PolymerChangedEvent<string>) {
const newValue = ev.detail.value;
if (newValue !== this._value) {
this.value = newValue;
@ -88,16 +205,30 @@ class HaDevicePicker extends SubscribeMixin(LitElement) {
}
}
private _fallbackDeviceName(
deviceId: string,
deviceEntityLookup: DeviceEntityLookup
): string | undefined {
for (const entity of deviceEntityLookup[deviceId] || []) {
const stateObj = this.hass.states[entity.entity_id];
if (stateObj) {
return computeStateName(stateObj);
}
}
return undefined;
}
static get styles(): CSSResult {
return css`
paper-dropdown-menu-light {
width: 100%;
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
paper-listbox {
min-width: 200px;
}
paper-item {
cursor: pointer;
[hidden] {
display: none;
}
`;
}

View File

@ -11,7 +11,6 @@ import {
import { HassEntity } from "home-assistant-js-websocket";
import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
@ -90,16 +89,6 @@ export class HaStateLabelBadge extends LitElement {
`;
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.addEventListener("click", (ev) => {
ev.stopPropagation();
if (this.state) {
fireEvent(this, "hass-more-info", { entityId: this.state.entity_id });
}
});
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);

View File

@ -1,8 +1,9 @@
import { Constructor, customElement, CSSResult, css } from "lit-element";
import { customElement, CSSResult, css } from "lit-element";
import "@material/mwc-checkbox";
// tslint:disable-next-line
import { Checkbox } from "@material/mwc-checkbox";
import { style } from "@material/mwc-checkbox/mwc-checkbox-css";
import { Constructor } from "../types";
// tslint:disable-next-line
const MwcCheckbox = customElements.get("mwc-checkbox") as Constructor<Checkbox>;

View File

@ -1,12 +1,8 @@
import {
classMap,
html,
customElement,
Constructor,
} from "@material/mwc-base/base-element";
import { classMap, html, customElement } from "@material/mwc-base/base-element";
import { ripple } from "@material/mwc-ripple/ripple-directive.js";
import "@material/mwc-fab";
import { Constructor } from "../types";
// tslint:disable-next-line
import { Fab } from "@material/mwc-fab";
// tslint:disable-next-line

View File

@ -1,265 +0,0 @@
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-paper-slider";
import { EventsMixin } from "../mixins/events-mixin";
/*
* @appliesMixin EventsMixin
*/
class HaForm extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
.error {
color: red;
}
paper-checkbox {
display: inline-block;
padding: 22px 0;
}
</style>
<template is="dom-if" if="[[_isArray(schema)]]" restamp="">
<template is="dom-if" if="[[error.base]]">
<div class="error">[[computeError(error.base, schema)]]</div>
</template>
<template is="dom-repeat" items="[[schema]]">
<ha-form
data="[[_getValue(data, item)]]"
schema="[[item]]"
error="[[_getValue(error, item)]]"
on-data-changed="_valueChanged"
compute-error="[[computeError]]"
compute-label="[[computeLabel]]"
compute-suffix="[[computeSuffix]]"
></ha-form>
</template>
</template>
<template is="dom-if" if="[[!_isArray(schema)]]" restamp="">
<template is="dom-if" if="[[error]]">
<div class="error">[[computeError(error, schema)]]</div>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "string")]]'
restamp=""
>
<template
is="dom-if"
if='[[_includes(schema.name, "password")]]'
restamp=""
>
<paper-input
type="[[_passwordFieldType(unmaskedPassword)]]"
label="[[computeLabel(schema)]]"
value="{{data}}"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
>
<paper-icon-button
toggles
active="{{unmaskedPassword}}"
slot="suffix"
icon="[[_passwordFieldIcon(unmaskedPassword)]]"
id="iconButton"
title="Click to toggle between masked and clear password"
>
</paper-icon-button>
</paper-input>
</template>
<template
is="dom-if"
if='[[!_includes(schema.name, "password")]]'
restamp=""
>
<paper-input
label="[[computeLabel(schema)]]"
value="{{data}}"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
></paper-input>
</template>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "integer")]]'
restamp=""
>
<template is="dom-if" if="[[_isRange(schema)]]" restamp="">
<div>
[[computeLabel(schema)]]
<ha-paper-slider
pin=""
value="{{data}}"
min="[[schema.valueMin]]"
max="[[schema.valueMax]]"
></ha-paper-slider>
</div>
</template>
<template is="dom-if" if="[[!_isRange(schema)]]" restamp="">
<paper-input
label="[[computeLabel(schema)]]"
value="{{data}}"
type="number"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
></paper-input>
</template>
</template>
<template is="dom-if" if='[[_equals(schema.type, "float")]]' restamp="">
<!-- TODO -->
<paper-input
label="[[computeLabel(schema)]]"
value="{{data}}"
required="[[schema.required]]"
auto-validate="[[schema.required]]"
error-message="Required"
>
<span suffix="" slot="suffix">[[computeSuffix(schema)]]</span>
</paper-input>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "boolean")]]'
restamp=""
>
<div>
<paper-checkbox checked="{{data}}"
>[[computeLabel(schema)]]</paper-checkbox
>
</div>
</template>
<template
is="dom-if"
if='[[_equals(schema.type, "select")]]'
restamp=""
>
<paper-dropdown-menu label="[[computeLabel(schema)]]">
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
selected="{{data}}"
>
<template is="dom-repeat" items="[[schema.options]]">
<paper-item item-name$="[[_optionValue(item)]]"
>[[_optionLabel(item)]]</paper-item
>
</template>
</paper-listbox>
</paper-dropdown-menu>
</template>
</template>
`;
}
static get properties() {
return {
data: {
type: Object,
notify: true,
},
schema: Object,
error: Object,
// A function that computes the label to be displayed for a given
// schema object.
computeLabel: {
type: Function,
value: () => (schema) => schema && schema.name,
},
// A function that computes the suffix to be displayed for a given
// schema object.
computeSuffix: {
type: Function,
value: () => (schema) =>
schema &&
schema.description &&
schema.description.unit_of_measurement,
},
// A function that computes an error message to be displayed for a
// given error ID, and relevant schema object
computeError: {
type: Function,
value: () => (error, schema) => error, // eslint-disable-line no-unused-vars
},
};
}
focus() {
const input = this.shadowRoot.querySelector(
"ha-form, paper-input, ha-paper-slider, paper-checkbox, paper-dropdown-menu"
);
if (!input) {
return;
}
input.focus();
}
_isArray(val) {
return Array.isArray(val);
}
_isRange(schema) {
return "valueMin" in schema && "valueMax" in schema;
}
_equals(a, b) {
return a === b;
}
_includes(a, b) {
return a.indexOf(b) >= 0;
}
_getValue(obj, item) {
if (obj) {
return obj[item.name];
}
return null;
}
_valueChanged(ev) {
let value = ev.detail.value;
if (ev.model.item.type === "integer") {
value = Number(ev.detail.value);
}
this.set(["data", ev.model.item.name], value);
}
_passwordFieldType(unmaskedPassword) {
return unmaskedPassword ? "text" : "password";
}
_passwordFieldIcon(unmaskedPassword) {
return unmaskedPassword ? "hass:eye-off" : "hass:eye";
}
_optionValue(item) {
return Array.isArray(item) ? item[0] : item;
}
_optionLabel(item) {
return Array.isArray(item) ? item[1] : item;
}
}
customElements.define("ha-form", HaForm);

View File

@ -0,0 +1,70 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
CSSResult,
css,
query,
} from "lit-element";
import {
HaFormElement,
HaFormBooleanData,
HaFormBooleanSchema,
} from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
import "@polymer/paper-checkbox/paper-checkbox";
// Not duplicate, is for typing
// tslint:disable-next-line
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
@customElement("ha-form-boolean")
export class HaFormBoolean extends LitElement implements HaFormElement {
@property() public schema!: HaFormBooleanSchema;
@property() public data!: HaFormBooleanData;
@property() public label!: string;
@property() public suffix!: string;
@query("paper-checkbox") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
return html`
<paper-checkbox .checked=${this.data} @change=${this._valueChanged}>
${this.label}
</paper-checkbox>
`;
}
private _valueChanged(ev: Event) {
fireEvent(
this,
"value-changed",
{
value: (ev.target as PaperCheckboxElement).checked,
},
{ bubbles: false }
);
}
static get styles(): CSSResult {
return css`
paper-checkbox {
display: inline-block;
padding: 22px 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-boolean": HaFormBoolean;
}
}

View File

@ -0,0 +1,69 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
query,
} from "lit-element";
import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
import "@polymer/paper-input/paper-input";
// Not duplicate, is for typing
// tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input";
@customElement("ha-form-float")
export class HaFormFloat extends LitElement implements HaFormElement {
@property() public schema!: HaFormFloatSchema;
@property() public data!: HaFormFloatData;
@property() public label!: string;
@property() public suffix!: string;
@query("paper-input") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
return html`
<paper-input
.label=${this.label}
.value=${this._value}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
>
<span suffix="" slot="suffix">${this.suffix}</span>
</paper-input>
`;
}
private get _value() {
return this.data || 0;
}
private _valueChanged(ev: Event) {
const value = Number((ev.target as PaperInputElement).value);
if (this._value === value) {
return;
}
fireEvent(
this,
"value-changed",
{
value,
},
{ bubbles: false }
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-float": HaFormFloat;
}
}

View File

@ -0,0 +1,89 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
query,
} from "lit-element";
import {
HaFormElement,
HaFormIntegerData,
HaFormIntegerSchema,
} from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-paper-slider";
import "@polymer/paper-input/paper-input";
// Not duplicate, is for typing
// tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { PaperSliderElement } from "@polymer/paper-slider/paper-slider";
@customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement {
@property() public schema!: HaFormIntegerSchema;
@property() public data!: HaFormIntegerData;
@property() public label!: string;
@property() public suffix!: string;
@query("paper-input ha-paper-slider") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
return "valueMin" in this.schema && "valueMax" in this.schema
? html`
<div>
${this.label}
<ha-paper-slider
pin=""
.value=${this._value}
.min=${this.schema.valueMin}
.max=${this.schema.valueMax}
@value-changed=${this._valueChanged}
></ha-paper-slider>
</div>
`
: html`
<paper-input
type="number"
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private get _value() {
return this.data || 0;
}
private _valueChanged(ev: Event) {
const value = Number(
(ev.target as PaperInputElement | PaperSliderElement).value
);
if (this._value === value) {
return;
}
fireEvent(
this,
"value-changed",
{
value,
},
{ bubbles: false }
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-integer": HaFormInteger;
}
}

View File

@ -0,0 +1,119 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
query,
} from "lit-element";
import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-form-positive_time_period_dict")
export class HaFormTimePeriod extends LitElement implements HaFormElement {
@property() public schema!: HaFormTimeSchema;
@property() public data!: HaFormTimeData;
@property() public label!: string;
@property() public suffix!: string;
@query("paper-time-input") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult | void {
return html`
<paper-time-input
.label=${this.label}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
error-message="Required"
enable-second
format="24"
.hour=${this._parseDuration(this._hours)}
.min=${this._parseDuration(this._minutes)}
.sec=${this._parseDuration(this._seconds)}
@hour-changed=${this._hourChanged}
@min-changed=${this._minChanged}
@sec-changed=${this._secChanged}
float-input-labels
no-hours-limit
always-float-input-labels
hour-label="hh"
min-label="mm"
sec-label="ss"
></paper-time-input>
`;
}
private get _hours() {
return this.data && this.data.hours ? Number(this.data.hours) : 0;
}
private get _minutes() {
return this.data && this.data.minutes ? Number(this.data.minutes) : 0;
}
private get _seconds() {
return this.data && this.data.seconds ? Number(this.data.seconds) : 0;
}
private _parseDuration(value) {
return value.toString().padStart(2, "0");
}
private _hourChanged(ev) {
this._durationChanged(ev, "hours");
}
private _minChanged(ev) {
this._durationChanged(ev, "minutes");
}
private _secChanged(ev) {
this._durationChanged(ev, "seconds");
}
private _durationChanged(ev, unit) {
let value = Number(ev.detail.value);
if (value === this[`_${unit}`]) {
return;
}
let hours = this._hours;
let minutes = this._minutes;
if (unit === "seconds" && value > 59) {
minutes = minutes + Math.floor(value / 60);
value %= 60;
}
if (unit === "minutes" && value > 59) {
hours = hours + Math.floor(value / 60);
value %= 60;
}
fireEvent(
this,
"value-changed",
{
value: {
hours,
minutes,
seconds: this._seconds,
...{ [unit]: value },
},
},
{ bubbles: false }
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-positive_time_period_dict": HaFormTimePeriod;
}
}

View File

@ -0,0 +1,78 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
query,
} from "lit-element";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-item/paper-item";
@customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement {
@property() public schema!: HaFormSelectSchema;
@property() public data!: HaFormSelectData;
@property() public label!: string;
@property() public suffix!: string;
@query("paper-dropdown-menu") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
return html`
<paper-dropdown-menu .label=${this.label}>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
.selected=${this.data}
@selected-item-changed=${this._valueChanged}
>
${this.schema.options!.map(
(item) => html`
<paper-item .itemValue=${this._optionValue(item)}>
${this._optionLabel(item)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
`;
}
private _optionValue(item) {
return Array.isArray(item) ? item[0] : item;
}
private _optionLabel(item) {
return Array.isArray(item) ? item[1] : item;
}
private _valueChanged(ev: CustomEvent) {
if (!ev.detail.value) {
return;
}
fireEvent(
this,
"value-changed",
{
value: ev.detail.value.itemValue,
},
{ bubbles: false }
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-select": HaFormSelect;
}
}

View File

@ -0,0 +1,93 @@
import {
customElement,
LitElement,
html,
property,
TemplateResult,
query,
} from "lit-element";
import { HaFormElement, HaFormStringData, HaFormStringSchema } from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-icon-button/paper-icon-button";
// Not duplicate, is for typing
// tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input";
@customElement("ha-form-string")
export class HaFormString extends LitElement implements HaFormElement {
@property() public schema!: HaFormStringSchema;
@property() public data!: HaFormStringData;
@property() public label!: string;
@property() public suffix!: string;
@property() private _unmaskedPassword = false;
@query("paper-input") private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
return this.schema.name.includes("password")
? html`
<paper-input
.type=${this._unmaskedPassword ? "text" : "password"}
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
>
<paper-icon-button
toggles
.active=${this._unmaskedPassword}
slot="suffix"
.icon=${this._unmaskedPassword ? "hass:eye-off" : "hass:eye"}
id="iconButton"
title="Click to toggle between masked and clear password"
@click=${this._toggleUnmaskedPassword}
>
</paper-icon-button>
</paper-input>
`
: html`
<paper-input
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
error-message="Required"
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _toggleUnmaskedPassword(ev: Event) {
this._unmaskedPassword = (ev.target as any).active;
}
private _valueChanged(ev: Event) {
const value = (ev.target as PaperInputElement).value;
if (this.data === value) {
return;
}
fireEvent(
this,
"value-changed",
{
value,
},
{ bubbles: false }
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-string": HaFormString;
}
}

View File

@ -0,0 +1,237 @@
import {
customElement,
LitElement,
html,
property,
query,
CSSResult,
css,
PropertyValues,
} from "lit-element";
import "./ha-form-string";
import "./ha-form-integer";
import "./ha-form-float";
import "./ha-form-boolean";
import "./ha-form-select";
import "./ha-form-positive_time_period_dict";
import { fireEvent } from "../../common/dom/fire_event";
export type HaFormSchema =
| HaFormStringSchema
| HaFormIntegerSchema
| HaFormFloatSchema
| HaFormBooleanSchema
| HaFormSelectSchema
| HaFormTimeSchema;
export interface HaFormBaseSchema {
name: string;
default?: HaFormData;
required?: boolean;
optional?: boolean;
description?: { suffix?: string };
}
export interface HaFormIntegerSchema extends HaFormBaseSchema {
type: "integer";
default?: HaFormIntegerData;
valueMin?: number;
valueMax?: number;
}
export interface HaFormSelectSchema extends HaFormBaseSchema {
type: "select";
options?: string[];
}
export interface HaFormFloatSchema extends HaFormBaseSchema {
type: "float";
}
export interface HaFormStringSchema extends HaFormBaseSchema {
type: "string";
}
export interface HaFormBooleanSchema extends HaFormBaseSchema {
type: "boolean";
}
export interface HaFormTimeSchema extends HaFormBaseSchema {
type: "time";
}
export interface HaFormDataContainer {
[key: string]: HaFormData;
}
export type HaFormData =
| HaFormStringData
| HaFormIntegerData
| HaFormFloatData
| HaFormBooleanData
| HaFormSelectData
| HaFormTimeData;
export type HaFormStringData = string;
export type HaFormIntegerData = number;
export type HaFormFloatData = number;
export type HaFormBooleanData = boolean;
export type HaFormSelectData = string;
export interface HaFormTimeData {
hours?: number;
minutes?: number;
seconds?: number;
}
export interface HaFormElement extends LitElement {
schema: HaFormSchema;
data: HaFormDataContainer | HaFormData;
label?: string;
suffix?: string;
}
@customElement("ha-form")
export class HaForm extends LitElement implements HaFormElement {
@property() public data!: HaFormDataContainer | HaFormData;
@property() public schema!: HaFormSchema;
@property() public error;
@property() public computeError?: (schema: HaFormSchema, error) => string;
@property() public computeLabel?: (schema: HaFormSchema) => string;
@property() public computeSuffix?: (schema: HaFormSchema) => string;
@query("ha-form") private _childForm?: HaForm;
@query("#element") private _elementContainer?: HTMLDivElement;
public focus() {
const input = this._childForm
? this._childForm
: this._elementContainer
? this._elementContainer.lastChild
: undefined;
if (!input) {
return;
}
(input as HTMLElement).focus();
}
protected render() {
if (Array.isArray(this.schema)) {
return html`
${this.error && this.error.base
? html`
<div class="error">
${this._computeError(this.error.base, this.schema)}
</div>
`
: ""}
${this.schema.map(
(item) => html`
<ha-form
.data=${this._getValue(this.data, item)}
.schema=${item}
.error=${this._getValue(this.error, item)}
@value-changed=${this._valueChanged}
.computeError=${this.computeError}
.computeLabel=${this.computeLabel}
.computeSuffix=${this.computeSuffix}
></ha-form>
`
)}
`;
}
return html`
${this.error
? html`
<div class="error">
${this._computeError(this.error, this.schema)}
</div>
`
: ""}
<div id="element" @value-changed=${this._valueChanged}></div>
`;
}
protected updated(changedProperties: PropertyValues) {
const schemaChanged = changedProperties.has("schema");
const oldSchema = schemaChanged
? changedProperties.get("schema")
: undefined;
if (
!Array.isArray(this.schema) &&
schemaChanged &&
(!oldSchema || (oldSchema as HaFormSchema).type !== this.schema.type)
) {
const element = document.createElement(
`ha-form-${this.schema.type}`
) as HaFormElement;
element.schema = this.schema;
element.data = this.data;
element.label = this._computeLabel(this.schema);
element.suffix = this._computeSuffix(this.schema);
if (this._elementContainer!.lastChild) {
this._elementContainer!.removeChild(this._elementContainer!.lastChild);
}
this._elementContainer!.append(element);
} else if (this._elementContainer && this._elementContainer.lastChild) {
const element = this._elementContainer!.lastChild as HaFormElement;
element.schema = this.schema;
element.data = this.data;
element.label = this._computeLabel(this.schema);
element.suffix = this._computeSuffix(this.schema);
}
}
private _computeLabel(schema: HaFormSchema) {
return this.computeLabel
? this.computeLabel(schema)
: schema
? schema.name
: "";
}
private _computeSuffix(schema: HaFormSchema) {
return this.computeSuffix
? this.computeSuffix(schema)
: schema && schema.description
? schema.description.suffix
: "";
}
private _computeError(error, schema: HaFormSchema) {
return this.computeError ? this.computeError(error, schema) : error;
}
private _getValue(obj, item) {
if (obj) {
return obj[item.name];
}
return null;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema;
const data = this.data as HaFormDataContainer;
data[schema.name] = ev.detail.value;
fireEvent(this, "value-changed", {
value: { ...data },
});
}
static get styles(): CSSResult {
return css`
.error {
color: var(--error-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form": HaForm;
}
}

View File

@ -1,4 +1,5 @@
import { Constructor } from "lit-element";
import { Constructor } from "../types";
import "@polymer/iron-icon/iron-icon";
// Not duplicate, this is for typing.
// tslint:disable-next-line

View File

@ -1,31 +1,21 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
CSSResult,
css,
property,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "./ha-icon";
class HaLabelBadge extends LitElement {
public value?: string;
public icon?: string;
public label?: string;
public description?: string;
public image?: string;
static get properties(): PropertyDeclarations {
return {
value: {},
icon: {},
label: {},
description: {},
image: {},
};
}
@property() public value?: string;
@property() public icon?: string;
@property() public label?: string;
@property() public description?: string;
@property() public image?: string;
protected render(): TemplateResult | void {
return html`

View File

@ -1,6 +1,6 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import { Constructor } from "lit-element";
import { PolymerElement } from "@polymer/polymer";
import { Constructor } from "../types";
const paperDropdownClass = customElements.get(
"paper-dropdown-menu"

View File

@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@ -1,5 +1,5 @@
import { Constructor } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { Constructor } from "../types";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";

View File

@ -1,8 +1,9 @@
import { Constructor, customElement, CSSResult, css, query } from "lit-element";
import { customElement, CSSResult, css, query } from "lit-element";
import "@material/mwc-switch";
import { style } from "@material/mwc-switch/mwc-switch-css";
// tslint:disable-next-line
import { Switch } from "@material/mwc-switch";
import { Constructor } from "../types";
// tslint:disable-next-line
const MwcSwitch = customElements.get("mwc-switch") as Constructor<Switch>;
@ -12,7 +13,10 @@ export class HaSwitch extends MwcSwitch {
protected firstUpdated() {
super.firstUpdated();
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
this.style.setProperty(
"--mdc-theme-secondary",
"var(--switch-checked-color)"
);
this.classList.toggle(
"slotted",
Boolean(this._slot.assignedNodes().length)
@ -29,12 +33,12 @@ export class HaSwitch extends MwcSwitch {
align-items: center;
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb {
background-color: var(--paper-toggle-button-unchecked-button-color);
border-color: var(--paper-toggle-button-unchecked-button-color);
background-color: var(--switch-unchecked-button-color);
border-color: var(--switch-unchecked-button-color);
}
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track {
background-color: var(--paper-toggle-button-unchecked-bar-color);
border-color: var(--paper-toggle-button-unchecked-bar-color);
background-color: var(--switch-unchecked-track-color);
border-color: var(--switch-unchecked-track-color);
}
:host(.slotted) .mdc-switch {
margin-right: 24px;

View File

@ -87,6 +87,10 @@ export class PaperTimeInput extends PolymerElement {
label {
@apply --paper-font-caption;
color: var(
--paper-input-container-color,
var(--secondary-text-color)
);
}
.time-input-wrap {
@ -106,14 +110,17 @@ export class PaperTimeInput extends PolymerElement {
id="hour"
type="number"
value="{{hour}}"
label="[[hourLabel]]"
on-change="_shouldFormatHour"
required=""
on-focus="_onFocus"
required
prevent-invalid-input
auto-validate="[[autoValidate]]"
prevent-invalid-input=""
maxlength="2"
max="[[_computeHourMax(format)]]"
min="0"
no-label-float=""
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
>
<span suffix="" slot="suffix">:</span>
@ -124,15 +131,40 @@ export class PaperTimeInput extends PolymerElement {
id="min"
type="number"
value="{{min}}"
label="[[minLabel]]"
on-change="_formatMin"
required=""
on-focus="_onFocus"
required
auto-validate="[[autoValidate]]"
prevent-invalid-input=""
prevent-invalid-input
maxlength="2"
max="59"
min="0"
no-label-float=""
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
>
<span hidden$="[[!enableSecond]]" suffix slot="suffix">:</span>
</paper-input>
<!-- Sec Input -->
<paper-input
id="sec"
type="number"
value="{{sec}}"
label="[[secLabel]]"
on-change="_formatSec"
on-focus="_onFocus"
required
auto-validate="[[autoValidate]]"
prevent-invalid-input
maxlength="2"
max="59"
min="0"
no-label-float$="[[!floatInputLabels]]"
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
hidden$="[[!enableSecond]]"
>
</paper-input>
@ -180,6 +212,20 @@ export class PaperTimeInput extends PolymerElement {
type: Boolean,
value: false,
},
/**
* float the input labels
*/
floatInputLabels: {
type: Boolean,
value: false,
},
/**
* always float the input labels
*/
alwaysFloatInputLabels: {
type: Boolean,
value: false,
},
/**
* 12 or 24 hr format
*/
@ -208,6 +254,48 @@ export class PaperTimeInput extends PolymerElement {
type: String,
notify: true,
},
/**
* second
*/
sec: {
type: String,
notify: true,
},
/**
* Suffix for the hour input
*/
hourLabel: {
type: String,
value: "",
},
/**
* Suffix for the min input
*/
minLabel: {
type: String,
value: ":",
},
/**
* Suffix for the sec input
*/
secLabel: {
type: String,
value: "",
},
/**
* show the sec field
*/
enableSecond: {
type: Boolean,
value: false,
},
/**
* limit hours input
*/
noHoursLimit: {
type: Boolean,
value: false,
},
/**
* AM or PM
*/
@ -223,7 +311,7 @@ export class PaperTimeInput extends PolymerElement {
type: String,
notify: true,
readOnly: true,
computed: "_computeTime(min, hour, amPm)",
computed: "_computeTime(min, hour, sec, amPm)",
},
};
}
@ -238,6 +326,10 @@ export class PaperTimeInput extends PolymerElement {
if (!this.$.hour.validate() | !this.$.min.validate()) {
valid = false;
}
// Validate second field
if (this.enableSecond && !this.$.sec.validate()) {
valid = false;
}
// Validate AM PM if 12 hour time
if (this.format === 12 && !this.$.dropdown.validate()) {
valid = false;
@ -248,15 +340,37 @@ export class PaperTimeInput extends PolymerElement {
/**
* Create time string
*/
_computeTime(min, hour, amPm) {
if (hour && min) {
// No ampm on 24 hr time
if (this.format === 24) {
amPm = "";
_computeTime(min, hour, sec, amPm) {
let str;
if (hour || min || (sec && this.enableSecond)) {
hour = hour || "00";
min = min || "00";
sec = sec || "00";
str = hour + ":" + min;
// add sec field
if (this.enableSecond && sec) {
str = str + ":" + sec;
}
// No ampm on 24 hr time
if (this.format === 12) {
str = str + " " + amPm;
}
return hour + ":" + min + " " + amPm;
}
return undefined;
return str;
}
_onFocus(ev) {
ev.target.inputElement.inputElement.select();
}
/**
* Format sec
*/
_formatSec() {
if (this.sec.toString().length === 1) {
this.sec = this.sec.toString().padStart(2, "0");
}
}
/**
@ -264,16 +378,16 @@ export class PaperTimeInput extends PolymerElement {
*/
_formatMin() {
if (this.min.toString().length === 1) {
this.min = this.min < 10 ? "0" + this.min : this.min;
this.min = this.min.toString().padStart(2, "0");
}
}
/**
* Hour needs a leading zero in 24hr format
* Format hour
*/
_shouldFormatHour() {
if (this.format === 24 && this.hour.toString().length === 1) {
this.hour = this.hour < 10 ? "0" + this.hour : this.hour;
this.hour = this.hour.toString().padStart(2, "0");
}
}
@ -281,6 +395,9 @@ export class PaperTimeInput extends PolymerElement {
* 24 hour format has a max hr of 23
*/
_computeHourMax(format) {
if (this.noHoursLimit) {
return null;
}
if (format === 12) {
return format;
}

View File

@ -181,23 +181,63 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
state.attributes.target_temp_low
);
addColumn(name + " current temperature", true);
addColumn(
`${this.hass.localize(
"ui.card.climate.current_temperature",
"name",
name
)}`,
true
);
if (hasHeat) {
addColumn(name + " heating", true, true);
addColumn(
`${this.hass.localize("ui.card.climate.heating", "name", name)}`,
true,
true
);
// The "heating" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasCool) {
addColumn(name + " cooling", true, true);
addColumn(
`${this.hass.localize("ui.card.climate.cooling", "name", name)}`,
true,
true
);
// The "cooling" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasTargetRange) {
addColumn(name + " target temperature high", true);
addColumn(name + " target temperature low", true);
addColumn(
`${this.hass.localize(
"ui.card.climate.target_temperature_mode",
"name",
name,
"mode",
this.hass.localize("ui.card.climate.high")
)}`,
true
);
addColumn(
`${this.hass.localize(
"ui.card.climate.target_temperature_mode",
"name",
name,
"mode",
this.hass.localize("ui.card.climate.low")
)}`,
true
);
} else {
addColumn(name + " target temperature", true);
addColumn(
`${this.hass.localize(
"ui.card.climate.target_temperature_entity",
"name",
name
)}`,
true
);
}
states.states.forEach((state) => {

View File

@ -18,19 +18,20 @@ import { fireEvent } from "../../common/dom/fire_event";
import { User, fetchUsers } from "../../data/user";
import { compare } from "../../common/string/compare";
class HaEntityPicker extends LitElement {
class HaUserPicker extends LitElement {
public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public users?: User[];
private _sortedUsers = memoizeOne((users?: User[]) => {
if (!users || users.length === 1) {
return users || [];
if (!users) {
return [];
}
const sorted = [...users];
sorted.sort((a, b) => compare(a.name, b.name));
return sorted;
return users
.filter((user) => !user.system_generated)
.sort((a, b) => compare(a.name, b.name));
});
protected render(): TemplateResult | void {
@ -101,4 +102,4 @@ class HaEntityPicker extends LitElement {
}
}
customElements.define("ha-user-picker", HaEntityPicker);
customElements.define("ha-user-picker", HaUserPicker);

View File

@ -39,6 +39,15 @@ export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
device_id: deviceId,
});
export const fetchDeviceActionCapabilities = (
hass: HomeAssistant,
action: DeviceAction
) =>
hass.callWS<DeviceAction[]>({
type: "device_automation/action/capabilities",
action,
});
export const fetchDeviceConditionCapabilities = (
hass: HomeAssistant,
condition: DeviceCondition
@ -57,7 +66,7 @@ export const fetchDeviceTriggerCapabilities = (
trigger,
});
const whitelist = ["above", "below", "for"];
const whitelist = ["above", "below", "code", "for"];
export const deviceAutomationsEqual = (
a: DeviceAutomation,

View File

@ -11,7 +11,7 @@ export interface LovelaceConfig {
export interface LovelaceViewConfig {
index?: number;
title?: string;
badges?: string[];
badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[];
path?: string;
icon?: string;
@ -25,6 +25,11 @@ export interface ShowViewConfig {
user?: string;
}
export interface LovelaceBadgeConfig {
type?: string;
[key: string]: any;
}
export interface LovelaceCardConfig {
index?: number;
view_index?: number;
@ -32,11 +37,11 @@ export interface LovelaceCardConfig {
[key: string]: any;
}
export interface ToggleActionConfig {
export interface ToggleActionConfig extends BaseActionConfig {
action: "toggle";
}
export interface CallServiceActionConfig {
export interface CallServiceActionConfig extends BaseActionConfig {
action: "call-service";
service: string;
service_data?: {
@ -45,24 +50,37 @@ export interface CallServiceActionConfig {
};
}
export interface NavigateActionConfig {
export interface NavigateActionConfig extends BaseActionConfig {
action: "navigate";
navigation_path: string;
}
export interface UrlActionConfig {
export interface UrlActionConfig extends BaseActionConfig {
action: "url";
url_path: string;
}
export interface MoreInfoActionConfig {
export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info";
}
export interface NoActionConfig {
export interface NoActionConfig extends BaseActionConfig {
action: "none";
}
export interface BaseActionConfig {
confirmation?: ConfirmationRestrictionConfig;
}
export interface ConfirmationRestrictionConfig {
text?: string;
exemptions?: RestrictionConfig[];
}
export interface RestrictionConfig {
user: string;
}
export type ActionConfig =
| ToggleActionConfig
| CallServiceActionConfig
@ -108,3 +126,7 @@ export const getLovelaceCollection = (conn: Connection) =>
export interface WindowWithLovelaceProm extends Window {
llConfProm?: Promise<LovelaceConfig>;
}
export interface LongPressOptions {
hasDoubleClick?: boolean;
}

View File

@ -60,7 +60,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
@opened-changed="${this._openedChanged}"
>
<h2>
${this.hass.localize("ui.dialogs.config_entry_system_options.title")}
${this.hass.localize(
"ui.dialogs.config_entry_system_options.title",
"integration",
this.hass.localize(
`component.${this._params.entry.domain}.config.title`
) || this._params.entry.domain
)}
</h2>
<paper-dialog-scrollable>
${this._loading
@ -89,7 +95,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
</p>
<p class="secondary">
${this.hass.localize(
"ui.dialogs.config_entry_system_options.enable_new_entities_description"
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
"integration",
this.hass.localize(
`component.${
this._params.entry.domain
}.config.title`
) || this._params.entry.domain
)}
</p>
</div>

View File

@ -14,7 +14,7 @@ import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-spinner/paper-spinner";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import "../../components/ha-form";
import "../../components/ha-form/ha-form";
import "../../components/ha-markdown";
import "../../resources/ha-style";
import "../../components/dialog/ha-paper-dialog";
@ -141,6 +141,7 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
></step-flow-pick-handler>
`
: this._step.type === "form"

View File

@ -75,6 +75,7 @@ export interface DataEntryFlowDialogParams {
continueFlowId?: string;
dialogClosedCallback?: (params: { flowFinished: boolean }) => void;
flowConfig: FlowConfig;
showAdvanced?: boolean;
}
export const loadDataEntryFlowDialog = () =>

View File

@ -12,10 +12,9 @@ import "@material/mwc-button";
import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-spinner/paper-spinner";
import "../../components/ha-form";
import "../../components/ha-form/ha-form";
import "../../components/ha-markdown";
import "../../resources/ha-style";
import { PolymerChangedEvent, applyPolymerEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { configFlowContentStyles } from "./styles";
@ -69,7 +68,7 @@ class StepFlowForm extends LitElement {
${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)}
<ha-form
.data=${stepData}
@data-changed=${this._stepDataChanged}
@value-changed=${this._stepDataChanged}
.schema=${step.data_schema}
.error=${step.errors}
.computeLabel=${this._labelCallback}
@ -169,8 +168,8 @@ class StepFlowForm extends LitElement {
}
}
private _stepDataChanged(ev: PolymerChangedEvent<any>): void {
this._stepData = applyPolymerEvent(ev, this._stepData);
private _stepDataChanged(ev: CustomEvent): void {
this._stepData = ev.detail.value;
}
private _labelCallback = (field: FieldSchema): string =>

View File

@ -31,6 +31,7 @@ class StepFlowPickHandler extends LitElement {
@property() public hass!: HomeAssistant;
@property() public handlers!: string[];
@property() public showAdvanced?: boolean;
@property() private filter?: string;
private _width?: number;
@ -79,18 +80,24 @@ class StepFlowPickHandler extends LitElement {
`
)}
</div>
<p>
${this.hass.localize(
"ui.panel.config.integrations.note_about_integrations"
)}<br />
${this.hass.localize(
"ui.panel.config.integrations.note_about_website_reference"
)}<a href="https://www.home-assistant.io/integrations/"
>${this.hass.localize(
"ui.panel.config.integrations.home_assistant_website"
)}.</a
>
</p>
${this.showAdvanced
? html`
<p>
${this.hass.localize(
"ui.panel.config.integrations.note_about_integrations"
)}<br />
${this.hass.localize(
"ui.panel.config.integrations.note_about_website_reference"
)}<a
href="https://www.home-assistant.io/integrations/"
target="_blank"
>${this.hass.localize(
"ui.panel.config.integrations.home_assistant_website"
)}</a
>.
</p>
`
: ""}
`;
}
@ -133,6 +140,11 @@ class StepFlowPickHandler extends LitElement {
}
p {
text-align: center;
padding: 16px;
margin: 0;
}
p > a {
color: var(--primary-color);
}
`;
}

View File

@ -0,0 +1,107 @@
import {
LitElement,
html,
css,
CSSResult,
TemplateResult,
customElement,
property,
} from "lit-element";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-switch";
import { HomeAssistant } from "../../types";
import { ConfirmationDialogParams } from "./show-dialog-confirmation";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
@customElement("dialog-confirmation")
class DialogConfirmation extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _params?: ConfirmationDialogParams;
public async showDialog(params: ConfirmationDialogParams): Promise<void> {
this._params = params;
}
protected render(): TemplateResult | void {
if (!this._params) {
return html``;
}
return html`
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
>
<h2>
${this._params.title
? this._params.title
: this.hass.localize("ui.dialogs.confirmation.title")}
</h2>
<paper-dialog-scrollable>
<p>${this._params.text}</p>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<mwc-button @click="${this._dismiss}">
${this.hass.localize("ui.dialogs.confirmation.cancel")}
</mwc-button>
<mwc-button @click="${this._confirm}">
${this.hass.localize("ui.dialogs.confirmation.ok")}
</mwc-button>
</div>
</ha-paper-dialog>
`;
}
private async _dismiss(): Promise<void> {
this._params = undefined;
}
private async _confirm(): Promise<void> {
this._params!.confirm();
this._dismiss();
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-paper-dialog {
min-width: 400px;
max-width: 500px;
}
@media (max-width: 400px) {
ha-paper-dialog {
min-width: initial;
}
}
p {
margin: 0;
padding-top: 6px;
padding-bottom: 24px;
color: var(--primary-text-color);
}
.secondary {
color: var(--secondary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-confirmation": DialogConfirmation;
}
}

View File

@ -0,0 +1,21 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface ConfirmationDialogParams {
title?: string;
text: string;
confirm: () => void;
}
export const loadConfirmationDialog = () =>
import(/* webpackChunkName: "confirmation" */ "./dialog-confirmation");
export const showConfirmationDialog = (
element: HTMLElement,
systemLogDetailParams: ConfirmationDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-confirmation",
dialogImport: loadConfirmationDialog,
dialogParams: systemLogDetailParams,
});
};

View File

@ -29,8 +29,9 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
width: 80px;
}
.actions mwc-button {
min-width: 160px;
margin-bottom: 16px;
flex: 1 0 50%;
margin: 0 4px 16px;
max-width: 200px;
}
mwc-button.disarm {
color: var(--google-red-500);
@ -137,7 +138,7 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
<div class="layout horizontal center-justified actions">
<template is="dom-if" if="[[_disarmVisible]]">
<mwc-button
raised
outlined
class="disarm"
on-click="_callService"
data-service="alarm_disarm"
@ -148,7 +149,7 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
</template>
<template is="dom-if" if="[[_armVisible]]">
<mwc-button
raised
outlined
on-click="_callService"
data-service="alarm_arm_home"
disabled="[[!_codeValid]]"
@ -156,7 +157,7 @@ class MoreInfoAlarmControlPanel extends LocalizeMixin(PolymerElement) {
[[localize('ui.card.alarm_control_panel.arm_home')]]
</mwc-button>
<mwc-button
raised
outlined
on-click="_callService"
data-service="alarm_arm_away"
disabled="[[!_codeValid]]"

View File

@ -1,8 +1,4 @@
import {
PropertyDeclarations,
PropertyValues,
UpdatingElement,
} from "lit-element";
import { PropertyValues, UpdatingElement, property } from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import "./more-info-alarm_control_panel";
@ -10,6 +6,7 @@ import "./more-info-automation";
import "./more-info-camera";
import "./more-info-climate";
import "./more-info-configurator";
import "./more-info-counter";
import "./more-info-cover";
import "./more-info-default";
import "./more-info-fan";
@ -32,17 +29,10 @@ import dynamicContentUpdater from "../../../common/dom/dynamic_content_updater";
import { HomeAssistant } from "../../../types";
class MoreInfoContent extends UpdatingElement {
public hass?: HomeAssistant;
public stateObj?: HassEntity;
@property() public hass?: HomeAssistant;
@property() public stateObj?: HassEntity;
private _detachedChild?: ChildNode;
static get properties(): PropertyDeclarations {
return {
hass: {},
stateObj: {},
};
}
protected firstUpdated(): void {
this.style.position = "relative";
this.style.display = "block";

View File

@ -0,0 +1,70 @@
import {
LitElement,
html,
TemplateResult,
CSSResult,
css,
property,
customElement,
} from "lit-element";
import "@material/mwc-button";
import { HassEntity } from "home-assistant-js-websocket";
import { HomeAssistant } from "../../../types";
@customElement("more-info-counter")
class MoreInfoCounter extends LitElement {
@property() public hass!: HomeAssistant;
@property() public stateObj?: HassEntity;
protected render(): TemplateResult | void {
if (!this.hass || !this.stateObj) {
return html``;
}
return html`
<div class="actions">
<mwc-button
.action="${"increment"}"
@click="${this._handleActionClick}"
>
${this.hass!.localize("ui.card.counter.actions.increment")}
</mwc-button>
<mwc-button
.action="${"decrement"}"
@click="${this._handleActionClick}"
>
${this.hass!.localize("ui.card.counter.actions.decrement")}
</mwc-button>
<mwc-button .action="${"reset"}" @click="${this._handleActionClick}">
${this.hass!.localize("ui.card.counter.actions.reset")}
</mwc-button>
</div>
`;
}
private _handleActionClick(e: MouseEvent): void {
const action = (e.currentTarget as any).action;
this.hass.callService("counter", action, {
entity_id: this.stateObj!.entity_id,
});
}
static get styles(): CSSResult {
return css`
.actions {
margin: 0 8px;
padding-top: 20px;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-counter": MoreInfoCounter;
}
}

View File

@ -1,83 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-relative-time";
import LocalizeMixin from "../../../mixins/localize-mixin";
import formatTime from "../../../common/datetime/format_time";
class MoreInfoSun extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex iron-flex-alignment"></style>
<template
is="dom-repeat"
items="[[computeOrder(risingDate, settingDate)]]"
>
<div class="data-entry layout justified horizontal">
<div class="key">
<span>[[itemCaption(item)]]</span>
<ha-relative-time
hass="[[hass]]"
datetime-obj="[[itemDate(item)]]"
></ha-relative-time>
</div>
<div class="value">[[itemValue(item)]]</div>
</div>
</template>
<div class="data-entry layout justified horizontal">
<div class="key">
[[localize('ui.dialogs.more_info_control.sun.elevation')]]
</div>
<div class="value">[[stateObj.attributes.elevation]]</div>
</div>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
risingDate: {
type: Object,
computed: "computeRising(stateObj)",
},
settingDate: {
type: Object,
computed: "computeSetting(stateObj)",
},
};
}
computeRising(stateObj) {
return new Date(stateObj.attributes.next_rising);
}
computeSetting(stateObj) {
return new Date(stateObj.attributes.next_setting);
}
computeOrder(risingDate, settingDate) {
return risingDate > settingDate ? ["set", "ris"] : ["ris", "set"];
}
itemCaption(type) {
if (type === "ris") {
return this.localize("ui.dialogs.more_info_control.sun.rising");
}
return this.localize("ui.dialogs.more_info_control.sun.setting");
}
itemDate(type) {
return type === "ris" ? this.risingDate : this.settingDate;
}
itemValue(type) {
return formatTime(this.itemDate(type), this.hass.language);
}
}
customElements.define("more-info-sun", MoreInfoSun);

View File

@ -0,0 +1,84 @@
import {
property,
LitElement,
TemplateResult,
html,
customElement,
CSSResult,
css,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import "../../../components/ha-relative-time";
import formatTime from "../../../common/datetime/format_time";
import { HomeAssistant } from "../../../types";
@customElement("more-info-sun")
class MoreInfoSun extends LitElement {
@property() public hass!: HomeAssistant;
@property() public stateObj?: HassEntity;
protected render(): TemplateResult | void {
if (!this.hass || !this.stateObj) {
return html``;
}
const risingDate = new Date(this.stateObj.attributes.next_rising);
const settingDate = new Date(this.stateObj.attributes.next_setting);
const order = risingDate > settingDate ? ["set", "ris"] : ["ris", "set"];
return html`
${order.map((item) => {
return html`
<div class="row">
<div class="key">
<span
>${item === "ris"
? this.hass.localize(
"ui.dialogs.more_info_control.sun.rising"
)
: this.hass.localize(
"ui.dialogs.more_info_control.sun.setting"
)}</span
>
<ha-relative-time
.hass=${this.hass}
.datetimeObj=${item === "ris" ? risingDate : settingDate}
></ha-relative-time>
</div>
<div class="value">
${formatTime(
item === "ris" ? risingDate : settingDate,
this.hass.language
)}
</div>
</div>
`;
})}
<div class="row">
<div class="key">
${this.hass.localize("ui.dialogs.more_info_control.sun.elevation")}
</div>
<div class="value">${this.stateObj.attributes.elevation}</div>
</div>
`;
}
static get styles(): CSSResult {
return css`
.row {
margin: 0 8px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-sun": MoreInfoSun;
}
}

View File

@ -1,235 +0,0 @@
import "@polymer/iron-icon/iron-icon";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import LocalizeMixin from "../../../mixins/localize-mixin";
/*
* @appliesMixin LocalizeMixin
*/
class MoreInfoWeather extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style>
iron-icon {
color: var(--paper-item-icon-color);
}
.section {
margin: 16px 0 8px 0;
font-size: 1.2em;
}
.flex {
display: flex;
height: 32px;
align-items: center;
}
.main {
flex: 1;
margin-left: 24px;
}
.temp,
.templow {
min-width: 48px;
text-align: right;
}
.templow {
margin: 0 16px;
color: var(--secondary-text-color);
}
.attribution {
color: var(--secondary-text-color);
text-align: center;
}
</style>
<div class="flex">
<iron-icon icon="hass:thermometer"></iron-icon>
<div class="main">
[[localize('ui.card.weather.attributes.temperature')]]
</div>
<div>
[[stateObj.attributes.temperature]] [[getUnit('temperature')]]
</div>
</div>
<template is="dom-if" if="[[_showValue(stateObj.attributes.pressure)]]">
<div class="flex">
<iron-icon icon="hass:gauge"></iron-icon>
<div class="main">
[[localize('ui.card.weather.attributes.air_pressure')]]
</div>
<div>
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
</div>
</div>
</template>
<template is="dom-if" if="[[_showValue(stateObj.attributes.humidity)]]">
<div class="flex">
<iron-icon icon="hass:water-percent"></iron-icon>
<div class="main">
[[localize('ui.card.weather.attributes.humidity')]]
</div>
<div>[[stateObj.attributes.humidity]] %</div>
</div>
</template>
<template is="dom-if" if="[[_showValue(stateObj.attributes.wind_speed)]]">
<div class="flex">
<iron-icon icon="hass:weather-windy"></iron-icon>
<div class="main">
[[localize('ui.card.weather.attributes.wind_speed')]]
</div>
<div>
[[getWind(stateObj.attributes.wind_speed,
stateObj.attributes.wind_bearing, localize)]]
</div>
</div>
</template>
<template is="dom-if" if="[[_showValue(stateObj.attributes.visibility)]]">
<div class="flex">
<iron-icon icon="hass:eye"></iron-icon>
<div class="main">
[[localize('ui.card.weather.attributes.visibility')]]
</div>
<div>[[stateObj.attributes.visibility]] [[getUnit('length')]]</div>
</div>
</template>
<template is="dom-if" if="[[stateObj.attributes.forecast]]">
<div class="section">[[localize('ui.card.weather.forecast')]]:</div>
<template is="dom-repeat" items="[[stateObj.attributes.forecast]]">
<div class="flex">
<template is="dom-if" if="[[_showValue(item.condition)]]">
<iron-icon icon="[[getWeatherIcon(item.condition)]]"></iron-icon>
</template>
<template is="dom-if" if="[[!_showValue(item.templow)]]">
<div class="main">[[computeDateTime(item.datetime)]]</div>
</template>
<template is="dom-if" if="[[_showValue(item.templow)]]">
<div class="main">[[computeDate(item.datetime)]]</div>
<div class="templow">
[[item.templow]] [[getUnit('temperature')]]
</div>
</template>
<div class="temp">
[[item.temperature]] [[getUnit('temperature')]]
</div>
</div>
</template>
</template>
<template is="dom-if" if="stateObj.attributes.attribution">
<div class="attribution">[[stateObj.attributes.attribution]]</div>
</template>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
};
}
constructor() {
super();
this.cardinalDirections = [
"N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW",
"N",
];
this.weatherIcons = {
"clear-night": "hass:weather-night",
cloudy: "hass:weather-cloudy",
exceptional: "hass:alert-circle-outline",
fog: "hass:weather-fog",
hail: "hass:weather-hail",
lightning: "hass:weather-lightning",
"lightning-rainy": "hass:weather-lightning-rainy",
partlycloudy: "hass:weather-partly-cloudy",
pouring: "hass:weather-pouring",
rainy: "hass:weather-rainy",
snowy: "hass:weather-snowy",
"snowy-rainy": "hass:weather-snowy-rainy",
sunny: "hass:weather-sunny",
windy: "hass:weather-windy",
"windy-variant": "hass:weather-windy-variant",
};
}
computeDate(data) {
const date = new Date(data);
return date.toLocaleDateString(this.hass.language, {
weekday: "long",
month: "short",
day: "numeric",
});
}
computeDateTime(data) {
const date = new Date(data);
return date.toLocaleDateString(this.hass.language, {
weekday: "long",
hour: "numeric",
});
}
getUnit(measure) {
const lengthUnit = this.hass.config.unit_system.length || "";
switch (measure) {
case "air_pressure":
return lengthUnit === "km" ? "hPa" : "inHg";
case "length":
return lengthUnit;
case "precipitation":
return lengthUnit === "km" ? "mm" : "in";
default:
return this.hass.config.unit_system[measure] || "";
}
}
windBearingToText(degree) {
const degreenum = parseInt(degree);
if (isFinite(degreenum)) {
return this.cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
}
return degree;
}
getWind(speed, bearing, localize) {
if (bearing != null) {
const cardinalDirection = this.windBearingToText(bearing);
return `${speed} ${this.getUnit("length")}/h (${localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection})`;
}
return `${speed} ${this.getUnit("length")}/h`;
}
getWeatherIcon(condition) {
return this.weatherIcons[condition];
}
_showValue(item) {
return typeof item !== "undefined" && item !== null;
}
}
customElements.define("more-info-weather", MoreInfoWeather);

View File

@ -0,0 +1,288 @@
import "@polymer/iron-icon/iron-icon";
import {
LitElement,
property,
CSSResult,
css,
customElement,
PropertyValues,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import { TemplateResult, html } from "lit-html";
import { HomeAssistant } from "../../../types";
const cardinalDirections = [
"N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW",
"N",
];
const weatherIcons = {
"clear-night": "hass:weather-night",
cloudy: "hass:weather-cloudy",
exceptional: "hass:alert-circle-outline",
fog: "hass:weather-fog",
hail: "hass:weather-hail",
lightning: "hass:weather-lightning",
"lightning-rainy": "hass:weather-lightning-rainy",
partlycloudy: "hass:weather-partly-cloudy",
pouring: "hass:weather-pouring",
rainy: "hass:weather-rainy",
snowy: "hass:weather-snowy",
"snowy-rainy": "hass:weather-snowy-rainy",
sunny: "hass:weather-sunny",
windy: "hass:weather-windy",
"windy-variant": "hass:weather-windy-variant",
};
@customElement("more-info-weather")
class MoreInfoWeather extends LitElement {
@property() public hass!: HomeAssistant;
@property() public stateObj?: HassEntity;
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("stateObj")) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
!oldHass ||
oldHass.language !== this.hass.language ||
oldHass.config.unit_system !== this.hass.config.unit_system
) {
return true;
}
return false;
}
protected render(): TemplateResult | void {
if (!this.hass || !this.stateObj) {
return html``;
}
return html`
<div class="flex">
<iron-icon icon="hass:thermometer"></iron-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.temperature")}
</div>
<div>
${this.stateObj.attributes.temperature} ${this.getUnit("temperature")}
</div>
</div>
${this.stateObj.attributes.pressure
? html`
<div class="flex">
<iron-icon icon="hass:gauge"></iron-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.air_pressure")}
</div>
<div>
${this.stateObj.attributes.pressure}
${this.getUnit("air_pressure")}
</div>
</div>
`
: ""}
${this.stateObj.attributes.humidity
? html`
<div class="flex">
<iron-icon icon="hass:water-percent"></iron-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.humidity")}
</div>
<div>${this.stateObj.attributes.humidity} %</div>
</div>
`
: ""}
${this.stateObj.attributes.wind_speed
? html`
<div class="flex">
<iron-icon icon="hass:weather-windy"></iron-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.wind_speed")}
</div>
<div>
${this.getWind(
this.stateObj.attributes.wind_speed,
this.stateObj.attributes.wind_bearing
)}
</div>
</div>
`
: ""}
${this.stateObj.attributes.visibility
? html`
<div class="flex">
<iron-icon icon="hass:eye"></iron-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.visibility")}
</div>
<div>
${this.stateObj.attributes.visibility} ${this.getUnit("length")}
</div>
</div>
`
: ""}
${this.stateObj.attributes.forecast
? html`
<div class="section">
${this.hass.localize("ui.card.weather.forecast")}:
</div>
${this.stateObj.attributes.forecast.map((item) => {
return html`
<div class="flex">
${item.condition
? html`
<iron-icon
.icon="${weatherIcons[item.condition]}"
></iron-icon>
`
: ""}
${!item.templow
? html`
<div class="main">
${this.computeDateTime(item.datetime)}
</div>
`
: ""}
${item.templow
? html`
<div class="main">
${this.computeDate(item.datetime)}
</div>
<div class="templow">
${item.templow} ${this.getUnit("temperature")}
</div>
`
: ""};
<div class="temp">
${item.temperature} ${this.getUnit("temperature")}
</div>
</div>
`;
})}
`
: ""}
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
${this.stateObj.attributes.attribution}
</div>
`
: ""}
`;
}
static get styles(): CSSResult {
return css`
iron-icon {
color: var(--paper-item-icon-color);
}
.section {
margin: 16px 0 8px 0;
font-size: 1.2em;
}
.flex {
display: flex;
height: 32px;
align-items: center;
}
.main {
flex: 1;
margin-left: 24px;
}
.temp,
.templow {
min-width: 48px;
text-align: right;
}
.templow {
margin: 0 16px;
color: var(--secondary-text-color);
}
.attribution {
color: var(--secondary-text-color);
text-align: center;
}
`;
}
private computeDate(data) {
const date = new Date(data);
return date.toLocaleDateString(this.hass.language, {
weekday: "long",
month: "short",
day: "numeric",
});
}
private computeDateTime(data) {
const date = new Date(data);
return date.toLocaleDateString(this.hass.language, {
weekday: "long",
hour: "numeric",
});
}
private getUnit(measure: string): string {
const lengthUnit = this.hass.config.unit_system.length || "";
switch (measure) {
case "air_pressure":
return lengthUnit === "km" ? "hPa" : "inHg";
case "length":
return lengthUnit;
case "precipitation":
return lengthUnit === "km" ? "mm" : "in";
default:
return this.hass.config.unit_system[measure] || "";
}
}
private windBearingToText(degree: string): string {
const degreenum = parseInt(degree, 10);
if (isFinite(degreenum)) {
// tslint:disable-next-line: no-bitwise
return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
}
return degree;
}
private getWind(speed: string, bearing: string) {
if (bearing != null) {
const cardinalDirection = this.windBearingToText(bearing);
return `${speed} ${this.getUnit("length")}/h (${this.hass.localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection})`;
}
return `${speed} ${this.getUnit("length")}/h`;
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-weather": MoreInfoWeather;
}
}

View File

@ -1,4 +1,4 @@
import applyThemesOnElement from "../common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { demoConfig } from "./demo_config";
import { demoServices } from "./demo_services";

View File

@ -1,45 +1,20 @@
import {
Constructor,
LitElement,
PropertyDeclarations,
PropertyValues,
} from "lit-element";
import { getLocalLanguage } from "../util/hass-translation";
import { localizeLiteBaseMixin } from "./localize-lite-base-mixin";
import { LitElement, PropertyValues, property } from "lit-element";
import { getLocalLanguage, getTranslation } from "../util/hass-translation";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
import { Constructor, Resources } from "../types";
const empty = () => "";
interface LitLocalizeLiteMixin {
language: string;
resources: {};
translationFragment: string;
localize: LocalizeFunc;
}
export const litLocalizeLiteMixin = <T extends LitElement>(
superClass: Constructor<T>
): Constructor<T & LitLocalizeLiteMixin> =>
// @ts-ignore
class extends localizeLiteBaseMixin(superClass) {
public localize: LocalizeFunc;
static get properties(): PropertyDeclarations {
return {
localize: {},
language: {},
resources: {},
translationFragment: {},
};
}
constructor() {
super();
// This will prevent undefined errors if called before connected to DOM.
this.localize = empty;
// Use browser language setup before login.
this.language = getLocalLanguage();
}
export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
class LitLocalizeLiteClass extends superClass {
// Initialized to empty will prevent undefined errors if called before connected to DOM.
@property() public localize: LocalizeFunc = empty;
@property() public resources?: Resources;
// Use browser language setup before login.
@property() public language?: string = getLocalLanguage();
@property() public translationFragment?: string;
public connectedCallback(): void {
super.connectedCallback();
@ -51,7 +26,7 @@ export const litLocalizeLiteMixin = <T extends LitElement>(
);
}
public updated(changedProperties: PropertyValues) {
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (
changedProperties.has("language") ||
@ -64,4 +39,38 @@ export const litLocalizeLiteMixin = <T extends LitElement>(
);
}
}
};
protected async _initializeLocalizeLite() {
if (this.resources) {
return;
}
if (!this.translationFragment) {
// In dev mode, we will issue a warning if after a second we are still
// not configured correctly.
if (__DEV__) {
setTimeout(
() =>
!this.resources &&
// tslint:disable-next-line
console.error(
"Forgot to pass in resources or set translationFragment for",
this.nodeName
),
1000
);
}
return;
}
const { language, data } = await getTranslation(
this.translationFragment!,
this.language!
);
this.resources = {
[language]: data,
};
}
}
return LitLocalizeLiteClass;
};

View File

@ -1,51 +0,0 @@
/**
* Lite base mixin to add localization without depending on the Hass object.
*/
import { getTranslation } from "../util/hass-translation";
import { Resources } from "../types";
/**
* @polymerMixin
*/
export const localizeLiteBaseMixin = (superClass) =>
class extends superClass {
public resources?: Resources;
public language?: string;
public translationFragment?: string;
protected _initializeLocalizeLite() {
if (this.resources) {
return;
}
if (!this.translationFragment) {
// In dev mode, we will issue a warning if after a second we are still
// not configured correctly.
if (__DEV__) {
setTimeout(
() =>
!this.resources &&
// tslint:disable-next-line
console.error(
"Forgot to pass in resources or set translationFragment for",
this.nodeName
),
1000
);
}
return;
}
this._downloadResources();
}
private async _downloadResources() {
const { language, data } = await getTranslation(
this.translationFragment!,
this.language!
);
this.resources = {
[language]: data,
};
}
};

View File

@ -1,51 +0,0 @@
/**
* Lite mixin to add localization without depending on the Hass object.
*/
import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin";
import { getLocalLanguage } from "../util/hass-translation";
import { localizeLiteBaseMixin } from "./localize-lite-base-mixin";
import { computeLocalize } from "../common/translations/localize";
/**
* @polymerMixin
*/
export const localizeLiteMixin = dedupingMixin(
(superClass) =>
class extends localizeLiteBaseMixin(superClass) {
static get properties() {
return {
language: {
type: String,
// Use browser language setup before login.
value: getLocalLanguage(),
},
resources: Object,
// The fragment to load.
translationFragment: String,
/**
* Translates a string to the current `language`. Any parameters to the
* string should be passed in order, as follows:
* `localize(stringKey, param1Name, param1Value, param2Name, param2Value)`
*/
localize: {
type: Function,
computed: "__computeLocalize(language, resources, formats)",
},
};
}
public ready() {
super.ready();
this._initializeLocalizeLite();
}
protected __computeLocalize(language, resources, formats?) {
return computeLocalize(
this.constructor.prototype,
language,
resources,
formats
);
}
}
);

View File

@ -1,5 +1,5 @@
import { UpdatingElement, Constructor, PropertyValues } from "lit-element";
import { HomeAssistant } from "../types";
import { UpdatingElement, PropertyValues } from "lit-element";
import { HomeAssistant, Constructor } from "../types";
export interface ProvideHassElement {
provideHass(element: HTMLElement);
@ -7,9 +7,9 @@ export interface ProvideHassElement {
/* tslint:disable */
export const ProvideHassLitMixin = <T extends UpdatingElement>(
superClass: Constructor<T>
): Constructor<T & ProvideHassElement> =>
export const ProvideHassLitMixin = <T extends Constructor<UpdatingElement>>(
superClass: T
) =>
// @ts-ignore
class extends superClass {
protected hass!: HomeAssistant;

View File

@ -1,32 +1,21 @@
import {
LitElement,
Constructor,
PropertyValues,
PropertyDeclarations,
} from "lit-element";
import { LitElement, PropertyValues, property } from "lit-element";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { HomeAssistant, Constructor } from "../types";
export interface HassSubscribeElement {
hassSubscribe(): UnsubscribeFunc[];
}
/* tslint:disable-next-line */
export const SubscribeMixin = <T extends LitElement>(
superClass: Constructor<T>
): Constructor<T & HassSubscribeElement> =>
// @ts-ignore
class extends superClass {
private hass?: HomeAssistant;
export const SubscribeMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
class SubscribeClass extends superClass {
@property() public hass?: HomeAssistant;
/* tslint:disable-next-line */
private __unsubs?: UnsubscribeFunc[];
static get properties(): PropertyDeclarations {
return {
hass: {},
};
}
public connectedCallback() {
super.connectedCallback();
this.__checkSubscribed();
@ -50,7 +39,6 @@ export const SubscribeMixin = <T extends LitElement>(
}
protected hassSubscribe(): UnsubscribeFunc[] {
super.hassSubscribe();
return [];
}
@ -64,4 +52,6 @@ export const SubscribeMixin = <T extends LitElement>(
}
this.__unsubs = this.hassSubscribe();
}
};
}
return SubscribeClass;
};

View File

@ -74,6 +74,7 @@ class IntegrationBadge extends LitElement {
.title {
min-height: 2.3em;
word-break: break-word;
}
`;
}

View File

@ -2,9 +2,9 @@ import {
LitElement,
html,
css,
PropertyDeclarations,
CSSResult,
TemplateResult,
property,
} from "lit-element";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
@ -17,19 +17,11 @@ import { HomeAssistant } from "../../../types";
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
class DialogAreaDetail extends LitElement {
public hass!: HomeAssistant;
private _name!: string;
private _error?: string;
private _params?: AreaRegistryDetailDialogParams;
private _submitting?: boolean;
static get properties(): PropertyDeclarations {
return {
_error: {},
_name: {},
_params: {},
};
}
@property() public hass!: HomeAssistant;
@property() private _name!: string;
@property() private _error?: string;
@property() private _params?: AreaRegistryDetailDialogParams;
@property() private _submitting?: boolean;
public async showDialog(
params: AreaRegistryDetailDialogParams

View File

@ -4,8 +4,8 @@ import {
html,
CSSResult,
css,
PropertyDeclarations,
PropertyValues,
property,
} from "lit-element";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
@ -38,26 +38,14 @@ function AutomationEditor(mountEl, props, mergeEl) {
}
export class HaAutomationEditor extends LitElement {
public hass!: HomeAssistant;
public automation!: AutomationEntity;
public isWide?: boolean;
public creatingNew?: boolean;
private _config?: AutomationConfig;
private _dirty?: boolean;
@property() public hass!: HomeAssistant;
@property() public automation!: AutomationEntity;
@property() public isWide?: boolean;
@property() public creatingNew?: boolean;
@property() private _config?: AutomationConfig;
@property() private _dirty?: boolean;
private _rendered?: unknown;
private _errors?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
automation: {},
creatingNew: {},
isWide: {},
_errors: {},
_dirty: {},
_config: {},
};
}
@property() private _errors?: string;
constructor() {
super();

View File

@ -1,10 +1,10 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
CSSResult,
css,
property,
} from "lit-element";
import "@material/mwc-button";
import "../../../../components/buttons/ha-call-api-button";
@ -21,15 +21,8 @@ import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
export class CloudGooglePref extends LitElement {
public hass?: HomeAssistant;
public cloudStatus?: CloudStatusLoggedIn;
static get properties(): PropertyDeclarations {
return {
hass: {},
cloudStatus: {},
};
}
@property() public hass?: HomeAssistant;
@property() public cloudStatus?: CloudStatusLoggedIn;
protected render(): TemplateResult | void {
if (!this.cloudStatus) {

View File

@ -1,11 +1,11 @@
import {
html,
LitElement,
PropertyDeclarations,
TemplateResult,
customElement,
CSSResult,
css,
property,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-item/paper-item-body";
@ -26,15 +26,8 @@ import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dia
@customElement("cloud-remote-pref")
export class CloudRemotePref extends LitElement {
public hass?: HomeAssistant;
public cloudStatus?: CloudStatusLoggedIn;
static get properties(): PropertyDeclarations {
return {
hass: {},
cloudStatus: {},
};
}
@property() public hass?: HomeAssistant;
@property() public cloudStatus?: CloudStatusLoggedIn;
protected render(): TemplateResult | void {
if (!this.cloudStatus) {

View File

@ -1,9 +1,4 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
} from "lit-element";
import { html, LitElement, PropertyValues, property } from "lit-element";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-spinner/paper-spinner";
@ -22,21 +17,11 @@ import {
import { showManageCloudhookDialog } from "../dialog-manage-cloudhook/show-dialog-manage-cloudhook";
export class CloudWebhooks extends LitElement {
public hass?: HomeAssistant;
public cloudStatus?: CloudStatusLoggedIn;
private _cloudHooks?: { [webhookId: string]: CloudWebhook };
private _localHooks?: Webhook[];
private _progress: string[];
static get properties(): PropertyDeclarations {
return {
hass: {},
cloudStatus: {},
_cloudHooks: {},
_localHooks: {},
_progress: {},
};
}
@property() public hass?: HomeAssistant;
@property() public cloudStatus?: CloudStatusLoggedIn;
@property() private _cloudHooks?: { [webhookId: string]: CloudWebhook };
@property() private _localHooks?: Webhook[];
@property() private _progress: string[];
constructor() {
super();

View File

@ -1,10 +1,4 @@
import {
html,
LitElement,
PropertyDeclarations,
css,
CSSResult,
} from "lit-element";
import { html, LitElement, css, CSSResult, property } from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
@ -24,13 +18,7 @@ const inputLabel = "Public URL Click to copy to clipboard";
export class DialogManageCloudhook extends LitElement {
protected hass?: HomeAssistant;
private _params?: WebhookDialogParams;
static get properties(): PropertyDeclarations {
return {
_params: {},
};
}
@property() private _params?: WebhookDialogParams;
public async showDialog(params: WebhookDialogParams) {
this._params = params;

View File

@ -26,9 +26,14 @@ class HaFormCustomize extends PolymerElement {
if="[[computeShowWarning(localConfig, globalConfig)]]"
>
<div class="warning">
It seems that your configuration.yaml doesn't properly include
customize.yaml<br />
Changes made here won't affect your configuration.
It seems that your configuration.yaml doesn't properly
<a
href="https://www.home-assistant.io/docs/configuration/customizing-devices/#customization-using-the-ui"
target="_blank"
>include customize.yaml</a
>.<br />
Changes made here are written in it, but will not be applied after a
configuration reload unless the include is in place.
</div>
</template>
<template is="dom-if" if="[[hasLocalAttributes]]">

View File

@ -1,157 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../../components/ha-card";
import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin";
import { compare } from "../../../../common/string/compare";
import { updateDeviceRegistryEntry } from "../../../../data/device_registry";
import {
loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog,
} from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
/*
* @appliesMixin EventsMixin
*/
class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style>
ha-card {
flex: 1 0 100%;
padding-bottom: 10px;
min-width: 0;
}
.card-header {
display: flex;
justify-content: space-between;
}
.card-header .name {
width: 90%;
}
.device {
width: 30%;
}
.device .name {
font-weight: bold;
}
.device .model,
.device .manuf,
.device .area {
color: var(--secondary-text-color);
}
.area .extra-info .name {
color: var(--primary-text-color);
}
.extra-info {
margin-top: 8px;
}
.manuf,
.entity-id,
.area {
color: var(--secondary-text-color);
}
</style>
<ha-card>
<div class="card-content">
<div class="info">
<div class="model">[[device.model]]</div>
<div class="manuf">
[[localize('ui.panel.config.integrations.config_entry.manuf',
'manufacturer', device.manufacturer)]]
</div>
<template is="dom-if" if="[[device.area_id]]">
<div class="area">
<div class="extra-info">
[[localize('ui.panel.config.integrations.device_registry.area')]]
<span class="name">{{_computeArea(areas, device)}}</span>
</div>
</div>
</template>
</div>
<template is="dom-if" if="[[device.via_device_id]]">
<div class="extra-info">
[[localize('ui.panel.config.integrations.config_entry.via')]]
<span class="hub"
>[[_computeDeviceName(devices, device.via_device_id)]]</span
>
</div>
</template>
<template is="dom-if" if="[[device.sw_version]]">
<div class="extra-info">
[[localize('ui.panel.config.integrations.config_entry.firmware',
'version', device.sw_version)]]
</div>
</template>
</div>
</ha-card>
`;
}
static get properties() {
return {
device: Object,
devices: Array,
areas: Array,
hass: Object,
narrow: {
type: Boolean,
reflectToAttribute: true,
},
_childDevices: {
type: Array,
computed: "_computeChildDevices(device, devices)",
},
};
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
loadDeviceRegistryDetailDialog();
}
_computeArea(areas, device) {
if (!areas || !device || !device.area_id) {
return "No Area";
}
// +1 because of "No Area" entry
return areas.find((area) => area.area_id === device.area_id).name;
}
_computeChildDevices(device, devices) {
return devices
.filter((dev) => dev.via_device_id === device.id)
.sort((dev1, dev2) => compare(dev1.name, dev2.name));
}
_deviceName(device) {
return device.name_by_user || device.name;
}
_computeDeviceName(devices, deviceId) {
const device = devices.find((dev) => dev.id === deviceId);
return device
? this._deviceName(device)
: `(${this.localize(
"ui.panel.config.integrations.config_entry.device_unavailable"
)})`;
}
_gotoSettings() {
const device = this.device;
showDeviceRegistryDetailDialog(this, {
device,
updateEntry: async (updates) => {
await updateDeviceRegistryEntry(this.hass, device.id, updates);
},
});
}
_openMoreInfo(ev) {
this.fire("hass-more-info", { entityId: ev.model.entity.entity_id });
}
}
customElements.define("ha-device-card", HaDeviceCard);

View File

@ -0,0 +1,132 @@
import "../../../../components/ha-card";
import { DeviceRegistryEntry } from "../../../../data/device_registry";
import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
import {
LitElement,
html,
customElement,
property,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import { HomeAssistant } from "../../../../types";
import { AreaRegistryEntry } from "../../../../data/area_registry";
@customElement("ha-device-card")
export class HaDeviceCard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@property() public devices!: DeviceRegistryEntry[];
@property() public areas!: AreaRegistryEntry[];
@property() public narrow!: boolean;
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
<div class="info">
<div class="model">${this.device.model}</div>
<div class="manuf">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.manuf",
"manufacturer",
this.device.manufacturer
)}
</div>
${this.device.area_id
? html`
<div class="area">
<div class="extra-info">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.area",
"area",
this._computeArea(this.areas, this.device)
)}
</div>
</div>
`
: ""}
</div>
${this.device.via_device_id
? html`
<div class="extra-info">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.via"
)}
<span class="hub"
>${this._computeDeviceName(
this.devices,
this.device.via_device_id
)}</span
>
</div>
`
: ""}
${this.device.sw_version
? html`
<div class="extra-info">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.firmware",
"version",
this.device.sw_version
)}
</div>
`
: ""}
</div>
</ha-card>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
loadDeviceRegistryDetailDialog();
}
private _computeArea(areas, device) {
if (!areas || !device || !device.area_id) {
return "No Area";
}
// +1 because of "No Area" entry
return areas.find((area) => area.area_id === device.area_id).name;
}
private _deviceName(device) {
return device.name_by_user || device.name;
}
private _computeDeviceName(devices, deviceId) {
const device = devices.find((dev) => dev.id === deviceId);
return device
? this._deviceName(device)
: `(${this.hass.localize(
"ui.panel.config.integrations.config_entry.device_unavailable"
)})`;
}
static get styles(): CSSResult {
return css`
ha-card {
flex: 1 0 100%;
padding-bottom: 10px;
min-width: 0;
}
.device {
width: 30%;
}
.area {
color: var(--primary-text-color);
}
.extra-info {
margin-top: 8px;
}
.manuf,
.entity-id,
.model {
color: var(--secondary-text-color);
}
`;
}
}

View File

@ -62,6 +62,7 @@ export class HaDeviceEntitiesCard extends LitElement {
${stateObj
? html`
<state-badge
@click=${this._openMoreInfo}
.stateObj=${stateObj}
slot="item-icon"
></state-badge>
@ -72,7 +73,7 @@ export class HaDeviceEntitiesCard extends LitElement {
.icon=${domainIcon(computeDomain(entry.entity_id))}
></ha-icon>
`}
<paper-item-body two-line>
<paper-item-body two-line @click=${this._openMoreInfo}>
<div class="name">${entry.stateName}</div>
<div class="secondary entity-id">${entry.entity_id}</div>
</paper-item-body>
@ -81,7 +82,7 @@ export class HaDeviceEntitiesCard extends LitElement {
? html`
<paper-icon-button
@click=${this._openMoreInfo}
icon="hass:open-in-new"
icon="hass:information-outline"
></paper-icon-button>
`
: ""}
@ -139,6 +140,12 @@ export class HaDeviceEntitiesCard extends LitElement {
.disabled-entry {
color: var(--secondary-text-color);
}
state-badge {
cursor: pointer;
}
paper-icon-item:not(.disabled-entry) paper-item-body {
cursor: pointer;
}
`;
}
}

View File

@ -26,6 +26,7 @@ import {
updateEntityRegistryEntry,
removeEntityRegistryEntry,
} from "../../../data/entity_registry";
import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
class DialogEntityRegistryDetail extends LitElement {
@property() public hass!: HomeAssistant;
@ -139,7 +140,7 @@ class DialogEntityRegistryDetail extends LitElement {
<div class="paper-dialog-buttons">
<mwc-button
class="warning"
@click="${this._deleteEntry}"
@click="${this._confirmDeleteEntry}"
.disabled=${this._submitting}
>
${this.hass.localize(
@ -186,22 +187,6 @@ class DialogEntityRegistryDetail extends LitElement {
}
private async _deleteEntry(): Promise<void> {
if (
!confirm(
`${this.hass.localize(
"ui.panel.config.entity_registry.editor.confirm_delete"
)}
${this.hass.localize(
"ui.panel.config.entity_registry.editor.confirm_delete2",
"platform",
this._platform
)}`
)
) {
return;
}
this._submitting = true;
try {
@ -212,6 +197,20 @@ ${this.hass.localize(
}
}
private _confirmDeleteEntry(): void {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.entity_registry.editor.confirm_delete"
),
text: this.hass.localize(
"ui.panel.config.entity_registry.editor.confirm_delete2",
"platform",
this._platform
),
confirm: () => this._deleteEntry(),
});
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;

View File

@ -17,6 +17,7 @@ import { DeviceRegistryEntry } from "../../../../data/device_registry";
import { AreaRegistryEntry } from "../../../../data/area_registry";
import { fireEvent } from "../../../../common/dom/fire_event";
import { showConfigEntrySystemOptionsDialog } from "../../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
import { showConfirmationDialog } from "../../../../dialogs/confirmation/show-dialog-confirmation";
class HaConfigEntryPage extends LitElement {
@property() public hass!: HomeAssistant;
@ -84,18 +85,33 @@ class HaConfigEntryPage extends LitElement {
slot="toolbar-icon"
icon="hass:settings"
@click=${this._showSettings}
title=${this.hass.localize(
"ui.panel.config.integrations.config_entry.settings_button",
"integration",
configEntry.title
)}
></paper-icon-button>
`
: ""}
<paper-icon-button
slot="toolbar-icon"
icon="hass:message-settings-variant"
title=${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options_button",
"integration",
configEntry.title
)}
@click=${this._showSystemOptions}
></paper-icon-button>
<paper-icon-button
slot="toolbar-icon"
icon="hass:delete"
@click=${this._removeEntry}
title=${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_button",
"integration",
configEntry.title
)}
@click=${this._confirmRemoveEntry}
></paper-icon-button>
<div class="content">
@ -144,17 +160,16 @@ class HaConfigEntryPage extends LitElement {
});
}
private _removeEntry() {
if (
!confirm(
this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_confirm"
)
)
) {
return;
}
private _confirmRemoveEntry() {
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_confirm"
),
confirm: () => this._removeEntry(),
});
}
private _removeEntry() {
deleteConfigEntry(this.hass, this.configEntryId).then((result) => {
fireEvent(this, "hass-reload-entries");
if (result.require_restart) {

View File

@ -42,6 +42,7 @@ import { DataEntryFlowProgress } from "../../../data/data_entry_flow";
@customElement("ha-config-entries-dashboard")
export class HaConfigManagerDashboard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public showAdvanced!: boolean;
@property() private configEntries!: ConfigEntry[];
@ -164,6 +165,7 @@ export class HaConfigManagerDashboard extends LitElement {
private _createFlow() {
showConfigFlowDialog(this, {
dialogClosedCallback: () => fireEvent(this, "hass-reload-entries"),
showAdvanced: this.showAdvanced,
});
}

View File

@ -39,6 +39,7 @@ declare global {
class HaConfigIntegrations extends HassRouterPage {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public showAdvanced!: boolean;
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
@ -99,6 +100,8 @@ class HaConfigIntegrations extends HassRouterPage {
pageEl.entityRegistryEntries = this._entityRegistryEntries;
pageEl.configEntries = this._configEntries;
pageEl.narrow = this.narrow;
pageEl.showAdvanced = this.showAdvanced;
if (this._currentPage === "dashboard") {
pageEl.configEntriesInProgress = this._configEntriesInProgress;
@ -108,7 +111,6 @@ class HaConfigIntegrations extends HassRouterPage {
pageEl.configEntryId = this.routeTail.path.substr(1);
pageEl.deviceRegistryEntries = this._deviceRegistryEntries;
pageEl.areas = this._areas;
pageEl.narrow = this.narrow;
}
private _loadData() {

View File

@ -2,7 +2,7 @@ import { h, Component } from "preact";
import "../../../../components/device/ha-device-picker";
import "../../../../components/device/ha-device-condition-picker";
import "../../../../components/ha-form";
import "../../../../components/ha-form/ha-form";
import {
fetchDeviceConditionCapabilities,
@ -64,9 +64,9 @@ export default class DeviceCondition extends Component<any, any> {
{extraFieldsData && (
<ha-form
data={Object.assign({}, ...extraFieldsData)}
onData-changed={this._extraFieldsChanged}
schema={this.state.capabilities.extra_fields}
computeLabel={this._extraFieldsComputeLabelCallback(hass.localize)}
onvalue-changed={this._extraFieldsChanged}
/>
)}
</div>
@ -83,7 +83,7 @@ export default class DeviceCondition extends Component<any, any> {
}
public componentDidUpdate(prevProps) {
if (prevProps.condition !== this.props.condition) {
if (!deviceAutomationsEqual(prevProps.condition, this.props.condition)) {
this._getCapabilities();
}
}
@ -98,15 +98,9 @@ export default class DeviceCondition extends Component<any, any> {
}
private _extraFieldsChanged(ev) {
if (!ev.detail.path) {
return;
}
const item = ev.detail.path.replace("data.", "");
const value = ev.detail.value || undefined;
this.props.onChange(this.props.index, {
...this.props.condition,
[item]: value,
...ev.detail.value,
});
}

View File

@ -2,8 +2,14 @@ import { h, Component } from "preact";
import "../../../../components/device/ha-device-picker";
import "../../../../components/device/ha-device-action-picker";
import { HomeAssistant } from "../../../../types";
import "../../../../components/ha-form/ha-form";
import {
fetchDeviceActionCapabilities,
deviceAutomationsEqual,
} from "../../../../data/device_automation";
import { DeviceAction } from "../../../../data/script";
import { HomeAssistant } from "../../../../types";
export default class DeviceActionEditor extends Component<
{
@ -14,6 +20,7 @@ export default class DeviceActionEditor extends Component<
},
{
device_id: string | undefined;
capabilities: any | undefined;
}
> {
public static defaultConfig: DeviceAction = {
@ -22,16 +29,26 @@ export default class DeviceActionEditor extends Component<
entity_id: "",
};
private _origAction;
constructor() {
super();
this.devicePicked = this.devicePicked.bind(this);
this.deviceActionPicked = this.deviceActionPicked.bind(this);
this.state = { device_id: undefined };
this._extraFieldsChanged = this._extraFieldsChanged.bind(this);
this.state = { device_id: undefined, capabilities: undefined };
}
public render() {
const { action, hass } = this.props;
const deviceId = this.state.device_id || action.device_id;
const capabilities = this.state.capabilities;
const extraFieldsData =
capabilities && capabilities.extra_fields
? capabilities.extra_fields.map((item) => {
return { [item.name]: this.props.action[item.name] };
})
: undefined;
return (
<div>
@ -48,16 +65,71 @@ export default class DeviceActionEditor extends Component<
hass={hass}
label="Action"
/>
{extraFieldsData && (
<ha-form
data={Object.assign({}, ...extraFieldsData)}
onData-changed={this._extraFieldsChanged}
schema={this.state.capabilities.extra_fields}
computeLabel={this._extraFieldsComputeLabelCallback(hass.localize)}
/>
)}
</div>
);
}
public componentDidMount() {
if (!this.state.capabilities) {
this._getCapabilities();
}
if (this.props.action) {
this._origAction = this.props.action;
}
}
public componentDidUpdate(prevProps) {
if (!deviceAutomationsEqual(prevProps.action, this.props.action)) {
this._getCapabilities();
}
}
private devicePicked(ev) {
this.setState({ device_id: ev.target.value });
this.setState({ ...this.state, device_id: ev.target.value });
}
private deviceActionPicked(ev) {
const deviceAction = { ...ev.target.value };
let deviceAction = ev.target.value;
if (
this._origAction &&
deviceAutomationsEqual(this._origAction, deviceAction)
) {
deviceAction = this._origAction;
}
this.props.onChange(this.props.index, deviceAction);
}
private async _getCapabilities() {
const action = this.props.action;
const capabilities = action.domain
? await fetchDeviceActionCapabilities(this.props.hass, action)
: null;
this.setState({ ...this.state, capabilities });
}
private _extraFieldsChanged(ev) {
this.props.onChange(this.props.index, {
...this.props.action,
...ev.detail.value,
});
}
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.actions.type.device_id.extra_fields.${
schema.name
}`
) || schema.name;
}
}

View File

@ -2,7 +2,7 @@ import { h, Component } from "preact";
import "../../../../components/device/ha-device-picker";
import "../../../../components/device/ha-device-trigger-picker";
import "../../../../components/ha-form";
import "../../../../components/ha-form/ha-form";
import {
fetchDeviceTriggerCapabilities,
@ -65,9 +65,9 @@ export default class DeviceTrigger extends Component<any, any> {
{extraFieldsData && (
<ha-form
data={Object.assign({}, ...extraFieldsData)}
onData-changed={this._extraFieldsChanged}
schema={this.state.capabilities.extra_fields}
computeLabel={this._extraFieldsComputeLabelCallback(hass.localize)}
onvalue-changed={this._extraFieldsChanged}
/>
)}
</div>
@ -84,7 +84,7 @@ export default class DeviceTrigger extends Component<any, any> {
}
public componentDidUpdate(prevProps) {
if (prevProps.trigger !== this.props.trigger) {
if (!deviceAutomationsEqual(prevProps.trigger, this.props.trigger)) {
this._getCapabilities();
}
}
@ -99,15 +99,9 @@ export default class DeviceTrigger extends Component<any, any> {
}
private _extraFieldsChanged(ev) {
if (!ev.detail.path) {
return;
}
const item = ev.detail.path.replace("data.", "");
const value = ev.detail.value || undefined;
this.props.onChange(this.props.index, {
...this.props.trigger,
[item]: value,
...ev.detail.value,
});
}

View File

@ -1,5 +1,5 @@
import { h, Component } from "preact";
import yaml from "js-yaml";
import { safeDump, safeLoad } from "js-yaml";
import "../../../components/ha-code-editor";
const isEmpty = (obj: object) => {
@ -19,7 +19,7 @@ export default class YAMLTextArea extends Component<any, any> {
try {
value =
props.value && !isEmpty(props.value)
? yaml.safeDump(props.value)
? safeDump(props.value)
: undefined;
} catch (err) {
alert(`There was an error converting to YAML: ${err}`);
@ -40,7 +40,7 @@ export default class YAMLTextArea extends Component<any, any> {
if (value) {
try {
parsed = yaml.safeLoad(value);
parsed = safeLoad(value);
isValid = true;
} catch (err) {
// Invalid YAML

View File

@ -4,7 +4,7 @@ import {
html,
css,
CSSResult,
PropertyDeclarations,
property,
} from "lit-element";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
@ -30,21 +30,12 @@ import {
import { User, fetchUsers } from "../../../data/user";
class HaConfigPerson extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _storageItems?: Person[];
private _configItems?: Person[];
@property() public hass?: HomeAssistant;
@property() public isWide?: boolean;
@property() private _storageItems?: Person[];
@property() private _configItems?: Person[];
private _usersLoad?: Promise<User[]>;
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_storageItems: {},
_configItems: {},
};
}
protected render(): TemplateResult | void {
if (
!this.hass ||

View File

@ -14,9 +14,9 @@ import {
CSSResult,
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
property,
} from "lit-element";
import {
@ -37,39 +37,16 @@ import {
} from "./types";
export class ZHAClusterAttributes extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
public showHelp: boolean;
public selectedNode?: ZHADevice;
public selectedCluster?: Cluster;
private _attributes: Attribute[];
private _selectedAttributeIndex: number;
private _attributeValue?: any;
private _manufacturerCodeOverride?: string | number;
private _setAttributeServiceData?: SetAttributeServiceData;
constructor() {
super();
this.showHelp = false;
this._selectedAttributeIndex = -1;
this._attributes = [];
this._attributeValue = "";
}
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
showHelp: {},
selectedNode: {},
selectedCluster: {},
_attributes: {},
_selectedAttributeIndex: {},
_attributeValue: {},
_manufacturerCodeOverride: {},
_setAttributeServiceData: {},
};
}
@property() public hass?: HomeAssistant;
@property() public isWide?: boolean;
@property() public showHelp = false;
@property() public selectedNode?: ZHADevice;
@property() public selectedCluster?: Cluster;
@property() private _attributes: Attribute[] = [];
@property() private _selectedAttributeIndex = -1;
@property() private _attributeValue?: any = "";
@property() private _manufacturerCodeOverride?: string | number;
@property() private _setAttributeServiceData?: SetAttributeServiceData;
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedCluster")) {

View File

@ -13,9 +13,9 @@ import {
CSSResult,
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
property,
} from "lit-element";
import {
@ -34,36 +34,15 @@ import {
} from "./types";
export class ZHAClusterCommands extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
public selectedNode?: ZHADevice;
public selectedCluster?: Cluster;
private _showHelp: boolean;
private _commands: Command[];
private _selectedCommandIndex: number;
private _manufacturerCodeOverride?: number;
private _issueClusterCommandServiceData?: IssueCommandServiceData;
constructor() {
super();
this._showHelp = false;
this._selectedCommandIndex = -1;
this._commands = [];
}
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
selectedNode: {},
selectedCluster: {},
_showHelp: {},
_commands: {},
_selectedCommandIndex: {},
_manufacturerCodeOverride: {},
_issueClusterCommandServiceData: {},
};
}
@property() public hass?: HomeAssistant;
@property() public isWide?: boolean;
@property() public selectedNode?: ZHADevice;
@property() public selectedCluster?: Cluster;
@property() private _showHelp = false;
@property() private _commands: Command[] = [];
@property() private _selectedCommandIndex = -1;
@property() private _manufacturerCodeOverride?: number;
@property() private _issueClusterCommandServiceData?: IssueCommandServiceData;
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedCluster")) {

View File

@ -11,9 +11,9 @@ import {
CSSResult,
html,
LitElement,
PropertyDeclarations,
PropertyValues,
TemplateResult,
property,
} from "lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
@ -39,30 +39,12 @@ const computeClusterKey = (cluster: Cluster): string => {
};
export class ZHAClusters extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
public showHelp: boolean;
public selectedDevice?: ZHADevice;
private _selectedClusterIndex: number;
private _clusters: Cluster[];
constructor() {
super();
this.showHelp = false;
this._selectedClusterIndex = -1;
this._clusters = [];
}
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
showHelp: {},
selectedDevice: {},
_selectedClusterIndex: {},
_clusters: {},
};
}
@property() public hass?: HomeAssistant;
@property() public isWide?: boolean;
@property() public showHelp = false;
@property() public selectedDevice?: ZHADevice;
@property() private _selectedClusterIndex = -1;
@property() private _clusters: Cluster[] = [];
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedDevice")) {

View File

@ -10,8 +10,8 @@ import {
CSSResult,
html,
LitElement,
PropertyDeclarations,
TemplateResult,
property,
} from "lit-element";
import { navigate } from "../../../common/navigate";
@ -19,23 +19,9 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
export class ZHANetwork extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _showHelp: boolean;
constructor() {
super();
this._showHelp = false;
}
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_showHelp: {},
_joinParams: {},
};
}
@property() public hass?: HomeAssistant;
@property() public isWide?: boolean;
@property() private _showHelp = false;
protected render(): TemplateResult | void {
return html`

View File

@ -4,19 +4,21 @@ import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import yaml from "js-yaml";
import { safeLoad } from "js-yaml";
import "../../../components/ha-code-editor";
import "../../../resources/ha-style";
import "./events-list";
import "./event-subscribe-card";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
const ERROR_SENTINEL = {};
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class HaPanelDevEvent extends EventsMixin(PolymerElement) {
class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style include="ha-style iron-flex iron-positioning"></style>
@ -54,21 +56,26 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
<div class$="[[computeFormClasses(narrow)]]">
<div class="flex">
<p>
Fire an event on the event bus.
[[localize( 'ui.panel.developer-tools.tabs.events.description' )]]
<a
href="https://www.home-assistant.io/docs/configuration/events/"
target="_blank"
>Events Documentation.</a
>[[localize( 'ui.panel.developer-tools.tabs.events.documentation'
)]]</a
>
</p>
<div class="ha-form">
<paper-input
label="Event Type"
label="[[localize(
'ui.panel.developer-tools.tabs.events.type'
)]]"
autofocus
required
value="{{eventType}}"
></paper-input>
<p>Event Data (YAML, optional)</p>
<p>
[[localize( 'ui.panel.developer-tools.tabs.events.data' )]]
</p>
<ha-code-editor
mode="yaml"
value="[[eventData]]"
@ -76,13 +83,17 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
on-value-changed="_yamlChanged"
></ha-code-editor>
<mwc-button on-click="fireEvent" raised disabled="[[!validJSON]]"
>Fire Event</mwc-button
>[[localize( 'ui.panel.developer-tools.tabs.events.fire_event'
)]]</mwc-button
>
</div>
</div>
<div>
<div class="header">Available Events</div>
<div class="header">
[[localize( 'ui.panel.developer-tools.tabs.events.available_events'
)]]
</div>
<events-list
on-event-selected="eventSelected"
hass="[[hass]]"
@ -127,7 +138,7 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
_computeParsedEventData(eventData) {
try {
return eventData.trim() ? yaml.safeLoad(eventData) : {};
return eventData.trim() ? safeLoad(eventData) : {};
} catch (err) {
return ERROR_SENTINEL;
}
@ -143,13 +154,21 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
fireEvent() {
if (!this.eventType) {
alert("Event type is a mandatory field");
alert(
this.hass.localize(
"ui.panel.developer-tools.tabs.events.alert_event_type"
)
);
return;
}
this.hass.callApi("POST", "events/" + this.eventType, this.parsedJSON).then(
function() {
this.fire("hass-notification", {
message: "Event " + this.eventType + " successful fired!",
message: this.hass.localize(
"ui.panel.developer-tools.tabs.events.notification_event_fired",
"type",
this.eventType
),
});
}.bind(this)
);

View File

@ -37,12 +37,20 @@ class EventSubscribeCard extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Listen to events">
<ha-card
header=${this.hass!.localize(
"ui.panel.developer-tools.tabs.events.listen_to_events"
)}
>
<form>
<paper-input
.label=${this._subscribed
? "Listening to"
: "Event to subscribe to"}
? this.hass!.localize(
"ui.panel.developer-tools.tabs.events.listening_to"
)
: this.hass!.localize(
"ui.panel.developer-tools.tabs.events.subscribe_to"
)}
.disabled=${this._subscribed !== undefined}
.value=${this._eventType}
@value-changed=${this._valueChanged}
@ -52,14 +60,24 @@ class EventSubscribeCard extends LitElement {
@click=${this._handleSubmit}
type="submit"
>
${this._subscribed ? "Stop listening" : "Start listening"}
${this._subscribed
? this.hass!.localize(
"ui.panel.developer-tools.tabs.events.stop_listening"
)
: this.hass!.localize(
"ui.panel.developer-tools.tabs.events.start_listening"
)}
</mwc-button>
</form>
<div class="events">
${this._events.map(
(ev) => html`
<div class="event">
Event ${ev.id} fired
${this.hass!.localize(
"ui.panel.developer-tools.tabs.events.event_fired",
"name",
ev.id
)}
${format_time(
new Date(ev.event.time_fired),
this.hass!.language

View File

@ -2,11 +2,13 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class EventsList extends EventsMixin(PolymerElement) {
class EventsList extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style>
@ -29,8 +31,11 @@ class EventsList extends EventsMixin(PolymerElement) {
<template is="dom-repeat" items="[[events]]" as="event">
<li>
<a href="#" on-click="eventSelected">{{event.event}}</a>
<span> (</span><span>{{event.listener_count}}</span
><span> listeners)</span>
<span>
[[localize(
"ui.panel.developer-tools.tabs.events.count_listeners", "count",
event.listener_count )]]</span
>
</li>
</template>
</ul>

View File

@ -31,12 +31,18 @@ class HaPanelDevInfo extends LitElement {
const nonDefaultLinkText =
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
? "Go to the Lovelace UI"
: "Go to the states UI";
? this.hass.localize("ui.panel.developer-tools.tabs.info.lovelace_ui")
: this.hass.localize("ui.panel.developer-tools.tabs.info.states_ui");
const defaultPageText = `${
localStorage.defaultPage === OPT_IN_PANEL ? "Remove" : "Set"
} ${OPT_IN_PANEL} as default page on this device`;
const defaultPageText = `${this.hass.localize(
"ui.panel.developer-tools.tabs.info.default_ui",
"action",
localStorage.defaultPage === OPT_IN_PANEL
? this.hass.localize("ui.panel.developer-tools.tabs.info.remove")
: this.hass.localize("ui.panel.developer-tools.tabs.info.set"),
"name",
OPT_IN_PANEL
)}`;
return html`
<div class="about">
@ -45,43 +51,58 @@ class HaPanelDevInfo extends LitElement {
><img
src="/static/icons/favicon-192x192.png"
height="192"
alt="Home Assistant logo"
alt="${this.hass.localize(
"ui.panel.developer-tools.tabs.info.home_assistant_logo"
)}"
/></a>
<br />
Home Assistant<br />
${hass.config.version}
<h2>Home Assistant ${hass.config.version}</h2>
</p>
<p>
Path to configuration.yaml: ${hass.config.config_dir}
${this.hass.localize(
"ui.panel.developer-tools.tabs.info.path_configuration",
"path",
hass.config.config_dir
)}
</p>
<p class="develop">
<a
href="https://www.home-assistant.io/developers/credits/"
target="_blank"
>
Developed by a bunch of awesome people.
${this.hass.localize(
"ui.panel.developer-tools.tabs.info.developed_by"
)}
</a>
</p>
<p>
Published under the Apache 2.0 license<br />
Source:
${this.hass.localize(
"ui.panel.developer-tools.tabs.info.license"
)}<br />
${this.hass.localize("ui.panel.developer-tools.tabs.info.source")}
<a
href="https://github.com/home-assistant/home-assistant"
target="_blank"
>server</a
>${this.hass.localize(
"ui.panel.developer-tools.tabs.info.server"
)}</a
>
&mdash;
<a
href="https://github.com/home-assistant/home-assistant-polymer"
target="_blank"
>frontend-ui</a
>${this.hass.localize(
"ui.panel.developer-tools.tabs.info.frontend"
)}</a
>
</p>
<p>
Built using
${this.hass.localize(
"ui.panel.developer-tools.tabs.info.built_using"
)}
<a href="https://www.python.org">Python 3</a>,
<a href="https://www.polymer-project.org" target="_blank">Polymer</a>,
Icons by
${this.hass.localize("ui.panel.developer-tools.tabs.info.icons_by")}
<a href="https://www.google.com/design/icons/" target="_blank"
>Google</a
>
@ -91,22 +112,32 @@ class HaPanelDevInfo extends LitElement {
>.
</p>
<p>
Frontend version: ${JS_VERSION} - ${JS_TYPE}
${customUiList.length > 0
? html`
<div>
Custom UIs:
${customUiList.map(
(item) => html`
<div>
<a href="${item.url}" target="_blank"> ${item.name}</a>:
${item.version}
</div>
`
)}
</div>
`
: ""}
${this.hass.localize(
"ui.panel.developer-tools.tabs.info.frontend_version",
"version",
JS_VERSION,
"type",
JS_TYPE
)}
${
customUiList.length > 0
? html`
<div>
${this.hass.localize(
"ui.panel.developer-tools.tabs.info.custom_uis"
)}
${customUiList.map(
(item) => html`
<div>
<a href="${item.url}" target="_blank"> ${item.name}</a
>: ${item.version}
</div>
`
)}
</div>
`
: ""
}
</p>
<p>
<a href="${nonDefaultLink}">${nonDefaultLinkText}</a><br />

View File

@ -3,8 +3,8 @@ import {
html,
CSSResult,
css,
PropertyDeclarations,
TemplateResult,
property,
} from "lit-element";
import "@polymer/paper-spinner/paper-spinner";
import "../../../components/ha-card";
@ -32,15 +32,8 @@ const sortKeys = (a: string, b: string) => {
};
class SystemHealthCard extends LitElement {
public hass?: HomeAssistant;
private _info?: SystemHealthInfo;
static get properties(): PropertyDeclarations {
return {
hass: {},
_info: {},
};
}
@property() public hass!: HomeAssistant;
@property() private _info?: SystemHealthInfo;
protected render(): TemplateResult | void {
if (!this.hass) {
@ -85,7 +78,7 @@ class SystemHealthCard extends LitElement {
}
return html`
<ha-card header="System Health">
<ha-card header="${this.hass.localize("domain.system_health")}">
<div class="card-content">${sections}</div>
</ha-card>
`;
@ -105,8 +98,9 @@ class SystemHealthCard extends LitElement {
} catch (err) {
this._info = {
system_health: {
error:
"System Health component is not loaded. Add 'system_health:' to configuration.yaml",
error: this.hass.localize(
"ui.panel.developer-tools.tabs.info.system_health_error"
),
},
};
}

View File

@ -2,9 +2,9 @@ import {
LitElement,
html,
css,
PropertyDeclarations,
CSSResult,
TemplateResult,
property,
} from "lit-element";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
@ -13,15 +13,11 @@ import "../../../components/dialog/ha-paper-dialog";
import { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
class DialogSystemLogDetail extends LitElement {
private _params?: SystemLogDetailDialogParams;
static get properties(): PropertyDeclarations {
return {
_params: {},
};
}
@property() public hass!: HomeAssistant;
@property() private _params?: SystemLogDetailDialogParams;
public async showDialog(params: SystemLogDetailDialogParams): Promise<void> {
this._params = params;
@ -40,7 +36,13 @@ class DialogSystemLogDetail extends LitElement {
opened
@opened-changed="${this._openedChanged}"
>
<h2>Log Details (${item.level})</h2>
<h2>
${this.hass.localize(
"ui.panel.developer-tools.tabs.logs.details",
"level",
item.level
)}
</h2>
<paper-dialog-scrollable>
<p>${new Date(item.timestamp * 1000)}</p>
${item.message

View File

@ -3,8 +3,8 @@ import {
html,
CSSResult,
css,
PropertyDeclarations,
TemplateResult,
property,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import "@material/mwc-button";
@ -13,15 +13,8 @@ import { HomeAssistant } from "../../../types";
import { fetchErrorLog } from "../../../data/error_log";
class ErrorLogCard extends LitElement {
public hass?: HomeAssistant;
private _errorLog?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
_errorLog: {},
};
}
@property() public hass!: HomeAssistant;
@property() private _errorLog?: string;
protected render(): TemplateResult | void {
return html`
@ -35,7 +28,9 @@ class ErrorLogCard extends LitElement {
`
: html`
<mwc-button raised @click=${this._refreshErrorLog}>
Load Full Home Assistant Log
${this.hass.localize(
"ui.panel.developer-tools.tabs.logs.load_full_log"
)}
</mwc-button>
`}
</p>
@ -64,9 +59,12 @@ class ErrorLogCard extends LitElement {
}
private async _refreshErrorLog(): Promise<void> {
this._errorLog = "Loading error log…";
this._errorLog = this.hass.localize(
"ui.panel.developer-tools.tabs.logs.loading_log"
);
const log = await fetchErrorLog(this.hass!);
this._errorLog = log || "No errors have been reported.";
this._errorLog =
log || this.hass.localize("ui.panel.developer-tools.tabs.logs.no_errors");
}
}

View File

@ -3,9 +3,9 @@ import {
html,
CSSResult,
css,
PropertyDeclarations,
TemplateResult,
customElement,
property,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item-body";
@ -32,16 +32,9 @@ const formatLogTime = (date, language: string) => {
@customElement("system-log-card")
export class SystemLogCard extends LitElement {
public hass?: HomeAssistant;
@property() public hass!: HomeAssistant;
public loaded = false;
private _items?: LoggedError[];
static get properties(): PropertyDeclarations {
return {
hass: {},
_items: {},
};
}
@property() private _items?: LoggedError[];
public async fetchData(): Promise<void> {
this._items = undefined;
@ -61,7 +54,11 @@ export class SystemLogCard extends LitElement {
: html`
${this._items.length === 0
? html`
<div class="card-content">There are no new issues!</div>
<div class="card-content">
${this.hass.localize(
"ui.panel.developer-tools.tabs.logs.no_issues"
)}
</div>
`
: this._items.map(
(item) => html`
@ -78,12 +75,17 @@ export class SystemLogCard extends LitElement {
${item.source} (${item.level})
${item.count > 1
? html`
- message first occured at
${formatLogTime(
item.first_occured,
this.hass!.language
-
${this.hass.localize(
"ui.panel.developer-tools.tabs.logs.multiple_messages",
"time",
formatLogTime(
item.first_occured,
this.hass!.language
),
"counter",
item.count
)}
and shows up ${item.count} times
`
: html``}
</div>
@ -97,10 +99,14 @@ export class SystemLogCard extends LitElement {
.hass=${this.hass}
domain="system_log"
service="clear"
>Clear</ha-call-service-button
>${this.hass.localize(
"ui.panel.developer-tools.tabs.logs.clear"
)}</ha-call-service-button
>
<ha-progress-button @click=${this.fetchData}
>Refresh</ha-progress-button
>${this.hass.localize(
"ui.panel.developer-tools.tabs.logs.refresh"
)}</ha-progress-button
>
</div>
`}

View File

@ -19,7 +19,7 @@ import "./mqtt-subscribe-card";
@customElement("developer-tools-mqtt")
class HaPanelDevMqtt extends LitElement {
@property() public hass?: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() private topic = "";
@ -40,15 +40,25 @@ class HaPanelDevMqtt extends LitElement {
protected render(): TemplateResult {
return html`
<div class="content">
<ha-card header="Publish a packet">
<ha-card
header="${this.hass.localize(
"ui.panel.developer-tools.tabs.mqtt.description_publish"
)}"
>
<div class="card-content">
<paper-input
label="topic"
label="${this.hass.localize(
"ui.panel.developer-tools.tabs.mqtt.topic"
)}"
.value=${this.topic}
@value-changed=${this._handleTopic}
></paper-input>
<p>Payload (template allowed)</p>
<p>
${this.hass.localize(
"ui.panel.developer-tools.tabs.mqtt.payload"
)}
</p>
<ha-code-editor
mode="jinja2"
.value="${this.payload}"
@ -56,7 +66,11 @@ class HaPanelDevMqtt extends LitElement {
></ha-code-editor>
</div>
<div class="card-actions">
<mwc-button @click=${this._publish}>Publish</mwc-button>
<mwc-button @click=${this._publish}
>${this.hass.localize(
"ui.panel.developer-tools.tabs.mqtt.publish"
)}</mwc-button
>
</div>
</ha-card>

View File

@ -17,7 +17,7 @@ import { subscribeMQTTTopic, MQTTMessage } from "../../../data/mqtt";
@customElement("mqtt-subscribe-card")
class MqttSubscribeCard extends LitElement {
@property() public hass?: HomeAssistant;
@property() public hass!: HomeAssistant;
@property() private _topic = "";
@ -42,12 +42,20 @@ class MqttSubscribeCard extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Listen to a topic">
<ha-card
header="${this.hass.localize(
"ui.panel.developer-tools.tabs.mqtt.description_listen"
)}"
>
<form>
<paper-input
.label=${this._subscribed
? "Listening to"
: "Topic to subscribe to"}
? this.hass.localize(
"ui.panel.developer-tools.tabs.mqtt.listening_to"
)
: this.hass.localize(
"ui.panel.developer-tools.tabs.mqtt.subscribe_to"
)}
.disabled=${this._subscribed !== undefined}
.value=${this._topic}
@value-changed=${this._valueChanged}
@ -57,15 +65,28 @@ class MqttSubscribeCard extends LitElement {
@click=${this._handleSubmit}
type="submit"
>
${this._subscribed ? "Stop listening" : "Start listening"}
${this._subscribed
? this.hass.localize(
"ui.panel.developer-tools.tabs.mqtt.stop_listening"
)
: this.hass.localize(
"ui.panel.developer-tools.tabs.mqtt.start_listening"
)}
</mwc-button>
</form>
<div class="events">
${this._messages.map(
(msg) => html`
<div class="event">
Message ${msg.id} received on <b>${msg.message.topic}</b> at
${format_time(msg.time, this.hass!.language)}:
${this.hass.localize(
"ui.panel.developer-tools.tabs.mqtt.message_received",
"id",
msg.id,
"topic",
msg.message.topic,
"time",
format_time(msg.time, this.hass!.language)
)}
<pre>${msg.payload}</pre>
<div class="bottom">
QoS: ${msg.message.qos} - Retain:

View File

@ -2,7 +2,7 @@ import "@material/mwc-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import yaml from "js-yaml";
import { safeDump, safeLoad } from "js-yaml";
import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity";
import "../../../components/entity/ha-entity-picker";
@ -10,9 +10,13 @@ import "../../../components/ha-code-editor";
import "../../../components/ha-service-picker";
import "../../../resources/ha-style";
import "../../../util/app-localstorage-document";
import LocalizeMixin from "../../../mixins/localize-mixin";
const ERROR_SENTINEL = {};
class HaPanelDevService extends PolymerElement {
/*
* @appliesMixin LocalizeMixin
*/
class HaPanelDevService extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
@ -94,8 +98,7 @@ class HaPanelDevService extends PolymerElement {
<div class="content">
<p>
The service dev tool allows you to call any available service in Home
Assistant.
[[localize('ui.panel.developer-tools.tabs.services.description')]]
</p>
<div class="ha-form">
@ -113,7 +116,7 @@ class HaPanelDevService extends PolymerElement {
allow-custom-entity
></ha-entity-picker>
</template>
<p>Service Data (YAML, optional)</p>
<p>[[localize('ui.panel.developer-tools.tabs.services.data')]]</p>
<ha-code-editor
mode="yaml"
value="[[serviceData]]"
@ -121,30 +124,42 @@ class HaPanelDevService extends PolymerElement {
on-value-changed="_yamlChanged"
></ha-code-editor>
<mwc-button on-click="_callService" raised disabled="[[!validJSON]]">
Call Service
[[localize('ui.panel.developer-tools.tabs.services.call_service')]]
</mwc-button>
</div>
<template is="dom-if" if="[[!domainService]]">
<h1>Select a service to see the description</h1>
<h1>
[[localize('ui.panel.developer-tools.tabs.services.select_service')]]
</h1>
</template>
<template is="dom-if" if="[[domainService]]">
<template is="dom-if" if="[[!_description]]">
<h1>No description is available</h1>
<h1>
[[localize('ui.panel.developer-tools.tabs.services.no_description')]]
</h1>
</template>
<template is="dom-if" if="[[_description]]">
<h3>[[_description]]</h3>
<table class="attributes">
<tr>
<th>Parameter</th>
<th>Description</th>
<th>Example</th>
<th>
[[localize('ui.panel.developer-tools.tabs.services.column_parameter')]]
</th>
<th>
[[localize('ui.panel.developer-tools.tabs.services.column_description')]]
</th>
<th>
[[localize('ui.panel.developer-tools.tabs.services.column_example')]]
</th>
</tr>
<template is="dom-if" if="[[!_attributes.length]]">
<tr>
<td colspan="3">This service takes no parameters.</td>
<td colspan="3">
[[localize('ui.panel.developer-tools.tabs.services.no_parameters')]]
</td>
</tr>
</template>
<template is="dom-repeat" items="[[_attributes]]" as="attribute">
@ -158,7 +173,7 @@ class HaPanelDevService extends PolymerElement {
<template is="dom-if" if="[[_attributes.length]]">
<mwc-button on-click="_fillExampleData">
Fill Example Data
[[localize('ui.panel.developer-tools.tabs.services.fill_example_data')]]
</mwc-button>
</template>
</template>
@ -251,7 +266,7 @@ class HaPanelDevService extends PolymerElement {
_computeParsedServiceData(serviceData) {
try {
return serviceData.trim() ? yaml.safeLoad(serviceData) : {};
return serviceData.trim() ? safeLoad(serviceData) : {};
} catch (err) {
return ERROR_SENTINEL;
}
@ -276,7 +291,13 @@ class HaPanelDevService extends PolymerElement {
_callService() {
if (this.parsedJSON === ERROR_SENTINEL) {
// eslint-disable-next-line
alert(`Error parsing YAML: ${this.serviceData}`);
alert(
this.hass.localize(
"ui.panel.developer-tools.tabs.services.alert_parsing_yaml",
"data",
this.serviceData
)
);
return;
}
@ -289,18 +310,18 @@ class HaPanelDevService extends PolymerElement {
if (attribute.example) {
let value = "";
try {
value = yaml.safeLoad(attribute.example);
value = safeLoad(attribute.example);
} catch (err) {
value = attribute.example;
}
example[attribute.key] = value;
}
});
this.serviceData = yaml.safeDump(example);
this.serviceData = safeDump(example);
}
_entityPicked(ev) {
this.serviceData = yaml.safeDump({
this.serviceData = safeDump({
...this.parsedJSON,
entity_id: ev.target.value,
});

View File

@ -4,18 +4,20 @@ import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import yaml from "js-yaml";
import { safeDump, safeLoad } from "js-yaml";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-code-editor";
import "../../../resources/ha-style";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
const ERROR_SENTINEL = {};
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class HaPanelDevState extends EventsMixin(PolymerElement) {
class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style include="ha-style">
@ -70,8 +72,8 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
<div class="inputs">
<p>
Set the representation of a device within Home Assistant.<br />
This will not communicate with the actual device.
[[localize('ui.panel.developer-tools.tabs.states.description1')]]<br />
[[localize('ui.panel.developer-tools.tabs.states.description2')]]
</p>
<ha-entity-picker
@ -82,7 +84,7 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
allow-custom-entity
></ha-entity-picker>
<paper-input
label="State"
label="[[localize('ui.panel.developer-tools.tabs.states.state')]]"
required
autocapitalize="none"
autocomplete="off"
@ -91,7 +93,9 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
value="{{_state}}"
class="state-input"
></paper-input>
<p>State attributes (YAML, optional)</p>
<p>
[[localize('ui.panel.developer-tools.tabs.states.state_attributes')]]
</p>
<ha-code-editor
mode="yaml"
value="[[_stateAttributes]]"
@ -99,54 +103,58 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
on-value-changed="_yamlChanged"
></ha-code-editor>
<mwc-button on-click="handleSetState" disabled="[[!validJSON]]" raised
>Set State</mwc-button
>[[localize('ui.panel.developer-tools.tabs.states.set_state')]]</mwc-button
>
</div>
<h1>Current entities</h1>
<h1>
[[localize('ui.panel.developer-tools.tabs.states.current_entities')]]
</h1>
<table class="entities">
<tr>
<th>Entity</th>
<th>State</th>
<th>[[localize('ui.panel.developer-tools.tabs.states.entity')]]</th>
<th>[[localize('ui.panel.developer-tools.tabs.states.state')]]</th>
<th hidden$="[[narrow]]">
Attributes
[[localize('ui.panel.developer-tools.tabs.states.attributes')]]
<paper-checkbox checked="{{_showAttributes}}"></paper-checkbox>
</th>
</tr>
<tr>
<th>
<paper-input
label="Filter entities"
label="[[localize('ui.panel.developer-tools.tabs.states.filter_entities')]]"
type="search"
value="{{_entityFilter}}"
></paper-input>
</th>
<th>
<paper-input
label="Filter states"
label="[[localize('ui.panel.developer-tools.tabs.states.filter_states')]]"
type="search"
value="{{_stateFilter}}"
></paper-input>
</th>
<th hidden$="[[!computeShowAttributes(narrow, _showAttributes)]]">
<paper-input
label="Filter attributes"
label="[[localize('ui.panel.developer-tools.tabs.states.filter_attributes')]]"
type="search"
value="{{_attributeFilter}}"
></paper-input>
</th>
</tr>
<tr hidden$="[[!computeShowEntitiesPlaceholder(_entities)]]">
<td colspan="3">No entities</td>
<td colspan="3">
[[localize('ui.panel.developer-tools.tabs.states.no_entities')]]
</td>
</tr>
<template is="dom-repeat" items="[[_entities]]" as="entity">
<tr>
<td>
<paper-icon-button
on-click="entityMoreInfo"
icon="hass:open-in-new"
alt="More Info"
title="More Info"
icon="hass:information-outline"
alt="[[localize('ui.panel.developer-tools.tabs.states.more_info')]]"
title="[[localize('ui.panel.developer-tools.tabs.states.more_info')]]"
>
</paper-icon-button>
<a href="#" on-click="entitySelected">[[entity.entity_id]]</a>
@ -227,14 +235,14 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
var state = ev.model.entity;
this._entityId = state.entity_id;
this._state = state.state;
this._stateAttributes = yaml.safeDump(state.attributes);
this._stateAttributes = safeDump(state.attributes);
ev.preventDefault();
}
entityIdChanged() {
var state = this.hass.states[this._entityId];
this._state = state.state;
this._stateAttributes = yaml.safeDump(state.attributes);
this._stateAttributes = safeDump(state.attributes);
}
entityMoreInfo(ev) {
@ -244,7 +252,11 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
handleSetState() {
if (!this._entityId) {
alert("Entity is a mandatory field");
alert(
this.hass.localize(
"ui.panel.developer-tools.tabs.states.alert_entity_field"
)
);
return;
}
this.hass.callApi("POST", "states/" + this._entityId, {
@ -351,7 +363,7 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
_computeParsedStateAttributes(stateAttributes) {
try {
return stateAttributes.trim() ? yaml.safeLoad(stateAttributes) : {};
return stateAttributes.trim() ? safeLoad(stateAttributes) : {};
} catch (err) {
return ERROR_SENTINEL;
}

View File

@ -3,11 +3,12 @@ import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../components/ha-code-editor";
import "../../../resources/ha-style";
class HaPanelDevTemplate extends PolymerElement {
class HaPanelDevTemplate extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style iron-flex iron-positioning"></style>
@ -60,26 +61,25 @@ class HaPanelDevTemplate extends PolymerElement {
<div class$="[[computeFormClasses(narrow)]]">
<div class="edit-pane">
<p>
Templates are rendered using the Jinja2 template engine with some
Home Assistant specific extensions.
[[localize('ui.panel.developer-tools.tabs.templates.description')]]
</p>
<ul>
<li>
<a
href="http://jinja.pocoo.org/docs/dev/templates/"
target="_blank"
>Jinja2 template documentation</a
>[[localize('ui.panel.developer-tools.tabs.templates.jinja_documentation')]]</a
>
</li>
<li>
<a
href="https://home-assistant.io/docs/configuration/templating/"
target="_blank"
>Home Assistant template extensions</a
>[[localize('ui.panel.developer-tools.tabs.templates.template_extensions')]]</a
>
</li>
</ul>
<p>Template editor</p>
<p>[[localize('ui.panel.developer-tools.tabs.templates.editor')]]</p>
<ha-code-editor
mode="jinja2"
value="[[template]]"
@ -188,7 +188,9 @@ For loop example:
function(error) {
this.processed =
(error && error.body && error.body.message) ||
"Unknown error rendering template";
this.hass.localize(
"ui.panel.developer-tools.tabs.templates.unknown_error_template"
);
this.error = true;
this.rendering = false;
}.bind(this)

View File

@ -0,0 +1,140 @@
import { createBadgeElement } from "../common/create-badge-element";
import { processConfigEntities } from "../common/process-config-entities";
import { LovelaceBadge } from "../types";
import { EntityFilterEntityConfig } from "../entity-rows/types";
import { HomeAssistant } from "../../../types";
import { EntityFilterBadgeConfig } from "./types";
import { evaluateFilter } from "../common/evaluate-filter";
class EntityFilterBadge extends HTMLElement implements LovelaceBadge {
private _elements?: LovelaceBadge[];
private _config?: EntityFilterBadgeConfig;
private _configEntities?: EntityFilterEntityConfig[];
private _hass?: HomeAssistant;
private _oldEntities?: EntityFilterEntityConfig[];
public setConfig(config: EntityFilterBadgeConfig): void {
if (!config.entities || !Array.isArray(config.entities)) {
throw new Error("entities must be specified.");
}
if (
!(config.state_filter && Array.isArray(config.state_filter)) &&
!config.entities.every(
(entity) =>
typeof entity === "object" &&
entity.state_filter &&
Array.isArray(entity.state_filter)
)
) {
throw new Error("Incorrect filter config.");
}
this._config = config;
this._configEntities = undefined;
if (this.lastChild) {
this.removeChild(this.lastChild);
this._elements = undefined;
}
}
set hass(hass: HomeAssistant) {
if (!hass || !this._config) {
return;
}
if (this._elements) {
for (const element of this._elements) {
element.hass = hass;
}
}
if (!this.haveEntitiesChanged(hass)) {
this._hass = hass;
return;
}
this._hass = hass;
if (!this._configEntities) {
this._configEntities = processConfigEntities(this._config.entities);
}
const entitiesList = this._configEntities.filter((entityConf) => {
const stateObj = hass.states[entityConf.entity];
if (!stateObj) {
return false;
}
if (entityConf.state_filter) {
for (const filter of entityConf.state_filter) {
if (evaluateFilter(stateObj, filter)) {
return true;
}
}
} else {
for (const filter of this._config!.state_filter) {
if (evaluateFilter(stateObj, filter)) {
return true;
}
}
}
return false;
});
if (entitiesList.length === 0) {
this.style.display = "none";
return;
}
const isSame =
this._oldEntities &&
entitiesList.length === this._oldEntities.length &&
entitiesList.every((entity, idx) => entity === this._oldEntities![idx]);
if (!isSame) {
this._elements = [];
for (const badgeConfig of entitiesList) {
const element = createBadgeElement(badgeConfig);
element.hass = hass;
this._elements.push(element);
}
this._oldEntities = entitiesList;
}
if (!this._elements) {
return;
}
// Attach element if it has never been attached.
if (!this.lastChild) {
for (const element of this._elements) {
this.appendChild(element);
}
}
this.style.display = "inline";
}
private haveEntitiesChanged(hass: HomeAssistant): boolean {
if (!this._hass) {
return true;
}
if (!this._configEntities || this._hass.localize !== hass.localize) {
return true;
}
for (const config of this._configEntities) {
if (this._hass.states[config.entity] !== hass.states[config.entity]) {
return true;
}
}
return false;
}
}
customElements.define("hui-entity-filter-badge", EntityFilterBadge);

View File

@ -0,0 +1,65 @@
import {
html,
LitElement,
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import { LovelaceBadge } from "../types";
import { HomeAssistant } from "../../../types";
import { ErrorBadgeConfig } from "./types";
import "../../../components/ha-label-badge";
export const createErrorBadgeElement = (config) => {
const el = document.createElement("hui-error-badge");
el.setConfig(config);
return el;
};
export const createErrorBadgeConfig = (error) => ({
type: "error",
error,
});
@customElement("hui-error-badge")
export class HuiErrorBadge extends LitElement implements LovelaceBadge {
public hass?: HomeAssistant;
@property() private _config?: ErrorBadgeConfig;
public setConfig(config: ErrorBadgeConfig): void {
this._config = config;
}
protected render(): TemplateResult | void {
if (!this._config) {
return html``;
}
return html`
<ha-label-badge
label="Error"
icon="hass:alert"
description=${this._config.error}
></ha-label-badge>
`;
}
static get styles(): CSSResult {
return css`
:host {
--ha-label-badge-color: var(--label-badge-red, #fce588);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-error-badge": HuiErrorBadge;
}
}

View File

@ -0,0 +1,74 @@
import {
html,
LitElement,
TemplateResult,
customElement,
property,
} from "lit-element";
import "../../../components/entity/ha-state-label-badge";
import "../components/hui-warning-element";
import { LovelaceBadge } from "../types";
import { HomeAssistant } from "../../../types";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { StateLabelBadgeConfig } from "./types";
import { longPress } from "../common/directives/long-press-directive";
import { hasDoubleClick } from "../common/has-double-click";
import { handleClick } from "../common/handle-click";
@customElement("hui-state-label-badge")
export class HuiStateLabelBadge extends LitElement implements LovelaceBadge {
@property() public hass?: HomeAssistant;
@property() protected _config?: StateLabelBadgeConfig;
public setConfig(config: StateLabelBadgeConfig): void {
this._config = config;
}
protected render(): TemplateResult | void {
if (!this._config || !this.hass) {
return html``;
}
const stateObj = this.hass.states[this._config.entity!];
return html`
<ha-state-label-badge
.hass=${this.hass}
.state=${stateObj}
.title=${this._config.name
? this._config.name
: stateObj
? computeStateName(stateObj)
: ""}
.icon=${this._config.icon}
.image=${this._config.image}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
})}
></ha-state-label-badge>
`;
}
private _handleClick() {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-state-label-badge": HuiStateLabelBadge;
}
}

View File

@ -0,0 +1,22 @@
import { LovelaceBadgeConfig, ActionConfig } from "../../../data/lovelace";
import { EntityFilterEntityConfig } from "../entity-rows/types";
export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig {
type: "entity-filter";
entities: Array<EntityFilterEntityConfig | string>;
state_filter: Array<{ key: string } | string>;
}
export interface ErrorBadgeConfig extends LovelaceBadgeConfig {
error: string;
}
export interface StateLabelBadgeConfig extends LovelaceBadgeConfig {
entity: string;
name?: string;
icon?: string;
image?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}

View File

@ -22,6 +22,7 @@ import {
} from "../../../data/alarm_control_panel";
import { AlarmPanelCardConfig } from "./types";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
const ICONS = {
armed_away: "hass:shield-lock",
@ -81,19 +82,44 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
this._code = "";
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| AlarmPanelCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_config") || changedProps.has("_code")) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass) {
return (
oldHass.states[this._config!.entity] !==
this.hass!.states[this._config!.entity]
);
if (
!oldHass ||
oldHass.themes !== this.hass!.themes ||
oldHass.language !== this.hass!.language
) {
return true;
}
return true;
return (
oldHass.states[this._config!.entity] !==
this.hass!.states[this._config!.entity]
);
}
protected render(): TemplateResult | void {
@ -164,7 +190,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
<mwc-button
.value="${value}"
@click="${this._handlePadClick}"
dense
outlined
>
${value === "clear"
? this._label("clear_code")
@ -226,8 +252,6 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
--alarm-color-armed: var(--label-badge-red);
--alarm-color-autoarm: rgba(0, 153, 255, 0.1);
--alarm-state-color: var(--alarm-color-armed);
--base-unit: 15px;
font-size: calc(var(--base-unit));
}
ha-label-badge {
@ -271,13 +295,11 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
paper-input {
margin: 0 auto 8px;
max-width: 150px;
font-size: calc(var(--base-unit));
text-align: center;
}
.state {
margin-left: 16px;
font-size: calc(var(--base-unit) * 0.9);
position: relative;
bottom: 16px;
color: var(--alarm-state-color);
@ -289,14 +311,14 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
justify-content: center;
flex-wrap: wrap;
margin: auto;
width: 300px;
width: 100%;
max-width: 300px;
}
#keypad mwc-button {
margin-bottom: 5%;
text-size: 20px;
padding: 8px;
width: 30%;
padding: calc(var(--base-unit));
font-size: calc(var(--base-unit) * 1.1);
box-sizing: border-box;
}
@ -306,11 +328,9 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
display: flex;
flex-wrap: wrap;
justify-content: center;
font-size: calc(var(--base-unit) * 1);
}
.actions mwc-button {
min-width: calc(var(--base-unit) * 9);
margin: 0 4px 4px;
}

View File

@ -12,17 +12,13 @@ import {
import "../../../components/ha-card";
import "../components/hui-entities-toggle";
import { fireEvent } from "../../../common/dom/fire_event";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { HomeAssistant } from "../../../types";
import { EntityRow } from "../entity-rows/types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { processConfigEntities } from "../common/process-config-entities";
import { createRowElement } from "../common/create-row-element";
import { EntitiesCardConfig, EntitiesCardEntityConfig } from "./types";
import { computeDomain } from "../../../common/entity/compute_domain";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
@customElement("hui-entities-card")
class HuiEntitiesCard extends LitElement implements LovelaceCard {
@ -71,9 +67,22 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
this._configEntities = entities;
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (this._hass && this._config) {
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this._hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| EntitiesCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this._hass.themes, this._config.theme);
}
}
@ -82,16 +91,27 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
if (!this._config || !this._hass) {
return html``;
}
const { show_header_toggle, title } = this._config;
return html`
<ha-card>
${!title && !show_header_toggle
${!this._config.title &&
!this._config.show_header_toggle &&
!this._config.icon
? html``
: html`
<div class="card-header">
<div class="name">${title}</div>
${show_header_toggle === false
<div class="name">
${this._config.icon
? html`
<ha-icon
class="icon"
.icon="${this._config.icon}"
></ha-icon>
`
: ""}
${this._config.title}
</div>
${this._config.show_header_toggle === false
? html``
: html`
<hui-entities-toggle
@ -137,8 +157,8 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
overflow: hidden;
}
.state-card-dialog {
cursor: pointer;
.icon {
padding: 0px 18px 0px 8px;
}
`;
}
@ -148,23 +168,11 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
if (this._hass) {
element.hass = this._hass;
}
if (
entityConf.entity &&
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(entityConf.entity))
) {
element.classList.add("state-card-dialog");
element.addEventListener("click", () => this._handleClick(entityConf));
}
return html`
<div>${element}</div>
`;
}
private _handleClick(entityConf: EntitiesCardEntityConfig): void {
const entityId = entityConf.entity;
fireEvent(this, "hass-more-info", { entityId });
}
}
declare global {

View File

@ -19,7 +19,7 @@ import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { stateIcon } from "../../../common/entity/state_icon";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeDomain } from "../../../common/entity/compute_domain";
import { HomeAssistant, LightEntity } from "../../../types";
@ -28,6 +28,7 @@ import { longPress } from "../common/directives/long-press-directive";
import { handleClick } from "../common/handle-click";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { EntityButtonCardConfig } from "./types";
import { hasDoubleClick } from "../common/has-double-click";
@customElement("hui-entity-button-card")
class HuiEntityButtonCard extends LitElement implements LovelaceCard {
@ -61,6 +62,7 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
this._config = {
theme: "default",
hold_action: { action: "more-info" },
double_tap_action: { action: "none" },
show_icon: true,
show_name: true,
...config,
@ -89,13 +91,19 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass) {
return (
oldHass.states[this._config!.entity] !==
this.hass!.states[this._config!.entity]
);
if (
!oldHass ||
oldHass.themes !== this.hass!.themes ||
oldHass.language !== this.hass!.language
) {
return true;
}
return true;
return (
oldHass.states[this._config!.entity] !==
this.hass!.states[this._config!.entity]
);
}
protected render(): TemplateResult | void {
@ -118,9 +126,12 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
return html`
<ha-card
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
})}
>
${this._config.show_icon
? html`
@ -156,7 +167,16 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
const oldConfig = changedProps.get("_config") as
| EntityButtonCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
@ -212,12 +232,16 @@ class HuiEntityButtonCard extends LitElement implements LovelaceCard {
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
private _handleClick() {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
}
}

View File

@ -7,9 +7,9 @@ import {
css,
CSSResult,
} from "lit-element";
import { safeDump } from "js-yaml";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { ErrorCardConfig } from "./types";
@ -46,7 +46,7 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
return html`
${this._config.error}
<pre>${this._toStr(this._config.origConfig)}</pre>
<pre>${safeDump(this._config.origConfig)}</pre>
`;
}
@ -63,10 +63,6 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
}
`;
}
private _toStr(config: LovelaceCardConfig): string {
return JSON.stringify(config, null, 2);
}
}
declare global {

View File

@ -14,7 +14,7 @@ import "../../../components/ha-card";
import "../components/hui-warning";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { HomeAssistant } from "../../../types";
@ -149,8 +149,16 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| GaugeCardConfig
| undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}

View File

@ -11,7 +11,7 @@ import {
import { classMap } from "lit-html/directives/class-map";
import { computeStateName } from "../../../common/entity/compute_state_name";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import relativeTime from "../../../common/datetime/relative_time";
import "../../../components/entity/state-badge";
@ -26,6 +26,7 @@ import { longPress } from "../common/directives/long-press-directive";
import { processConfigEntities } from "../common/process-config-entities";
import { handleClick } from "../common/handle-click";
import { GlanceCardConfig, GlanceConfigEntity } from "./types";
import { hasDoubleClick } from "../common/has-double-click";
@customElement("hui-glance-card")
export class HuiGlanceCard extends LitElement implements LovelaceCard {
@ -86,17 +87,23 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass && this._configEntities) {
for (const entity of this._configEntities) {
if (
oldHass.states[entity.entity] !== this.hass!.states[entity.entity]
) {
return true;
}
}
return false;
if (
!this._configEntities ||
!oldHass ||
oldHass.themes !== this.hass!.themes ||
oldHass.language !== this.hass!.language
) {
return true;
}
return true;
for (const entity of this._configEntities) {
if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) {
return true;
}
}
return false;
}
protected render(): TemplateResult | void {
@ -116,14 +123,23 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
`;
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProperties.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| GlanceCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
@ -182,10 +198,13 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
return html`
<div
class="entity"
.entityConf="${entityConf}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
.config="${entityConf}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(entityConf.double_tap_action),
})}
>
${this._config!.show_name !== false
? html`
@ -206,7 +225,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
></state-badge>
`
: ""}
${this._config!.show_state !== false
${this._config!.show_state !== false && entityConf.show_state !== false
? html`
<div>
${entityConf.show_last_changed
@ -226,14 +245,19 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
`;
}
private _handleTap(ev: MouseEvent): void {
const config = (ev.currentTarget as any).entityConf as GlanceConfigEntity;
handleClick(this, this.hass!, config, false);
private _handleClick(ev: MouseEvent): void {
const config = (ev.currentTarget as any).config as GlanceConfigEntity;
handleClick(this, this.hass!, config, false, false);
}
private _handleHold(ev: MouseEvent): void {
const config = (ev.currentTarget as any).entityConf as GlanceConfigEntity;
handleClick(this, this.hass!, config, true);
const config = (ev.currentTarget as any).config as GlanceConfigEntity;
handleClick(this, this.hass!, config, true, false);
}
private _handleDblClick(ev: MouseEvent): void {
const config = (ev.currentTarget as any).config as GlanceConfigEntity;
handleClick(this, this.hass!, config, false, true);
}
}

View File

@ -11,7 +11,7 @@ import "@thomasloven/round-slider";
import { stateIcon } from "../../../common/entity/state_icon";
import { computeStateName } from "../../../common/entity/compute_state_name";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import "../../../components/ha-card";
import "../../../components/ha-icon";
@ -112,12 +112,12 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
filter: this._computeBrightness(stateObj),
color: this._computeColor(stateObj),
})}"
@click="${this._handleTap}"
@click="${this._handleClick}"
></ha-icon>
</div>
<div id="tooltip">
<div class="brightness" @ha-click="${this._handleTap}">
<div class="brightness" @ha-click="${this._handleClick}">
${brightness} %
</div>
<div class="name">
@ -145,7 +145,16 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
const oldConfig = changedProps.get("_config") as
| LightCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
@ -212,10 +221,12 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
.name {
position: absolute;
top: 160px;
left: 50%;
transform: translate(-50%);
font-size: var(--name-font-size);
bottom: 16px;
box-sizing: border-box;
text-align: center;
width: 100%;
padding: 0 16px;
}
.brightness {
@ -297,7 +308,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
private _handleTap() {
private _handleClick() {
toggleEntity(this.hass!, this._config!.entity!);
}

View File

@ -325,13 +325,25 @@ class HuiMapCard extends LitElement implements LovelaceCard {
continue;
}
// create icon
let iconHTML = "";
if (icon) {
const el = document.createElement("ha-icon");
el.setAttribute("icon", icon);
iconHTML = el.outerHTML;
} else {
const el = document.createElement("span");
el.innerHTML = title;
iconHTML = el.outerHTML;
}
// create marker with the icon
mapItems.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: icon ? `<ha-icon icon="${icon}"></ha-icon>` : title,
html: iconHTML,
iconSize: [24, 24],
className: "",
className: this._config!.dark_mode === true ? "dark" : "light",
}),
interactive: false,
title,
@ -455,6 +467,14 @@ class HuiMapCard extends LitElement implements LovelaceCard {
:host([ispanel]) #root {
height: 100%;
}
.dark {
color: #ffffff;
}
.light {
color: #000000;
}
`;
}
}

View File

@ -6,6 +6,7 @@ import {
property,
css,
CSSResult,
PropertyValues,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
@ -17,6 +18,7 @@ import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { MarkdownCardConfig } from "./types";
import { subscribeRenderTemplate } from "../../../data/ws-templates";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
@customElement("hui-markdown-card")
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
@ -84,6 +86,26 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
`;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this._hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| MarkdownCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this._hass.themes, this._config.theme);
}
}
private async _connect() {
if (!this._unsubRenderTemplate && this._hass && this._config) {
this._unsubRenderTemplate = subscribeRenderTemplate(

View File

@ -6,6 +6,7 @@ import {
property,
css,
CSSResult,
PropertyValues,
} from "lit-element";
import "../../../components/ha-card";
@ -16,6 +17,8 @@ import { classMap } from "lit-html/directives/class-map";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { PictureCardConfig } from "./types";
import { hasDoubleClick } from "../common/has-double-click";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
@customElement("hui-picture-card")
export class HuiPictureCard extends LitElement implements LovelaceCard {
@ -48,6 +51,26 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
this._config = config;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| PictureCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
protected render(): TemplateResult | void {
if (!this._config || !this.hass) {
return html``;
@ -55,9 +78,12 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
return html`
<ha-card
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
})}
class="${classMap({
clickable: Boolean(
this._config.tap_action || this._config.hold_action
@ -86,12 +112,16 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
`;
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
private _handleClick() {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
}
}

View File

@ -6,6 +6,7 @@ import {
customElement,
css,
CSSResult,
PropertyValues,
} from "lit-element";
import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
@ -13,6 +14,7 @@ import { LovelaceCard } from "../types";
import { HomeAssistant } from "../../../types";
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
import { PictureElementsCardConfig } from "./types";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
@customElement("hui-picture-elements-card")
class HuiPictureElementsCard extends LitElement implements LovelaceCard {
@ -49,6 +51,26 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
this._config = config;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this._hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| PictureElementsCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this._hass.themes, this._config.theme);
}
}
protected render(): TemplateResult | void {
if (!this._config) {
return html``;

View File

@ -25,6 +25,8 @@ import { handleClick } from "../common/handle-click";
import { UNAVAILABLE } from "../../../data/entity";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { PictureEntityCardConfig } from "./types";
import { hasDoubleClick } from "../common/has-double-click";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
@customElement("hui-picture-entity-card")
class HuiPictureEntityCard extends LitElement implements LovelaceCard {
@ -67,6 +69,26 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
return hasConfigOrEntityChanged(this, changedProps);
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| PictureEntityCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
protected render(): TemplateResult | void {
if (!this._config || !this.hass) {
return html``;
@ -124,9 +146,12 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
.cameraView=${this._config.camera_view}
.entity=${this._config.entity}
.aspectRatio=${this._config.aspect_ratio}
@ha-click=${this._handleTap}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
.longPress=${longPress()}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
})}
class=${classMap({
clickable: stateObj.state !== UNAVAILABLE,
})}
@ -177,12 +202,16 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
`;
}
private _handleTap() {
handleClick(this, this.hass!, this._config!, false);
private _handleClick() {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true);
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
}
}

View File

@ -10,15 +10,14 @@ import {
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateIcon } from "../../../common/entity/state_icon";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../components/hui-image";
import "../components/hui-warning-element";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateIcon } from "../../../common/entity/state_icon";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { LovelaceCard, LovelaceCardEditor } from "../types";
@ -26,8 +25,10 @@ import { HomeAssistant } from "../../../types";
import { longPress } from "../common/directives/long-press-directive";
import { processConfigEntities } from "../common/process-config-entities";
import { handleClick } from "../common/handle-click";
import { PictureGlanceCardConfig, ConfigEntity } from "./types";
import { hasDoubleClick } from "../common/has-double-click";
import { PictureGlanceCardConfig, PictureGlanceEntityConfig } from "./types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]);
@ -49,9 +50,9 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
@property() private _config?: PictureGlanceCardConfig;
private _entitiesDialog?: ConfigEntity[];
private _entitiesDialog?: PictureGlanceEntityConfig[];
private _entitiesToggle?: ConfigEntity[];
private _entitiesToggle?: PictureGlanceEntityConfig[];
public getCardSize(): number {
return 3;
@ -93,6 +94,14 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
!oldHass ||
oldHass.themes !== this.hass!.themes ||
oldHass.language !== this.hass!.language
) {
return true;
}
if (this._entitiesDialog) {
for (const entity of this._entitiesDialog) {
if (
@ -116,6 +125,26 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
return false;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| PictureGlanceCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
protected render(): TemplateResult | void {
if (!this._config || !this.hass) {
return html``;
@ -131,9 +160,12 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
this._config.camera_image
),
})}
@ha-click=${this._handleTap}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
.longPress=${longPress()}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
})}
.config=${this._config}
.hass=${this.hass}
.image=${this._config.image}
@ -150,12 +182,12 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
<div class="title">${this._config.title}</div>
`
: ""}
<div>
<div class="row">
${this._entitiesDialog!.map((entityConf) =>
this.renderEntity(entityConf, true)
)}
</div>
<div>
<div class="row">
${this._entitiesToggle!.map((entityConf) =>
this.renderEntity(entityConf, false)
)}
@ -166,7 +198,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
}
private renderEntity(
entityConf: ConfigEntity,
entityConf: PictureGlanceEntityConfig,
dialog: boolean
): TemplateResult {
const stateObj = this.hass!.states[entityConf.entity];
@ -189,34 +221,57 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
}
return html`
<ha-icon
@ha-click=${this._handleTap}
@ha-hold=${this._handleHold}
.longPress=${longPress()}
.config=${entityConf}
class="${classMap({
"state-on": !STATES_OFF.has(stateObj.state),
})}"
.icon="${entityConf.icon || stateIcon(stateObj)}"
title="${`
<div class="wrapper">
<ha-icon
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(entityConf.double_tap_action),
})}
.config=${entityConf}
class="${classMap({
"state-on": !STATES_OFF.has(stateObj.state),
})}"
.icon="${entityConf.icon || stateIcon(stateObj)}"
title="${`
${computeStateName(stateObj)} : ${computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass!.language
)}
this.hass!.localize,
stateObj,
this.hass!.language
)}
`}"
></ha-icon>
></ha-icon>
${this._config!.show_state !== true && entityConf.show_state !== true
? html`
<div class="state"></div>
`
: html`
<div class="state">
${computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass!.language
)}
</div>
`}
</div>
`;
}
private _handleTap(ev: MouseEvent): void {
private _handleClick(ev: MouseEvent): void {
const config = (ev.currentTarget as any).config as any;
handleClick(this, this.hass!, config, false);
handleClick(this, this.hass!, config, false, false);
}
private _handleHold(ev: MouseEvent): void {
const config = (ev.currentTarget as any).config as any;
handleClick(this, this.hass!, config, true);
handleClick(this, this.hass!, config, true, false);
}
private _handleDblClick(ev: MouseEvent): void {
const config = (ev.currentTarget as any).config as any;
handleClick(this, this.hass!, config, false, true);
}
static get styles(): CSSResult {
@ -249,6 +304,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
color: white;
display: flex;
justify-content: space-between;
flex-direction: row;
}
.box .title {
@ -265,6 +321,30 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
ha-icon.state-on {
color: white;
}
ha-icon.show-state {
width: 20px;
height: 20px;
padding-bottom: 4px;
padding-top: 4px;
}
.state {
display: block;
font-size: 12px;
text-align: center;
line-height: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row {
display: flex;
flex-direction: row;
}
.wrapper {
display: flex;
flex-direction: column;
width: 40px;
}
`;
}
}

View File

@ -20,6 +20,7 @@ import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { PlantStatusCardConfig, PlantAttributeTarget } from "./types";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
const SENSORS = {
moisture: "hass:water",
@ -60,6 +61,26 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard {
return hasConfigOrEntityChanged(this, changedProps);
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| PlantStatusCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
protected render(): TemplateResult | void {
if (!this.hass || !this._config) {
return html``;

View File

@ -11,7 +11,7 @@ import {
} from "lit-element";
import "@polymer/paper-spinner/paper-spinner";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateIcon } from "../../../common/entity/state_icon";
@ -277,7 +277,16 @@ class HuiSensorCard extends LitElement implements LovelaceCard {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
const oldConfig = changedProps.get("_config") as
| SensorCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config!.theme);
}

View File

@ -6,6 +6,7 @@ import {
CSSResult,
property,
customElement,
PropertyValues,
} from "lit-element";
import { repeat } from "lit-html/directives/repeat";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
@ -23,7 +24,8 @@ import {
clearItems,
addItem,
} from "../../../data/shopping-list";
import { ShoppingListCardConfig } from "./types";
import { ShoppingListCardConfig, SensorCardConfig } from "./types";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
@customElement("hui-shopping-list-card")
class HuiShoppingListCard extends LitElement implements LovelaceCard {
@ -77,6 +79,26 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
}
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| SensorCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
protected render(): TemplateResult | void {
if (!this._config || !this.hass) {
return html``;

View File

@ -5,21 +5,23 @@ import {
TemplateResult,
customElement,
property,
css,
CSSResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "@polymer/paper-icon-button/paper-icon-button";
import "@thomasloven/round-slider";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../components/hui-warning";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { loadRoundslider } from "../../../resources/jquery.roundslider.ondemand";
import { UNIT_F } from "../../../common/const";
import { fireEvent } from "../../../common/dom/fire_event";
import { ThermostatCardConfig } from "./types";
@ -29,17 +31,7 @@ import {
compareClimateHvacModes,
CLIMATE_PRESET_NONE,
} from "../../../data/climate";
const thermostatConfig = {
radius: 150,
circleShape: "pie",
startAngle: 315,
width: 5,
lineCap: "round",
handleSize: "+10",
showTooltip: false,
animation: false,
};
import { HassEntity } from "home-assistant-js-websocket";
const modeIcons: { [mode in HvacMode]: string } = {
auto: "hass:calendar-repeat",
@ -63,18 +55,15 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
}
@property() public hass?: HomeAssistant;
@property() private _config?: ThermostatCardConfig;
@property() private _roundSliderStyle?: TemplateResult;
@property() private _jQuery?: any;
private _broadCard?: boolean;
private _loaded?: boolean;
@property() private _loaded?: boolean;
@property() private _setTemp?: number | number[];
private _updated?: boolean;
private _large?: boolean;
private _medium?: boolean;
private _small?: boolean;
private _radius?: number;
public getCardSize(): number {
return 4;
@ -114,26 +103,69 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
}
const mode = stateObj.state in modeIcons ? stateObj.state : "unknown-mode";
const name =
this._config!.name ||
computeStateName(this.hass!.states[this._config!.entity]);
if (!this._radius || this._radius === 0) {
this._radius = 100;
}
return html`
${this.renderStyle()}
<ha-card
class="${classMap({
class=${classMap({
[mode]: true,
large: this._broadCard!,
small: !this._broadCard,
})}"
large: this._large!,
medium: this._medium!,
small: this._small!,
longName: name.length > 10,
})}
>
<div id="root">
<paper-icon-button
icon="hass:dots-vertical"
class="more-info"
@click="${this._handleMoreInfo}"
@click=${this._handleMoreInfo}
></paper-icon-button>
<div id="thermostat"></div>
<div id="thermostat">
${stateObj.state === "unavailable"
? html`
<round-slider
.radius=${this._radius}
disabled="true"
></round-slider>
`
: stateObj.attributes.target_temp_low &&
stateObj.attributes.target_temp_high
? html`
<round-slider
.radius=${this._radius}
.low=${stateObj.attributes.target_temp_low}
.high=${stateObj.attributes.target_temp_high}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.max_temp}
.step=${this._stepSize}
@value-changing=${this._dragEvent}
@value-changed=${this._setTemperature}
></round-slider>
`
: html`
<round-slider
.radius=${this._radius}
.value=${stateObj.attributes.temperature !== null &&
Number.isFinite(Number(stateObj.attributes.temperature))
? stateObj.attributes.temperature
: stateObj.attributes.min_temp}
.step=${this._stepSize}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.max_temp}
@value-changing=${this._dragEvent}
@value-changed=${this._setTemperature}
></round-slider>
`}
</div>
<div id="tooltip">
<div class="title">
${this._config.name || computeStateName(stateObj)}
</div>
<div class="title">${name}</div>
<div class="current-temperature">
<span class="current-temperature-text">
${stateObj.attributes.current_temperature}
@ -147,7 +179,18 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
</span>
</div>
<div class="climate-info">
<div id="set-temperature"></div>
<div id="set-temperature">
${!this._setTemp
? ""
: Array.isArray(this._setTemp)
? html`
${this._setTemp[0].toFixed(1)} -
${this._setTemp[1].toFixed(1)}
`
: html`
${this._setTemp.toFixed(1)}
`}
</div>
<div class="current-mode">
${stateObj.attributes.hvac_action
? this.hass!.localize(
@ -185,13 +228,6 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
return hasConfigOrEntityChanged(this, changedProps);
}
protected firstUpdated(): void {
this._updated = true;
if (this.isConnected && !this._loaded) {
this._initialLoad();
}
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass || !changedProps.has("hass")) {
@ -199,34 +235,42 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| ThermostatCardConfig
| undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
this._setTemp = this._getSetTemp(this.hass!.states[this._config!.entity]);
}
if (!stateObj) {
return;
protected firstUpdated(): void {
this._updated = true;
if (this.isConnected && !this._loaded) {
this._initialLoad();
}
}
private async _initialLoad(): Promise<void> {
this._large = this._medium = this._small = false;
this._radius = this.clientWidth / 3.9;
if (this.clientWidth > 450) {
this._large = true;
} else if (this.clientWidth < 350) {
this._small = true;
} else {
this._medium = true;
}
if (
this._jQuery &&
// If jQuery changed, we just rendered in firstUpdated
!changedProps.has("_jQuery") &&
(!oldHass || oldHass.states[this._config.entity] !== stateObj)
) {
const [sliderValue, uiValue, sliderType] = this._genSliderValue(stateObj);
this._jQuery("#thermostat", this.shadowRoot).roundSlider({
sliderType,
value: sliderValue,
disabled: sliderValue === null,
min: stateObj.attributes.min_temp,
max: stateObj.attributes.max_temp,
});
this._updateSetTemp(uiValue);
}
this._loaded = true;
}
private get _stepSize(): number {
@ -238,119 +282,55 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
return this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5;
}
private async _initialLoad(): Promise<void> {
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
if (!stateObj) {
// Card will require refresh to work again
return;
}
this._loaded = true;
await this.updateComplete;
let radius = this.clientWidth / 3.2;
this._broadCard = this.clientWidth > 390;
if (radius === 0) {
radius = 100;
}
(this.shadowRoot!.querySelector(
"#thermostat"
) as HTMLElement)!.style.height = radius * 2 + "px";
const loaded = await loadRoundslider();
this._roundSliderStyle = loaded.roundSliderStyle;
this._jQuery = loaded.jQuery;
const [sliderValue, uiValue, sliderType] = this._genSliderValue(stateObj);
this._jQuery("#thermostat", this.shadowRoot).roundSlider({
...thermostatConfig,
radius,
min: stateObj.attributes.min_temp,
max: stateObj.attributes.max_temp,
sliderType,
change: (value) => this._setTemperature(value),
drag: (value) => this._dragEvent(value),
value: sliderValue,
disabled: sliderValue === null,
step: this._stepSize,
});
this._updateSetTemp(uiValue);
}
private _genSliderValue(
stateObj: ClimateEntity
): [string | number | null, string, string] {
let sliderType: string;
let sliderValue: string | number | null;
let uiValue: string;
private _getSetTemp(stateObj: HassEntity) {
if (stateObj.state === "unavailable") {
sliderType = "min-range";
sliderValue = null;
uiValue = this.hass!.localize("state.default.unavailable");
} else if (
stateObj.attributes.target_temp_low &&
stateObj.attributes.target_temp_high
) {
sliderType = "range";
sliderValue = `${stateObj.attributes.target_temp_low}, ${
stateObj.attributes.target_temp_high
}`;
uiValue = this.formatTemp(
[
String(stateObj.attributes.target_temp_low),
String(stateObj.attributes.target_temp_high),
],
false
);
} else {
sliderType = "min-range";
sliderValue = Number.isFinite(Number(stateObj.attributes.temperature))
? stateObj.attributes.temperature
: null;
uiValue = sliderValue !== null ? String(sliderValue) : "";
return this.hass!.localize("state.default.unavailable");
}
return [sliderValue, uiValue, sliderType];
}
private _updateSetTemp(value: string): void {
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = value;
}
private _dragEvent(e): void {
this._updateSetTemp(this.formatTemp(String(e.value).split(","), true));
}
private _setTemperature(e): void {
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
if (
stateObj.attributes.target_temp_low &&
stateObj.attributes.target_temp_high
) {
if (e.handle.index === 1) {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
target_temp_low: e.handle.value,
target_temp_high: stateObj.attributes.target_temp_high,
});
} else {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
target_temp_low: stateObj.attributes.target_temp_low,
target_temp_high: e.handle.value,
});
}
return [
stateObj.attributes.target_temp_low,
stateObj.attributes.target_temp_high,
];
}
return stateObj.attributes.temperature;
}
private _dragEvent(e): void {
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
if (e.detail.low) {
this._setTemp = [e.detail.low, stateObj.attributes.target_temp_high];
} else if (e.detail.high) {
this._setTemp = [stateObj.attributes.target_temp_low, e.detail.high];
} else {
this._setTemp = e.detail.value;
}
}
private _setTemperature(e): void {
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
if (e.detail.low) {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
target_temp_low: e.detail.low,
target_temp_high: stateObj.attributes.target_temp_high,
});
} else if (e.detail.high) {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
target_temp_low: stateObj.attributes.target_temp_low,
target_temp_high: e.detail.high,
});
} else {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
temperature: e.value,
temperature: e.detail.value,
});
}
}
@ -382,217 +362,199 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
});
}
private formatTemp(temps: string[], spaceStepSize: boolean): string {
temps = temps.filter(Boolean);
// If we are sliding the slider, append 0 to the temperatures if we're
// having a 0.5 step size, so that the text doesn't jump while sliding
if (spaceStepSize) {
const stepSize = this._stepSize;
temps = temps.map((val) =>
val.includes(".") || stepSize === 1 ? val : `${val}.0`
);
}
return temps.join("-");
}
private renderStyle(): TemplateResult {
return html`
${this._roundSliderStyle}
<style>
:host {
display: block;
}
ha-card {
overflow: hidden;
--rail-border-color: transparent;
--auto-color: green;
--eco-color: springgreen;
--cool-color: #2b9af9;
--heat-color: #ff8100;
--manual-color: #44739e;
--off-color: #8a8a8a;
--fan_only-color: #8a8a8a;
--dry-color: #efbd07;
--idle-color: #8a8a8a;
--unknown-color: #bac;
}
#root {
position: relative;
overflow: hidden;
}
.auto,
.heat_cool {
--mode-color: var(--auto-color);
}
.cool {
--mode-color: var(--cool-color);
}
.heat {
--mode-color: var(--heat-color);
}
.manual {
--mode-color: var(--manual-color);
}
.off {
--mode-color: var(--off-color);
}
.fan_only {
--mode-color: var(--fan_only-color);
}
.eco {
--mode-color: var(--eco-color);
}
.dry {
--mode-color: var(--dry-color);
}
.idle {
--mode-color: var(--idle-color);
}
.unknown-mode {
--mode-color: var(--unknown-color);
}
.no-title {
--title-position-top: 33% !important;
}
.large {
--thermostat-padding-top: 25px;
--thermostat-margin-bottom: 25px;
--title-font-size: 28px;
--title-position-top: 27%;
--climate-info-position-top: 81%;
--set-temperature-font-size: 25px;
--current-temperature-font-size: 71px;
--current-temperature-position-top: 10%;
--current-temperature-text-padding-left: 15px;
--uom-font-size: 20px;
--uom-margin-left: -18px;
--current-mode-font-size: 18px;
--set-temperature-margin-bottom: -5px;
}
.small {
--thermostat-padding-top: 15px;
--thermostat-margin-bottom: 15px;
--title-font-size: 18px;
--title-position-top: 28%;
--climate-info-position-top: 79%;
--set-temperature-font-size: 16px;
--current-temperature-font-size: 25px;
--current-temperature-position-top: 5%;
--current-temperature-text-padding-left: 7px;
--uom-font-size: 12px;
--uom-margin-left: -5px;
--current-mode-font-size: 14px;
--set-temperature-margin-bottom: 0px;
}
#thermostat {
margin: 0 auto var(--thermostat-margin-bottom);
padding-top: var(--thermostat-padding-top);
}
#thermostat .rs-range-color {
background-color: var(--mode-color, var(--disabled-text-color));
}
#thermostat .rs-path-color {
background-color: var(--disabled-text-color);
}
#thermostat .rs-handle {
background-color: var(--paper-card-background-color, white);
padding: 10px;
margin: -10px 0 0 -8px !important;
border: 2px solid var(--disabled-text-color);
}
#thermostat .rs-handle.rs-focus {
border-color: var(--mode-color, var(--disabled-text-color));
}
#thermostat .rs-handle:after {
border-color: var(--mode-color, var(--disabled-text-color));
background-color: var(--mode-color, var(--disabled-text-color));
}
#thermostat .rs-border {
border-color: var(--rail-border-color);
}
#thermostat .rs-bar.rs-transition.rs-first,
.rs-bar.rs-transition.rs-second {
z-index: 20 !important;
}
#thermostat .rs-readonly {
z-index: 10;
top: auto;
}
#thermostat .rs-inner.rs-bg-color.rs-border,
#thermostat .rs-overlay.rs-transition.rs-bg-color {
background-color: var(--paper-card-background-color, white);
}
#tooltip {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
text-align: center;
z-index: 15;
color: var(--primary-text-color);
}
#set-temperature {
font-size: var(--set-temperature-font-size);
margin-bottom: var(--set-temperature-margin-bottom);
min-height: 1.2em;
}
.title {
font-size: var(--title-font-size);
position: absolute;
top: var(--title-position-top);
left: 50%;
transform: translate(-50%, -50%);
}
.climate-info {
position: absolute;
top: var(--climate-info-position-top);
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
}
.current-mode {
font-size: var(--current-mode-font-size);
color: var(--secondary-text-color);
}
.modes {
margin-top: 16px;
}
.modes ha-icon {
color: var(--disabled-text-color);
cursor: pointer;
display: inline-block;
margin: 0 10px;
}
.modes ha-icon.selected-icon {
color: var(--mode-color);
}
.current-temperature {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: var(--current-temperature-font-size);
}
.current-temperature-text {
padding-left: var(--current-temperature-text-padding-left);
}
.uom {
font-size: var(--uom-font-size);
vertical-align: top;
margin-left: var(--uom-margin-left);
}
.more-info {
position: absolute;
cursor: pointer;
top: 0;
right: 0;
z-index: 25;
color: var(--secondary-text-color);
}
</style>
static get styles(): CSSResult {
return css`
:host {
display: block;
}
ha-card {
overflow: hidden;
--rail-border-color: transparent;
--auto-color: green;
--eco-color: springgreen;
--cool-color: #2b9af9;
--heat-color: #ff8100;
--manual-color: #44739e;
--off-color: #8a8a8a;
--fan_only-color: #8a8a8a;
--dry-color: #efbd07;
--idle-color: #8a8a8a;
--unknown-color: #bac;
}
#root {
position: relative;
overflow: hidden;
}
.auto,
.heat_cool {
--mode-color: var(--auto-color);
}
.cool {
--mode-color: var(--cool-color);
}
.heat {
--mode-color: var(--heat-color);
}
.manual {
--mode-color: var(--manual-color);
}
.off {
--mode-color: var(--off-color);
}
.fan_only {
--mode-color: var(--fan_only-color);
}
.eco {
--mode-color: var(--eco-color);
}
.dry {
--mode-color: var(--dry-color);
}
.idle {
--mode-color: var(--idle-color);
}
.unknown-mode {
--mode-color: var(--unknown-color);
}
.no-title {
--title-position-top: 33% !important;
}
.large {
--thermostat-padding-top: 32px;
--thermostat-margin-bottom: 32px;
--title-font-size: 28px;
--title-position-top: 25%;
--climate-info-position-top: 80%;
--set-temperature-font-size: 25px;
--current-temperature-font-size: 71px;
--current-temperature-position-top: 10%;
--current-temperature-text-padding-left: 15px;
--uom-font-size: 20px;
--uom-margin-left: -18px;
--current-mode-font-size: 18px;
--current-mod-margin-top: 6px;
--current-mod-margin-bottom: 12px;
--set-temperature-margin-bottom: -5px;
}
.medium {
--thermostat-padding-top: 20px;
--thermostat-margin-bottom: 20px;
--title-font-size: 23px;
--title-position-top: 27%;
--climate-info-position-top: 84%;
--set-temperature-font-size: 20px;
--current-temperature-font-size: 65px;
--current-temperature-position-top: 10%;
--current-temperature-text-padding-left: 15px;
--uom-font-size: 18px;
--uom-margin-left: -16px;
--current-mode-font-size: 16px;
--current-mod-margin-top: 4px;
--current-mod-margin-bottom: 4px;
--set-temperature-margin-bottom: -5px;
}
.small {
--thermostat-padding-top: 15px;
--thermostat-margin-bottom: 15px;
--title-font-size: 18px;
--title-position-top: 28%;
--climate-info-position-top: 78%;
--set-temperature-font-size: 16px;
--current-temperature-font-size: 55px;
--current-temperature-position-top: 5%;
--current-temperature-text-padding-left: 16px;
--uom-font-size: 16px;
--uom-margin-left: -14px;
--current-mode-font-size: 14px;
--current-mod-margin-top: 2px;
--current-mod-margin-bottom: 4px;
--set-temperature-margin-bottom: 0px;
}
.longName {
--title-font-size: 18px;
}
#thermostat {
margin: 0 auto var(--thermostat-margin-bottom);
padding-top: var(--thermostat-padding-top);
padding-bottom: 32px;
display: flex;
justify-content: center;
align-items: center;
}
#thermostat round-slider {
margin: 0 auto;
display: inline-block;
--round-slider-path-color: var(--disabled-text-color);
--round-slider-bar-color: var(--mode-color);
z-index: 20;
}
#tooltip {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
text-align: center;
z-index: 15;
color: var(--primary-text-color);
}
#set-temperature {
font-size: var(--set-temperature-font-size);
margin-bottom: var(--set-temperature-margin-bottom);
min-height: 1.2em;
}
.title {
font-size: var(--title-font-size);
position: absolute;
top: var(--title-position-top);
left: 50%;
transform: translate(-50%, -50%);
}
.climate-info {
position: absolute;
top: var(--climate-info-position-top);
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
}
.current-mode {
font-size: var(--current-mode-font-size);
color: var(--secondary-text-color);
margin-top: var(--current-mod-margin-top);
margin-bottom: var(--current-mod-margin-bottom);
}
.modes ha-icon {
color: var(--disabled-text-color);
cursor: pointer;
display: inline-block;
margin: 0 10px;
}
.modes ha-icon.selected-icon {
color: var(--mode-color);
}
.current-temperature {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: var(--current-temperature-font-size);
}
.current-temperature-text {
padding-left: var(--current-temperature-text-padding-left);
}
.uom {
font-size: var(--uom-font-size);
vertical-align: top;
margin-left: var(--uom-margin-left);
}
.more-info {
position: absolute;
cursor: pointer;
top: 0;
right: 0;
z-index: 25;
color: var(--secondary-text-color);
}
`;
}
}

View File

@ -22,6 +22,7 @@ import { WeatherForecastCardConfig } from "./types";
import { computeRTL } from "../../../common/util/compute_rtl";
import { fireEvent } from "../../../common/dom/fire_event";
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
const cardinalDirections = [
"N",
@ -92,6 +93,23 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| WeatherForecastCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
if (changedProps.has("hass")) {
toggleAttribute(this, "rtl", computeRTL(this.hass!));
}

View File

@ -8,6 +8,7 @@ export interface AlarmPanelCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
states?: string[];
theme?: string;
}
export interface ConditionalCardConfig extends LovelaceCardConfig {
@ -27,6 +28,9 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
service?: string;
service_data?: object;
url?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export interface EntitiesCardConfig extends LovelaceCardConfig {
@ -35,6 +39,7 @@ export interface EntitiesCardConfig extends LovelaceCardConfig {
title?: string;
entities: EntitiesCardEntityConfig[];
theme?: string;
icon?: string;
}
export interface EntityButtonCardConfig extends LovelaceCardConfig {
@ -46,6 +51,7 @@ export interface EntityButtonCardConfig extends LovelaceCardConfig {
theme?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export interface EntityFilterCardConfig extends LovelaceCardConfig {
@ -80,11 +86,17 @@ export interface GaugeCardConfig extends LovelaceCardConfig {
export interface ConfigEntity extends EntityConfig {
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export interface PictureGlanceEntityConfig extends ConfigEntity {
show_state?: boolean;
}
export interface GlanceConfigEntity extends ConfigEntity {
show_last_changed?: boolean;
image?: string;
show_state?: boolean;
}
export interface GlanceCardConfig extends LovelaceCardConfig {
@ -126,6 +138,7 @@ export interface MarkdownCardConfig extends LovelaceCardConfig {
title?: string;
card_size?: number;
entity_ids?: string | string[];
theme?: string;
}
export interface MediaControlCardConfig extends LovelaceCardConfig {
@ -136,6 +149,8 @@ export interface PictureCardConfig extends LovelaceCardConfig {
image?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
theme?: string;
}
export interface PictureElementsCardConfig extends LovelaceCardConfig {
@ -148,6 +163,7 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
aspect_ratio?: string;
entity?: string;
elements: LovelaceElementConfig[];
theme?: string;
}
export interface PictureEntityCardConfig extends LovelaceCardConfig {
@ -161,12 +177,14 @@ export interface PictureEntityCardConfig extends LovelaceCardConfig {
aspect_ratio?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
show_name?: boolean;
show_state?: boolean;
theme?: string;
}
export interface PictureGlanceCardConfig extends LovelaceCardConfig {
entities: EntityConfig[];
entities: PictureGlanceEntityConfig[];
title?: string;
image?: string;
camera_image?: string;
@ -177,6 +195,9 @@ export interface PictureGlanceCardConfig extends LovelaceCardConfig {
entity?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
show_state?: boolean;
theme?: string;
}
export interface PlantAttributeTarget extends EventTarget {
@ -186,6 +207,7 @@ export interface PlantAttributeTarget extends EventTarget {
export interface PlantStatusCardConfig extends LovelaceCardConfig {
name?: string;
entity: string;
theme?: string;
}
export interface SensorCardConfig extends LovelaceCardConfig {
@ -201,6 +223,7 @@ export interface SensorCardConfig extends LovelaceCardConfig {
export interface ShoppingListCardConfig extends LovelaceCardConfig {
title?: string;
theme?: string;
}
export interface StackCardConfig extends LovelaceCardConfig {

View File

@ -8,6 +8,7 @@ interface Config extends LovelaceElementConfig {
title?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export const computeTooltip = (hass: HomeAssistant, config: Config): string => {

View File

@ -0,0 +1,73 @@
import "../badges/hui-entity-filter-badge";
import "../badges/hui-state-label-badge";
import {
createErrorBadgeElement,
createErrorBadgeConfig,
HuiErrorBadge,
} from "../badges/hui-error-badge";
import { LovelaceBadge } from "../types";
import { LovelaceBadgeConfig } from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
const BADGE_TYPES = new Set(["entity-filter", "error", "state-label"]);
const CUSTOM_TYPE_PREFIX = "custom:";
const TIMEOUT = 2000;
const _createElement = (
tag: string,
config: LovelaceBadgeConfig
): LovelaceBadge => {
const element = document.createElement(tag) as LovelaceBadge;
try {
element.setConfig(config);
} catch (err) {
// tslint:disable-next-line
console.error(tag, err);
return _createErrorElement(err.message);
}
return element;
};
const _createErrorElement = (error: string): HuiErrorBadge =>
createErrorBadgeElement(createErrorBadgeConfig(error));
export const createBadgeElement = (
config: LovelaceBadgeConfig
): LovelaceBadge => {
if (!config || typeof config !== "object") {
return _createErrorElement("No config");
}
let type = config.type;
if (!type) {
type = "state-label";
}
if (type.startsWith(CUSTOM_TYPE_PREFIX)) {
const tag = type.substr(CUSTOM_TYPE_PREFIX.length);
if (customElements.get(tag)) {
return _createElement(tag, config);
}
const element = _createErrorElement(`Type doesn't exist: ${tag}`);
element.style.display = "None";
const timer = window.setTimeout(() => {
element.style.display = "";
}, TIMEOUT);
customElements.whenDefined(tag).then(() => {
clearTimeout(timer);
fireEvent(element, "ll-badge-rebuild");
});
return element;
}
if (!BADGE_TYPES.has(type)) {
return _createErrorElement(`Unknown type: ${type}`);
}
return _createElement(`hui-${type}-badge`, config);
};

View File

@ -1,5 +1,6 @@
import { directive, PropertyPart } from "lit-html";
import "@material/mwc-ripple";
import { LongPressOptions } from "../../../../data/lovelace";
const isTouch =
"ontouchstart" in window ||
@ -8,7 +9,7 @@ const isTouch =
interface LongPress extends HTMLElement {
holdTime: number;
bind(element: Element): void;
bind(element: Element, options): void;
}
interface LongPressElement extends Element {
longPress?: boolean;
@ -21,6 +22,7 @@ class LongPress extends HTMLElement implements LongPress {
protected held: boolean;
protected cooldownStart: boolean;
protected cooldownEnd: boolean;
private dblClickTimeout: number | undefined;
constructor() {
super();
@ -65,7 +67,7 @@ class LongPress extends HTMLElement implements LongPress {
});
}
public bind(element: LongPressElement) {
public bind(element: LongPressElement, options) {
if (element.longPress) {
return;
}
@ -120,6 +122,15 @@ class LongPress extends HTMLElement implements LongPress {
this.timer = undefined;
if (this.held) {
element.dispatchEvent(new Event("ha-hold"));
} else if (options.hasDoubleClick) {
if ((ev as MouseEvent).detail === 1) {
this.dblClickTimeout = window.setTimeout(() => {
element.dispatchEvent(new Event("ha-click"));
}, 250);
} else {
clearTimeout(this.dblClickTimeout);
element.dispatchEvent(new Event("ha-dblclick"));
}
} else {
element.dispatchEvent(new Event("ha-click"));
}
@ -127,9 +138,16 @@ class LongPress extends HTMLElement implements LongPress {
window.setTimeout(() => (this.cooldownEnd = false), 100);
};
const handleEnter = (ev: Event) => {
if ((ev as KeyboardEvent).keyCode === 13) {
return clickEnd(ev);
}
};
element.addEventListener("touchstart", clickStart, { passive: true });
element.addEventListener("touchend", clickEnd);
element.addEventListener("touchcancel", clickEnd);
element.addEventListener("keyup", handleEnter);
// iOS 13 sends a complete normal touchstart-touchend series of events followed by a mousedown-click series.
// That might be a bug, but until it's fixed, this should make long-press work.
@ -174,14 +192,19 @@ const getLongPress = (): LongPress => {
return longpress as LongPress;
};
export const longPressBind = (element: LongPressElement) => {
export const longPressBind = (
element: LongPressElement,
options: LongPressOptions
) => {
const longpress: LongPress = getLongPress();
if (!longpress) {
return;
}
longpress.bind(element);
longpress.bind(element, options);
};
export const longPress = directive(() => (part: PropertyPart) => {
longPressBind(part.committer.element);
});
export const longPress = directive(
(options: LongPressOptions = {}) => (part: PropertyPart) => {
longPressBind(part.committer.element, options);
}
);

View File

@ -34,6 +34,7 @@ import {
subscribeEntityRegistry,
EntityRegistryEntry,
} from "../../../data/entity_registry";
import { processEditorEntities } from "../editor/process-editor-entities";
const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
const DOMAINS_BADGES = [
@ -315,7 +316,7 @@ const generateViewConfig = (
const view: LovelaceViewConfig = {
path,
title,
badges,
badges: processEditorEntities(badges),
cards,
};

View File

@ -13,12 +13,16 @@ export const handleClick = (
camera_image?: string;
hold_action?: ActionConfig;
tap_action?: ActionConfig;
double_tap_action?: ActionConfig;
},
hold: boolean
hold: boolean,
dblClick: boolean
): void => {
let actionConfig: ActionConfig | undefined;
if (hold && config.hold_action) {
if (dblClick && config.double_tap_action) {
actionConfig = config.double_tap_action;
} else if (hold && config.hold_action) {
actionConfig = config.hold_action;
} else if (!hold && config.tap_action) {
actionConfig = config.tap_action;
@ -30,6 +34,23 @@ export const handleClick = (
};
}
if (
actionConfig.confirmation &&
(!actionConfig.confirmation.exemptions ||
!actionConfig.confirmation.exemptions.some(
(e) => e.user === hass!.user!.id
))
) {
if (
!confirm(
actionConfig.confirmation.text ||
`Are you sure you want to ${actionConfig.action}?`
)
) {
return;
}
}
switch (actionConfig.action) {
case "more-info":
if (config.entity || config.camera_image) {

View File

@ -15,6 +15,13 @@ export function hasConfigOrEntityChanged(
return true;
}
if (
oldHass.themes !== element.hass!.themes ||
oldHass.language !== element.hass!.language
) {
return true;
}
return (
oldHass.states[element._config!.entity] !==
element.hass!.states[element._config!.entity] ||

View File

@ -0,0 +1,6 @@
import { ActionConfig } from "../../../data/lovelace";
// Check if config or Entity changed
export function hasDoubleClick(config?: ActionConfig): boolean {
return config !== undefined && config.action !== "none";
}

View File

@ -165,7 +165,7 @@ export class HuiCardOptions extends LitElement {
}
private _deleteCard(): void {
confDeleteCard(this.lovelace!, this.path!);
confDeleteCard(this, this.hass!, this.lovelace!, this.path!);
}
}

View File

@ -16,8 +16,14 @@ import "../components/hui-warning";
import { HomeAssistant } from "../../../types";
import { computeRTL } from "../../../common/util/compute_rtl";
import { EntitiesCardEntityConfig } from "../cards/types";
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { longPress } from "../common/directives/long-press-directive";
import { hasDoubleClick } from "../common/has-double-click";
import { handleClick } from "../common/handle-click";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { classMap } from "lit-html/directives/class-map";
import { EntitiesCardEntityConfig } from "../cards/types";
class HuiGenericEntityRow extends LitElement {
@property() public hass?: HomeAssistant;
@ -46,15 +52,45 @@ class HuiGenericEntityRow extends LitElement {
`;
}
const pointer =
(this.config.tap_action && this.config.tap_action.action !== "none") ||
(this.config.entity &&
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this.config.entity)));
return html`
<state-badge
class=${classMap({
pointer,
})}
.hass=${this.hass}
.stateObj=${stateObj}
.overrideIcon=${this.config.icon}
.overrideImage=${this.config.image}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this.config.double_tap_action),
})}
tabindex="0"
></state-badge>
<div class="flex">
<div class="info">
<div
class=${classMap({
info: true,
pointer,
padName: this.showSecondary && !this.config.secondary_info,
padSecondary: Boolean(
!this.showSecondary || this.config.secondary_info
),
})}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this.config.double_tap_action),
})}
>
${this.config.name || computeStateName(stateObj)}
<div class="secondary">
${!this.showSecondary
@ -86,6 +122,18 @@ class HuiGenericEntityRow extends LitElement {
}
}
private _handleClick(): void {
handleClick(this, this.hass!, this.config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this.config!, true, false);
}
private _handleDblClick(): void {
handleClick(this, this.hass!, this.config!, false, true);
}
static get styles(): CSSResult {
return css`
:host {
@ -132,6 +180,15 @@ class HuiGenericEntityRow extends LitElement {
margin-left: 0;
margin-right: 8px;
}
.pointer {
cursor: pointer;
}
.padName {
padding: 12px 0px;
}
.padSecondary {
padding: 4px 0px;
}
`;
}
}

View File

@ -8,7 +8,7 @@ import {
property,
} from "lit-element";
import yaml from "js-yaml";
import { safeDump, safeLoad } from "js-yaml";
import "@material/mwc-button";
import { HomeAssistant } from "../../../../types";
@ -63,7 +63,7 @@ export class HuiCardEditor extends LitElement {
public set yaml(_yaml: string) {
this._yaml = _yaml;
try {
this._config = yaml.safeLoad(this.yaml);
this._config = safeLoad(this.yaml);
this._updateConfigElement();
this._error = undefined;
} catch (err) {
@ -80,7 +80,7 @@ export class HuiCardEditor extends LitElement {
}
public set value(config: LovelaceCardConfig | undefined) {
if (JSON.stringify(config) !== JSON.stringify(this._config || {})) {
this.yaml = yaml.safeDump(config);
this.yaml = safeDump(config);
}
}
@ -229,7 +229,7 @@ export class HuiCardEditor extends LitElement {
configElement = await elClass.getConfigElement();
} else {
configElement = undefined;
throw Error(`WARNING: No GUI editor available for: ${cardType}`);
throw Error(`WARNING: No visual editor available for: ${cardType}`);
}
this._configElement = configElement;

View File

@ -49,9 +49,6 @@ export class HuiCardPicker extends LitElement {
protected render(): TemplateResult | void {
return html`
<h3>
${this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card")}
</h3>
<div class="cards-container">
${cards.map((card: string) => {
return html`

View File

@ -10,7 +10,10 @@ import {
import { HomeAssistant } from "../../../../types";
import { HASSDomEvent } from "../../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../../data/lovelace";
import {
LovelaceCardConfig,
LovelaceViewConfig,
} from "../../../../data/lovelace";
import "./hui-card-editor";
// tslint:disable-next-line
import { HuiCardEditor } from "./hui-card-editor";
@ -40,6 +43,7 @@ export class HuiDialogEditCard extends LitElement {
@property() private _params?: EditCardDialogParams;
@property() private _cardConfig?: LovelaceCardConfig;
@property() private _viewConfig!: LovelaceViewConfig;
@property() private _saving: boolean = false;
@property() private _error?: string;
@ -47,10 +51,9 @@ export class HuiDialogEditCard extends LitElement {
public async showDialog(params: EditCardDialogParams): Promise<void> {
this._params = params;
const [view, card] = params.path;
this._viewConfig = params.lovelace.config.views[view];
this._cardConfig =
card !== undefined
? params.lovelace.config.views[view].cards![card]
: undefined;
card !== undefined ? this._viewConfig.cards![card] : undefined;
}
private get _cardEditorEl(): HuiCardEditor | null {
@ -67,6 +70,14 @@ export class HuiDialogEditCard extends LitElement {
heading = `${this.hass!.localize(
`ui.panel.lovelace.editor.card.${this._cardConfig.type}.name`
)} ${this.hass!.localize("ui.panel.lovelace.editor.edit_card.header")}`;
} else if (!this._cardConfig) {
heading = this._viewConfig.title
? this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
"name",
`"${this._viewConfig.title}"`
)
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
} else {
heading = this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.header"
@ -117,16 +128,20 @@ export class HuiDialogEditCard extends LitElement {
<mwc-button @click="${this._close}">
${this.hass!.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
?disabled="${!this._canSave || this._saving}"
@click="${this._save}"
>
${this._saving
? html`
<paper-spinner active alt="Saving"></paper-spinner>
`
: this.hass!.localize("ui.common.save")}
</mwc-button>
${this._cardConfig !== undefined
? html`
<mwc-button
?disabled="${!this._canSave || this._saving}"
@click="${this._save}"
>
${this._saving
? html`
<paper-spinner active alt="Saving"></paper-spinner>
`
: this.hass!.localize("ui.common.save")}
</mwc-button>
`
: ``}
</div>
</ha-paper-dialog>
`;

View File

@ -17,16 +17,18 @@ import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { configElementStyle } from "./config-elements-style";
import { AlarmPanelCardConfig } from "../../cards/types";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-icon";
import { AlarmPanelCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
const cardConfigStruct = struct({
type: "string",
entity: "string?",
name: "string?",
states: "array?",
theme: "string?",
});
@customElement("hui-alarm-panel-card-editor")
@ -53,6 +55,10 @@ export class HuiAlarmPanelCardEditor extends LitElement
return this._config!.states || [];
}
get _theme(): string {
return this._config!.theme || "Backend-selected";
}
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
@ -113,6 +119,12 @@ export class HuiAlarmPanelCardEditor extends LitElement
})}
</paper-listbox>
</paper-dropdown-menu>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
`;
}

View File

@ -16,10 +16,13 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { configElementStyle } from "./config-elements-style";
import { MarkdownCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
const cardConfigStruct = struct({
type: "string",
title: "string?",
content: "string",
theme: "string?",
});
@customElement("hui-markdown-card-editor")
@ -42,6 +45,10 @@ export class HuiMarkdownCardEditor extends LitElement
return this._config!.content || "";
}
get _theme(): string {
return this._config!.theme || "Backend-selected";
}
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
@ -73,6 +80,12 @@ export class HuiMarkdownCardEditor extends LitElement
autocomplete="off"
spellcheck="false"
></paper-textarea>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
`;
}

View File

@ -8,6 +8,7 @@ import {
import "@polymer/paper-input/paper-input";
import "../../components/hui-action-editor";
import "../../components/hui-theme-select-editor";
import { struct } from "../../common/structs/struct";
import {
@ -27,6 +28,7 @@ const cardConfigStruct = struct({
image: "string?",
tap_action: struct.optional(actionConfigStruct),
hold_action: struct.optional(actionConfigStruct),
theme: "string?",
});
@customElement("hui-picture-card-editor")
@ -53,6 +55,10 @@ export class HuiPictureCardEditor extends LitElement
return this._config!.hold_action || { action: "none" };
}
get _theme(): string {
return this._config!.theme || "Backend-selected";
}
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
@ -98,6 +104,12 @@ export class HuiPictureCardEditor extends LitElement
.configValue="${"hold_action"}"
@action-changed="${this._valueChanged}"
></hui-action-editor>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
</div>
`;

View File

@ -13,6 +13,7 @@ import "@polymer/paper-listbox/paper-listbox";
import "../../components/hui-action-editor";
import "../../components/hui-entity-editor";
import "../../../../components/ha-switch";
import "../../components/hui-theme-select-editor";
import { struct } from "../../common/structs/struct";
import {
@ -39,6 +40,7 @@ const cardConfigStruct = struct({
hold_action: struct.optional(actionConfigStruct),
show_name: "boolean?",
show_state: "boolean?",
theme: "string?",
});
@customElement("hui-picture-entity-card-editor")
@ -93,6 +95,10 @@ export class HuiPictureEntityCardEditor extends LitElement
return this._config!.show_state || true;
}
get _theme(): string {
return this._config!.theme || "Backend-selected";
}
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
@ -225,6 +231,12 @@ export class HuiPictureEntityCardEditor extends LitElement
.configValue="${"hold_action"}"
@action-changed="${this._valueChanged}"
></hui-action-editor>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
</div>
`;

View File

@ -13,6 +13,7 @@ import "@polymer/paper-listbox/paper-listbox";
import "../../components/hui-action-editor";
import "../../components/hui-entity-editor";
import "../../../../components/entity/ha-entity-picker";
import "../../components/hui-theme-select-editor";
import { struct } from "../../common/structs/struct";
import {
@ -41,6 +42,7 @@ const cardConfigStruct = struct({
tap_action: struct.optional(actionConfigStruct),
hold_action: struct.optional(actionConfigStruct),
entities: [entitiesConfigStruct],
theme: "string?",
});
@customElement("hui-picture-glance-card-editor")
@ -104,6 +106,10 @@ export class HuiPictureGlanceCardEditor extends LitElement
return this._config!.show_state || false;
}
get _theme(): string {
return this._config!.theme || "Backend-selected";
}
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
@ -224,6 +230,12 @@ export class HuiPictureGlanceCardEditor extends LitElement
.entities="${this._configEntities}"
@entities-changed="${this._valueChanged}"
></hui-entity-editor>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
`;
}

View File

@ -9,6 +9,7 @@ import "@polymer/paper-input/paper-input";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-icon";
import "../../components/hui-theme-select-editor";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
@ -22,6 +23,7 @@ const cardConfigStruct = struct({
type: "string",
entity: "string",
name: "string?",
theme: "string?",
});
@customElement("hui-plant-status-card-editor")
@ -44,6 +46,10 @@ export class HuiPlantStatusCardEditor extends LitElement
return this._config!.name || "";
}
get _theme(): string {
return this._config!.theme || "Backend-selected";
}
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
@ -75,6 +81,12 @@ export class HuiPlantStatusCardEditor extends LitElement
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
`;
}

View File

@ -14,9 +14,12 @@ import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { ShoppingListCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
const cardConfigStruct = struct({
type: "string",
title: "string?",
theme: "string?",
});
@customElement("hui-shopping-list-card-editor")
@ -35,6 +38,10 @@ export class HuiShoppingListEditor extends LitElement
return this._config!.title || "";
}
get _theme(): string {
return this._config!.theme || "Backend-selected";
}
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
@ -52,6 +59,12 @@ export class HuiShoppingListEditor extends LitElement
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
`;
}

View File

@ -7,6 +7,7 @@ import {
} from "lit-element";
import "../../../../components/entity/ha-entity-picker";
import "../../components/hui-theme-select-editor";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
@ -20,6 +21,7 @@ const cardConfigStruct = struct({
type: "string",
entity: "string?",
name: "string?",
theme: "string?",
});
@customElement("hui-weather-forecast-card-editor")
@ -42,6 +44,10 @@ export class HuiWeatherForecastCardEditor extends LitElement
return this._config!.name || "";
}
get _theme(): string {
return this._config!.theme || "Backend-selected";
}
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
@ -73,6 +79,12 @@ export class HuiWeatherForecastCardEditor extends LitElement
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
`;
}

View File

@ -1,16 +1,22 @@
import { Lovelace } from "../types";
import { deleteCard } from "./config-util";
import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
import { HomeAssistant } from "../../../types";
export async function confDeleteCard(
element: HTMLElement,
hass: HomeAssistant,
lovelace: Lovelace,
path: [number, number]
): Promise<void> {
if (!confirm("Are you sure you want to delete this card?")) {
return;
}
try {
await lovelace.saveConfig(deleteCard(lovelace.config, path));
} catch (err) {
alert(`Deleting failed: ${err.message}`);
}
showConfirmationDialog(element, {
text: hass.localize("ui.panel.lovelace.cards.confirm_delete"),
confirm: async () => {
try {
await lovelace.saveConfig(deleteCard(lovelace.config, path));
} catch (err) {
alert(`Deleting failed: ${err.message}`);
}
},
});
}

View File

@ -26,14 +26,15 @@ import { HomeAssistant } from "../../../../types";
import {
LovelaceViewConfig,
LovelaceCardConfig,
LovelaceBadgeConfig,
} from "../../../../data/lovelace";
import { fireEvent } from "../../../../common/dom/fire_event";
import { EntitiesEditorEvent, ViewEditEvent } from "../types";
import { processEditorEntities } from "../process-editor-entities";
import { EntityConfig } from "../../entity-rows/types";
import { navigate } from "../../../../common/navigate";
import { Lovelace } from "../../types";
import { deleteView, addView, replaceView } from "../config-util";
import { showConfirmationDialog } from "../../../../dialogs/confirmation/show-dialog-confirmation";
@customElement("hui-edit-view")
export class HuiEditView extends LitElement {
@ -45,7 +46,7 @@ export class HuiEditView extends LitElement {
@property() private _config?: LovelaceViewConfig;
@property() private _badges?: EntityConfig[];
@property() private _badges?: LovelaceBadgeConfig[];
@property() private _cards?: LovelaceCardConfig[];
@ -87,6 +88,18 @@ export class HuiEditView extends LitElement {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
private get _viewConfigTitle(): string {
if (!this._config || !this._config.title) {
return this.hass!.localize("ui.panel.lovelace.editor.edit_view.header");
}
return this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.header_name",
"name",
this._config.title
);
}
protected render(): TemplateResult | void {
let content;
switch (this._curTab) {
@ -118,7 +131,7 @@ export class HuiEditView extends LitElement {
return html`
<ha-paper-dialog with-backdrop>
<h2>
${this.hass!.localize("ui.panel.lovelace.editor.edit_view.header")}
${this._viewConfigTitle}
</h2>
<paper-tabs
scrollable
@ -133,12 +146,11 @@ export class HuiEditView extends LitElement {
<div class="paper-dialog-buttons">
${this.viewIndex !== undefined
? html`
<paper-icon-button
class="delete"
title="Delete"
icon="hass:delete"
@click="${this._delete}"
></paper-icon-button>
<mwc-button class="delete" @click="${this._deleteConfirm}">
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.delete"
)}
</mwc-button>
`
: ""}
<mwc-button @click="${this._closeDialog}"
@ -160,17 +172,6 @@ export class HuiEditView extends LitElement {
}
private async _delete(): Promise<void> {
if (this._cards && this._cards.length > 0) {
alert(
"You can't delete a view that has cards in it. Remove the cards first."
);
return;
}
if (!confirm("Are you sure you want to delete this view?")) {
return;
}
try {
await this.lovelace!.saveConfig(
deleteView(this.lovelace!.config, this.viewIndex!)
@ -182,6 +183,18 @@ export class HuiEditView extends LitElement {
}
}
private _deleteConfirm(): void {
if (this._cards && this._cards.length > 0) {
alert(this.hass!.localize("ui.panel.lovelace.views.existing_cards"));
return;
}
showConfirmationDialog(this, {
text: this.hass!.localize("ui.panel.lovelace.views.confirm_delete"),
confirm: () => this._delete(),
});
}
private async _resizeDialog(): Promise<void> {
await this.updateComplete;
fireEvent(this._dialog as HTMLElement, "iron-resize");
@ -216,7 +229,7 @@ export class HuiEditView extends LitElement {
const viewConf: LovelaceViewConfig = {
...this._config,
badges: this._badges!.map((entityConf) => entityConf.entity),
badges: this._badges,
cards: this._cards,
};
@ -246,7 +259,7 @@ export class HuiEditView extends LitElement {
if (!this._badges || !this.hass || !ev.detail || !ev.detail.entities) {
return;
}
this._badges = ev.detail.entities;
this._badges = processEditorEntities(ev.detail.entities);
}
private _isConfigChanged(): boolean {
@ -290,9 +303,9 @@ export class HuiEditView extends LitElement {
height: 14px;
margin-right: 20px;
}
paper-icon-button.delete {
.delete {
margin-right: auto;
color: var(--secondary-text-color);
--mdc-theme-primary: var(--secondary-text-color);
}
paper-spinner {
display: none;
@ -304,8 +317,8 @@ export class HuiEditView extends LitElement {
display: none;
}
.error {
color: #ef5350;
border-bottom: 1px solid #ef5350;
color: var(--error-color);
border-bottom: 1px solid var(--error-color);
}
</style>
`,

View File

@ -4,6 +4,8 @@ import {
TemplateResult,
customElement,
property,
CSSResult,
css,
} from "lit-element";
import "@polymer/paper-input/paper-input";
@ -80,20 +82,32 @@ export class HuiViewEditor extends LitElement {
${configElementStyle}
<div class="card-config">
<paper-input
label="Title"
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
@blur=${this._handleTitleBlur}
></paper-input>
<paper-input
label="Icon"
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.icon"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
label="URL Path"
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.url"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._path}
.configValue=${"path"}
@value-changed=${this._valueChanged}
@ -108,7 +122,14 @@ export class HuiViewEditor extends LitElement {
?checked=${this._panel !== false}
.configValue=${"panel"}
@change=${this._valueChanged}
>Panel Mode?</ha-switch
>${this.hass.localize(
"ui.panel.lovelace.editor.view.panel_mode.title"
)}</ha-switch
>
<span class="panel"
>${this.hass.localize(
"ui.panel.lovelace.editor.view.panel_mode.description"
)}</span
>
</div>
`;
@ -147,6 +168,14 @@ export class HuiViewEditor extends LitElement {
const config = { ...this._config, path: slugify(ev.currentTarget.value) };
fireEvent(this, "view-config-changed", { config });
}
static get styles(): CSSResult {
return css`
.panel {
color: var(--secondary-text-color);
}
`;
}
}
declare global {

View File

@ -15,6 +15,7 @@ import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { LovelaceElement, IconElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { hasDoubleClick } from "../common/has-double-click";
@customElement("hui-icon-element")
export class HuiIconElement extends LitElement implements LovelaceElement {
@ -38,19 +39,26 @@ export class HuiIconElement extends LitElement implements LovelaceElement {
<ha-icon
.icon="${this._config.icon}"
.title="${computeTooltip(this.hass, this._config)}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
})}
></ha-icon>
`;
}
private _handleTap(): void {
handleClick(this, this.hass!, this._config!, false);
private _handleClick(): void {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true);
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
}
static get styles(): CSSResult {

View File

@ -15,6 +15,7 @@ import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
import { LovelaceElement, ImageElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { hasDoubleClick } from "../common/has-double-click";
@customElement("hui-image-element")
export class HuiImageElement extends LitElement implements LovelaceElement {
@ -49,9 +50,12 @@ export class HuiImageElement extends LitElement implements LovelaceElement {
.stateFilter="${this._config.state_filter}"
.title="${computeTooltip(this.hass, this._config)}"
.aspectRatio="${this._config.aspect_ratio}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
})}
></hui-image>
`;
}
@ -69,12 +73,16 @@ export class HuiImageElement extends LitElement implements LovelaceElement {
`;
}
private _handleTap(): void {
handleClick(this, this.hass!, this._config!, false);
private _handleClick(): void {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true);
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
}
}

View File

@ -14,6 +14,9 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import { LovelaceElement, StateBadgeElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { longPress } from "../common/directives/long-press-directive";
import { hasDoubleClick } from "../common/has-double-click";
import { handleClick } from "../common/handle-click";
@customElement("hui-state-badge-element")
export class HuiStateBadgeElement extends LitElement
@ -61,9 +64,27 @@ export class HuiStateBadgeElement extends LitElement
: this._config.title === null
? ""
: this._config.title}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
})}
></ha-state-label-badge>
`;
}
private _handleClick() {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold() {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
}
}
declare global {

View File

@ -18,6 +18,7 @@ import { longPress } from "../common/directives/long-press-directive";
import { LovelaceElement, StateIconElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { hasDoubleClick } from "../common/has-double-click";
@customElement("hui-state-icon-element")
export class HuiStateIconElement extends LitElement implements LovelaceElement {
@ -59,9 +60,12 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement {
<state-badge
.stateObj="${stateObj}"
.title="${computeTooltip(this.hass, this._config)}"
@ha-click="${this._handleClick}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
})}
.overrideIcon=${this._config.icon}
></state-badge>
`;
@ -76,11 +80,15 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement {
}
private _handleClick(): void {
handleClick(this, this.hass!, this._config!, false);
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true);
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
}
}

View File

@ -9,7 +9,6 @@ import {
PropertyValues,
} from "lit-element";
import "../../../components/entity/ha-state-label-badge";
import "../components/hui-warning-element";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
@ -19,6 +18,7 @@ import { longPress } from "../common/directives/long-press-directive";
import { LovelaceElement, StateLabelElementConfig } from "./types";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { hasDoubleClick } from "../common/has-double-click";
@customElement("hui-state-label-element")
class HuiStateLabelElement extends LitElement implements LovelaceElement {
@ -59,9 +59,12 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
return html`
<div
.title="${computeTooltip(this.hass, this._config)}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config!.double_tap_action),
})}
>
${this._config.prefix}${stateObj
? computeStateDisplay(
@ -74,12 +77,16 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
`;
}
private _handleTap(): void {
handleClick(this, this.hass!, this._config!, false);
private _handleClick(): void {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true);
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick() {
handleClick(this, this.hass!, this._config!, false, true);
}
static get styles(): CSSResult {

View File

@ -22,6 +22,7 @@ export interface IconElementConfig extends LovelaceElementConfig {
name?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
icon: string;
}
@ -29,6 +30,7 @@ export interface ImageElementConfig extends LovelaceElementConfig {
entity?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
image?: string;
state_image?: string;
camera_image?: string;
@ -46,12 +48,16 @@ export interface ServiceButtonElementConfig extends LovelaceElementConfig {
export interface StateBadgeElementConfig extends LovelaceElementConfig {
entity: string;
title?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export interface StateIconElementConfig extends LovelaceElementConfig {
entity: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
icon?: string;
}
@ -61,4 +67,5 @@ export interface StateLabelElementConfig extends LovelaceElementConfig {
suffix?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}

View File

@ -18,19 +18,26 @@ import "../components/hui-warning";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { HomeAssistant, InputSelectEntity } from "../../../types";
import { EntityRow, EntityConfig } from "./types";
import { EntityRow } from "./types";
import { setInputSelectOption } from "../../../data/input-select";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { forwardHaptic } from "../../../data/haptics";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { longPress } from "../common/directives/long-press-directive";
import { hasDoubleClick } from "../common/has-double-click";
import { handleClick } from "../common/handle-click";
import { classMap } from "lit-html/directives/class-map";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { EntitiesCardEntityConfig } from "../cards/types";
@customElement("hui-input-select-entity-row")
class HuiInputSelectEntityRow extends LitElement implements EntityRow {
@property() public hass?: HomeAssistant;
@property() private _config?: EntityConfig;
@property() private _config?: EntitiesCardEntityConfig;
public setConfig(config: EntityConfig): void {
public setConfig(config: EntitiesCardEntityConfig): void {
if (!config || !config.entity) {
throw new Error("Invalid Configuration: 'entity' required");
}
@ -63,8 +70,25 @@ class HuiInputSelectEntityRow extends LitElement implements EntityRow {
`;
}
const pointer =
(this._config.tap_action && this._config.tap_action.action !== "none") ||
(this._config.entity &&
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this._config.entity)));
return html`
<state-badge .stateObj="${stateObj}"></state-badge>
<state-badge
.stateObj=${stateObj}
class=${classMap({
pointer,
})}
@ha-click=${this._handleClick}
@ha-hold=${this._handleHold}
@ha-dblclick=${this._handleDblClick}
.longPress=${longPress({
hasDoubleClick: hasDoubleClick(this._config.double_tap_action),
})}
tabindex="0"
></state-badge>
<ha-paper-dropdown-menu
.label=${this._config.name || computeStateName(stateObj)}
.value=${stateObj.state}
@ -103,6 +127,18 @@ class HuiInputSelectEntityRow extends LitElement implements EntityRow {
)!.selected = stateObj.attributes.options.indexOf(stateObj.state);
}
private _handleClick(): void {
handleClick(this, this.hass!, this._config!, false, false);
}
private _handleHold(): void {
handleClick(this, this.hass!, this._config!, true, false);
}
private _handleDblClick(): void {
handleClick(this, this.hass!, this._config!, false, true);
}
static get styles(): CSSResult {
return css`
:host {
@ -118,6 +154,9 @@ class HuiInputSelectEntityRow extends LitElement implements EntityRow {
cursor: pointer;
min-width: 200px;
}
.pointer {
cursor: pointer;
}
`;
}

View File

@ -1,6 +1,6 @@
import { LitElement, html, TemplateResult, CSSResult, css } from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import yaml from "js-yaml";
import { safeDump, safeLoad } from "js-yaml";
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
@ -96,7 +96,7 @@ class LovelaceFullConfigEditor extends LitElement {
}
protected firstUpdated() {
this.yamlEditor.value = yaml.safeDump(this.lovelace!.config);
this.yamlEditor.value = safeDump(this.lovelace!.config);
}
static get styles(): CSSResult[] {
@ -183,7 +183,7 @@ class LovelaceFullConfigEditor extends LitElement {
let value;
try {
value = yaml.safeLoad(this.yamlEditor.value);
value = safeLoad(this.yamlEditor.value);
} catch (err) {
alert(`Unable to parse YAML: ${err}`);
this._saving = false;

View File

@ -73,14 +73,6 @@ class HUIRoot extends LitElement {
);
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.classList.toggle(
"disable-text-select",
/Chrome/.test(navigator.userAgent) && /Android/.test(navigator.userAgent)
);
}
protected render(): TemplateResult | void {
return html`
<app-route .route="${this.route}" pattern="/:view" data="${
@ -321,9 +313,6 @@ class HUIRoot extends LitElement {
:host {
--dark-color: #455a64;
--text-dark-color: #fff;
}
:host(.disable-text-select) {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;

View File

@ -1,10 +1,15 @@
import { HomeAssistant } from "../../types";
import { LovelaceCardConfig, LovelaceConfig } from "../../data/lovelace";
import {
LovelaceCardConfig,
LovelaceConfig,
LovelaceBadgeConfig,
} from "../../data/lovelace";
declare global {
// tslint:disable-next-line
interface HASSDomEvents {
"ll-rebuild": {};
"ll-badge-rebuild": {};
}
}
@ -18,6 +23,11 @@ export interface Lovelace {
saveConfig: (newConfig: LovelaceConfig) => Promise<void>;
}
export interface LovelaceBadge extends HTMLElement {
hass?: HomeAssistant;
setConfig(config: LovelaceBadgeConfig): void;
}
export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant;
isPanel?: boolean;

View File

@ -5,7 +5,7 @@ import {
UpdatingElement,
} from "lit-element";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";

View File

@ -2,27 +2,29 @@ import {
html,
LitElement,
PropertyValues,
PropertyDeclarations,
TemplateResult,
property,
} from "lit-element";
import "../../../components/entity/ha-state-label-badge";
// This one is for types
// tslint:disable-next-line
import { HaStateLabelBadge } from "../../../components/entity/ha-state-label-badge";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { LovelaceViewConfig, LovelaceCardConfig } from "../../../data/lovelace";
import {
LovelaceViewConfig,
LovelaceCardConfig,
LovelaceBadgeConfig,
} from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { classMap } from "lit-html/directives/class-map";
import { Lovelace, LovelaceCard } from "../types";
import { Lovelace, LovelaceCard, LovelaceBadge } from "../types";
import { createCardElement } from "../common/create-card-element";
import { computeCardSize } from "../common/compute-card-size";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { HuiErrorCard } from "../cards/hui-error-card";
import { computeRTL } from "../../../common/util/compute_rtl";
import { createBadgeElement } from "../common/create-badge-element";
import { processConfigEntities } from "../common/process-config-entities";
let editCodeLoaded = false;
@ -46,29 +48,12 @@ const getColumnIndex = (columnEntityCount: number[], size: number) => {
};
export class HUIView extends LitElement {
public hass?: HomeAssistant;
public lovelace?: Lovelace;
public columns?: number;
public index?: number;
private _cards: Array<LovelaceCard | HuiErrorCard>;
private _badges: Array<{ element: HaStateLabelBadge; entityId: string }>;
static get properties(): PropertyDeclarations {
return {
hass: {},
lovelace: {},
columns: { type: Number },
index: { type: Number },
_cards: {},
_badges: {},
};
}
constructor() {
super();
this._cards = [];
this._badges = [];
}
@property() public hass?: HomeAssistant;
@property() public lovelace?: Lovelace;
@property({ type: Number }) public columns?: number;
@property({ type: Number }) public index?: number;
@property() private _cards: Array<LovelaceCard | HuiErrorCard> = [];
@property() private _badges: LovelaceBadge[] = [];
// Public to make demo happy
public createCardElement(cardConfig: LovelaceCardConfig) {
@ -88,6 +73,19 @@ export class HUIView extends LitElement {
return element;
}
public createBadgeElement(badgeConfig: LovelaceBadgeConfig) {
const element = createBadgeElement(badgeConfig) as LovelaceBadge;
element.hass = this.hass;
element.addEventListener(
"ll-badge-rebuild",
() => {
this._rebuildBadge(element, badgeConfig);
},
{ once: true }
);
return element;
}
protected render(): TemplateResult | void {
return html`
${this.renderStyles()}
@ -208,9 +206,7 @@ export class HUIView extends LitElement {
this._createBadges(lovelace.config.views[this.index!]);
} else if (hassChanged) {
this._badges.forEach((badge) => {
const { element, entityId } = badge;
element.hass = hass;
element.state = hass.states[entityId];
badge.hass = hass;
});
}
@ -261,16 +257,11 @@ export class HUIView extends LitElement {
}
const elements: HUIView["_badges"] = [];
const badges = processConfigEntities(config.badges);
const badges = processConfigEntities(config.badges as any);
for (const badge of badges) {
const element = document.createElement("ha-state-label-badge");
const entityId = badge.entity;
const element = createBadgeElement(badge);
element.hass = this.hass;
element.state = this.hass!.states[entityId];
element.name = badge.name;
element.icon = badge.icon;
element.image = badge.image;
elements.push({ element, entityId });
elements.push(element);
root.appendChild(element);
}
this._badges = elements;
@ -346,6 +337,17 @@ export class HUIView extends LitElement {
curCardEl === cardElToReplace ? newCardEl : curCardEl
);
}
private _rebuildBadge(
badgeElToReplace: LovelaceBadge,
config: LovelaceBadgeConfig
): void {
const newBadgeEl = this.createBadgeElement(config);
badgeElToReplace.parentElement!.replaceChild(newBadgeEl, badgeElToReplace);
this._badges = this._cards!.map((curBadgeEl) =>
curBadgeEl === badgeElToReplace ? newBadgeEl : curBadgeEl
);
}
}
declare global {

View File

@ -24,6 +24,10 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
width: 100%;
z-index: 0;
}
.light {
color: #000000;
}
</style>
<app-toolbar>
@ -120,16 +124,18 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
el.setAttribute("icon", entity.attributes.icon);
iconHTML = el.outerHTML;
} else {
iconHTML = title;
const el = document.createElement("span");
el.innerHTML = title;
iconHTML = el.outerHTML;
}
icon = this.Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: "",
className: "light",
});
// create market with the icon
// create marker with the icon
mapItems.push(
this.Leaflet.marker(
[entity.attributes.latitude, entity.attributes.longitude],

View File

@ -5,7 +5,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-form";
import "../../components/ha-form/ha-form";
import "../../components/ha-markdown";
import "../../resources/ha-style";
@ -30,6 +30,7 @@ class HaMfaModuleSetupFlow extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
ha-markdown img:first-child:last-child,
ha-markdown svg:first-child:last-child {
background-color: white;
display: block;
margin: 0 auto;
}

View File

@ -33,7 +33,9 @@ documentContainer.innerHTML = `<custom-style>
--scrollbar-thumb-color: rgb(194, 194, 194);
--error-state-color: #db4437;
--error-color: #db4437;
--error-state-color: var(--error-color);
/* states and badges */
--state-icon-color: #44739e;
@ -53,8 +55,10 @@ documentContainer.innerHTML = `<custom-style>
--sidebar-selected-icon-color: var(--primary-color);
/* controls */
--toggle-button-color: var(--primary-color);
/* --toggle-button-unchecked-color: var(--accent-color); */
--switch-checked-color: var(--primary-color);
/* --switch-unchecked-color: var(--accent-color); */
--switch-unchecked-button-color: var(--switch-unchecked-color, var(--paper-grey-50));
--switch-unchecked-track-color: var(--switch-unchecked-color, #000000);
--slider-color: var(--primary-color);
--slider-secondary-color: var(--light-primary-color);
--slider-bar-color: var(--disabled-text-color);
@ -69,7 +73,7 @@ documentContainer.innerHTML = `<custom-style>
--label-badge-grey: var(--paper-grey-500);
/*
Paper-styles color.html depency is stripped on build.
Paper-styles color.html dependency is stripped on build.
When a default paper-style color is used, it needs to be copied
from paper-styles/color.html to here.
*/
@ -110,12 +114,6 @@ documentContainer.innerHTML = `<custom-style>
--table-row-background-color: var(--primary-background-color);
--table-row-alternative-background-color: var(--secondary-background-color);
/* set our toggle style */
--paper-toggle-button-checked-ink-color: var(--toggle-button-color);
--paper-toggle-button-checked-button-color: var(--toggle-button-color);
--paper-toggle-button-checked-bar-color: var(--toggle-button-color);
--paper-toggle-button-unchecked-button-color: var(--toggle-button-unchecked-color, var(--paper-grey-50));
--paper-toggle-button-unchecked-bar-color: var(--toggle-button-unchecked-color, #000000);
/* set our slider style */
--paper-slider-knob-color: var(--slider-color);
--paper-slider-knob-start-color: var(--slider-color);
@ -125,6 +123,9 @@ documentContainer.innerHTML = `<custom-style>
--paper-slider-container-color: var(--slider-bar-color);
--ha-paper-slider-pin-font-size: 15px;
/* set data table style */
--data-table-background-color: var(--card-background-color);
/* rgb */
--rgb-primary-color: 3, 169, 244;
--rgb-accent-color: 255, 152, 0;

View File

@ -1,14 +0,0 @@
import { html } from "lit-element";
// jQuery import should come before plugin import
import { jQuery as jQuery_ } from "./jquery";
import "round-slider";
// eslint-disable-next-line
import roundSliderCSS from "round-slider/dist/roundslider.min.css";
export const jQuery = jQuery_;
export const roundSliderStyle = html`
<style>
${roundSliderCSS}
</style>
`;

View File

@ -1,15 +0,0 @@
import { TemplateResult } from "lit-element";
interface LoadedRoundSlider {
roundSliderStyle: TemplateResult;
jQuery: any;
}
let loaded: Promise<LoadedRoundSlider>;
export const loadRoundslider = async (): Promise<LoadedRoundSlider> => {
if (!loaded) {
loaded = import(/* webpackChunkName: "jquery-roundslider" */ "./jquery.roundslider");
}
return loaded;
};

View File

@ -1,5 +0,0 @@
import jQuery_ from "jquery";
(window as any).jQuery = jQuery_;
export const jQuery = jQuery_;

1
src/state-summary/state-card-display.js Normal file → Executable file
View File

@ -39,6 +39,7 @@ class StateCardDisplay extends LocalizeMixin(PolymerElement) {
text-align: right;
max-width: 40%;
flex: 0 0 auto;
overflow-wrap: break-word;
}
:host([rtl]) .state {
margin-right: 16px;

View File

@ -1,8 +1,8 @@
import { clearState } from "../util/ha-pref-storage";
import { askWrite } from "../common/auth/token_storage";
import { subscribeUser, userCollection } from "../data/ws-user";
import { Constructor, LitElement } from "lit-element";
import { HassBaseEl } from "./hass-base-mixin";
import { Constructor } from "../types";
declare global {
// for fire event
@ -11,7 +11,7 @@ declare global {
}
}
export default (superClass: Constructor<LitElement & HassBaseEl>) =>
export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
class extends superClass {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);

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