Merge pull request #5089 from home-assistant/dev

20200306.0
This commit is contained in:
Bram Kragten 2020-03-06 14:19:28 +01:00 committed by GitHub
commit 9ad121f9e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
158 changed files with 4969 additions and 4106 deletions

1
.gitattributes vendored
View File

@ -11,3 +11,4 @@
*.mp3 binary *.mp3 binary
demo/public/api/camera_proxy_stream/* binary demo/public/api/camera_proxy_stream/* binary
demo/public/api/media_player_proxy/* binary

View File

@ -3,6 +3,7 @@ name: Report a bug with the UI, Frontend or Lovelace
about: Report an issue related to the Home Assistant frontend. about: Report an issue related to the Home Assistant frontend.
labels: bug labels: bug
--- ---
<!-- READ THIS FIRST: <!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/ - 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 - Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
@ -10,6 +11,7 @@ labels: bug
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks. - Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment. DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
--> -->
## Checklist ## Checklist
- [ ] I have updated to the latest available Home Assistant version. - [ ] I have updated to the latest available Home Assistant version.
@ -17,21 +19,22 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
- [ ] I have tried a different browser to see if it is related to my browser. - [ ] I have tried a different browser to see if it is related to my browser.
## The problem ## The problem
<!-- <!--
Describe the issue you are experiencing here to communicate to the Describe the issue you are experiencing here to communicate to the
maintainers. Tell us about the current behavior. maintainers. Tell us about the current behavior.
If possible provide a screenshot with a description. If possible provide a screenshot with a description.
--> -->
## Expected behavior ## Expected behavior
<!-- <!--
Describe what you expected to happen or it should look/behave. Describe what you expected to happen or it should look/behave.
If possible provide a screenshot with a description. If possible provide a screenshot with a description.
--> -->
## Steps to reproduce ## Steps to reproduce
<!-- <!--
Provide steps for us, that helps reproducing your issue. Provide steps for us, that helps reproducing your issue.
For example: For example:
@ -43,8 +46,8 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
6. Set the HVAC action to cool 6. Set the HVAC action to cool
--> -->
## Environment ## Environment
<!-- <!--
Provide details about the versions you are using, which helps us reproducing Provide details about the versions you are using, which helps us reproducing
and finding the issue quicker. Version information is found in the and finding the issue quicker. Version information is found in the
@ -56,11 +59,11 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
- Home Assistant release with the issue: - Home Assistant release with the issue:
- Last working Home Assistant release (if known): - Last working Home Assistant release (if known):
- UI Type (States or Lovelace):
- Browser and browser version: - Browser and browser version:
- Operating system: - Operating system:
## Problem-relevant configuration ## Problem-relevant configuration
<!-- <!--
An example configuration that caused the problem for you. Fill this out even An example configuration that caused the problem for you. Fill this out even
if it seems unimportant to you. Please be sure to remove personal information if it seems unimportant to you. Please be sure to remove personal information
@ -72,6 +75,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
``` ```
## Javascript errors shown in your browser console/inspector ## Javascript errors shown in your browser console/inspector
<!-- <!--
If you come across any javascript or other error logs, e.g., in your browser If you come across any javascript or other error logs, e.g., in your browser
console/inspector please provide them. console/inspector please provide them.
@ -82,4 +86,3 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
``` ```
## Additional information ## Additional information

View File

@ -1,7 +1,7 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace - name: Report a bug that is NOT related to the UI, Frontend or Lovelace
url: https://github.com/home-assistant/home-assistant/issues url: https://github.com/home-assistant/core/issues
about: This is the issue tracker for our frontend. Please report other issues with the backend repository. about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
- name: Report incorrect or missing information on our website - name: Report incorrect or missing information on our website
url: https://github.com/home-assistant/home-assistant.io/issues url: https://github.com/home-assistant/home-assistant.io/issues

View File

@ -1,4 +1,4 @@
# Home Assistant Polymer [![Build Status](https://travis-ci.org/home-assistant/home-assistant-polymer.svg?branch=master)](https://travis-ci.org/home-assistant/home-assistant-polymer) # Home Assistant Frontend
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend. This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
@ -19,12 +19,15 @@ This is the repository for the official [Home Assistant](https://home-assistant.
## Frontend development ## Frontend development
### Classic environment ### Classic environment
A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project. A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project.
### Docker environment ### Docker environment
It is possible to compile the project and/or run commands in the development environment having only the [Docker](https://www.docker.com) pre-installed in the system. On the root of project you can do: It is possible to compile the project and/or run commands in the development environment having only the [Docker](https://www.docker.com) pre-installed in the system. On the root of project you can do:
* `sh ./script/docker_run.sh build` Build all the project with one command
* `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the *classic environment*) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash. - `sh ./script/docker_run.sh build` Build all the project with one command
- `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the _classic environment_) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
**Note**: if you have installed `npm` in addition to the `docker`, you can use the commands `npm run docker_build` and `npm run bash` to get a full build or bash as explained above **Note**: if you have installed `npm` in addition to the `docker`, you can use the commands `npm run docker_build` and `npm run bash` to get a full build or bash as explained above

View File

@ -24,7 +24,7 @@ gulp.task(
gulp.parallel("gen-icons-app", "gen-icons-mdi"), gulp.parallel("gen-icons-app", "gen-icons-mdi"),
"gen-pages-dev", "gen-pages-dev",
"gen-index-app-dev", "gen-index-app-dev",
gulp.series("create-test-translation", "build-translations") "build-translations"
), ),
"copy-static", "copy-static",
"webpack-watch-app" "webpack-watch-app"

View File

@ -65,6 +65,12 @@ function copyMapPanel(staticDir) {
); );
} }
gulp.task("copy-translations", (done) => {
const staticDir = paths.static;
copyTranslations(staticDir);
done();
});
gulp.task("copy-static", (done) => { gulp.task("copy-static", (done) => {
const staticDir = paths.static; const staticDir = paths.static;
const staticPath = genStaticPath(paths.static); const staticPath = genStaticPath(paths.static);

View File

@ -2,6 +2,7 @@ const gulp = require("gulp");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const paths = require("../paths"); const paths = require("../paths");
const { mapFiles } = require("../util");
const ICON_PACKAGE_PATH = path.resolve( const ICON_PACKAGE_PATH = path.resolve(
__dirname, __dirname,
@ -57,20 +58,6 @@ function generateIconset(iconsetName, iconNames) {
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`; return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
} }
// Helper function to map recursively over files in a folder and it's subfolders
function mapFiles(startPath, filter, mapFunc) {
const files = fs.readdirSync(startPath);
for (let i = 0; i < files.length; i++) {
const filename = path.join(startPath, files[i]);
const stat = fs.lstatSync(filename);
if (stat.isDirectory()) {
mapFiles(filename, filter, mapFunc);
} else if (filename.indexOf(filter) >= 0) {
mapFunc(filename);
}
}
}
// Find all icons used by the project. // Find all icons used by the project.
function findIcons(searchPath, iconsetName) { function findIcons(searchPath, iconsetName) {
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g"); const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");

View File

@ -1,14 +1,18 @@
const crypto = require("crypto");
const del = require("del"); const del = require("del");
const path = require("path"); const path = require("path");
const source = require("vinyl-source-stream");
const vinylBuffer = require("vinyl-buffer");
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs"); const fs = require("fs");
const foreach = require("gulp-foreach"); const foreach = require("gulp-foreach");
const hash = require("gulp-hash");
const hashFilename = require("gulp-hash-filename");
const merge = require("gulp-merge-json"); const merge = require("gulp-merge-json");
const minify = require("gulp-jsonminify"); const minify = require("gulp-jsonminify");
const rename = require("gulp-rename"); const rename = require("gulp-rename");
const transform = require("gulp-json-transform"); const transform = require("gulp-json-transform");
const { mapFiles } = require("../util");
const env = require("../env");
const paths = require("../paths");
const inDir = "translations"; const inDir = "translations";
const workDir = "build-translations"; const workDir = "build-translations";
@ -39,8 +43,6 @@ const TRANSLATION_FRAGMENTS = [
"developer-tools", "developer-tools",
]; ];
const tasks = [];
function recursiveFlatten(prefix, data) { function recursiveFlatten(prefix, data) {
let output = {}; let output = {};
Object.keys(data).forEach(function(key) { Object.keys(data).forEach(function(key) {
@ -116,11 +118,9 @@ function lokaliseTransform(data, original, file) {
return output; return output;
} }
let taskName = "clean-translations"; gulp.task("clean-translations", function() {
gulp.task(taskName, function() { return del([workDir]);
return del([`${outDir}/**/*.json`]);
}); });
tasks.push(taskName);
gulp.task("ensure-translations-build-dir", (done) => { gulp.task("ensure-translations-build-dir", (done) => {
if (!fs.existsSync(workDir)) { if (!fs.existsSync(workDir)) {
@ -129,29 +129,23 @@ gulp.task("ensure-translations-build-dir", (done) => {
done(); done();
}); });
taskName = "create-test-metadata"; gulp.task("create-test-metadata", function(cb) {
gulp.task( fs.writeFile(
taskName, workDir + "/testMetadata.json",
gulp.series("ensure-translations-build-dir", function writeTestMetaData(cb) { JSON.stringify({
fs.writeFile( test: {
workDir + "/testMetadata.json", nativeName: "Test",
JSON.stringify({ },
test: { }),
nativeName: "Test", cb
}, );
}), });
cb
);
})
);
tasks.push(taskName);
taskName = "create-test-translation";
gulp.task( gulp.task(
taskName, "create-test-translation",
gulp.series("create-test-metadata", function() { gulp.series("create-test-metadata", function createTestTranslation() {
return gulp return gulp
.src("src/translations/en.json") .src(path.join(paths.translations_src, "en.json"))
.pipe( .pipe(
transform(function(data, file) { transform(function(data, file) {
return recursiveEmpty(data); return recursiveEmpty(data);
@ -161,7 +155,6 @@ gulp.task(
.pipe(gulp.dest(workDir)); .pipe(gulp.dest(workDir));
}) })
); );
tasks.push(taskName);
/** /**
* This task will build a master translation file, to be used as the base for * This task will build a master translation file, to be used as the base for
@ -172,235 +165,215 @@ tasks.push(taskName);
* project is buildable immediately after merging new translation keys, since * project is buildable immediately after merging new translation keys, since
* the Lokalise update to translations/en.json will not happen immediately. * the Lokalise update to translations/en.json will not happen immediately.
*/ */
taskName = "build-master-translation"; gulp.task("build-master-translation", function() {
gulp.task( return gulp
taskName, .src(path.join(paths.translations_src, "en.json"))
gulp.series("clean-translations", function() { .pipe(
return gulp transform(function(data, file) {
.src("src/translations/en.json") return lokaliseTransform(data, data, file);
.pipe( })
transform(function(data, file) { )
return lokaliseTransform(data, data, file); .pipe(rename("translationMaster.json"))
}) .pipe(gulp.dest(workDir));
) });
.pipe(rename("translationMaster.json"))
.pipe(gulp.dest(workDir));
})
);
tasks.push(taskName);
taskName = "build-merged-translations"; gulp.task("build-merged-translations", function() {
gulp.task( return gulp
taskName, .src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
gulp.series("build-master-translation", function() { .pipe(
return gulp transform(function(data, file) {
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true }) return lokaliseTransform(data, data, file);
.pipe( })
transform(function(data, file) { )
return lokaliseTransform(data, data, file); .pipe(
}) foreach(function(stream, file) {
) // For each language generate a merged json file. It begins with the master
.pipe( // translation as a failsafe for untranslated strings, and merges all parent
foreach(function(stream, file) { // tags into one file for each specific subtag
// For each language generate a merged json file. It begins with the master //
// translation as a failsafe for untranslated strings, and merges all parent // TODO: This is a naive interpretation of BCP47 that should be improved.
// tags into one file for each specific subtag // Will be OK for now as long as we don't have anything more complicated
// // than a base translation + region.
// TODO: This is a naive interpretation of BCP47 that should be improved. const tr = path.basename(file.history[0], ".json");
// Will be OK for now as long as we don't have anything more complicated const subtags = tr.split("-");
// than a base translation + region. const src = [workDir + "/translationMaster.json"];
const tr = path.basename(file.history[0], ".json"); for (let i = 1; i <= subtags.length; i++) {
const subtags = tr.split("-"); const lang = subtags.slice(0, i).join("-");
const src = [workDir + "/translationMaster.json"]; if (lang === "test") {
for (let i = 1; i <= subtags.length; i++) { src.push(workDir + "/test.json");
const lang = subtags.slice(0, i).join("-"); } else if (lang !== "en") {
if (lang === "test") { src.push(inDir + "/" + lang + ".json");
src.push(workDir + "/test.json");
} else if (lang !== "en") {
src.push(inDir + "/" + lang + ".json");
}
} }
return gulp }
.src(src, { allowEmpty: true }) return gulp
.pipe(transform((data) => emptyFilter(data))) .src(src, { allowEmpty: true })
.pipe( .pipe(transform((data) => emptyFilter(data)))
merge({ .pipe(
fileName: tr + ".json", merge({
}) fileName: tr + ".json",
) })
.pipe(gulp.dest(fullDir)); )
}) .pipe(gulp.dest(fullDir));
); })
}) );
); });
tasks.push(taskName);
var taskName;
const splitTasks = []; const splitTasks = [];
TRANSLATION_FRAGMENTS.forEach((fragment) => { TRANSLATION_FRAGMENTS.forEach((fragment) => {
taskName = "build-translation-fragment-" + fragment; taskName = "build-translation-fragment-" + fragment;
gulp.task( gulp.task(taskName, function() {
taskName, // Return only the translations for this fragment.
gulp.series("build-merged-translations", function() { return gulp
// Return only the translations for this fragment. .src(fullDir + "/*.json")
return gulp .pipe(
.src(fullDir + "/*.json") transform((data) => ({
.pipe( ui: {
transform((data) => ({ panel: {
ui: { [fragment]: data.ui.panel[fragment],
panel: {
[fragment]: data.ui.panel[fragment],
},
}, },
})) },
) }))
.pipe(gulp.dest(workDir + "/" + fragment)); )
}) .pipe(gulp.dest(workDir + "/" + fragment));
); });
tasks.push(taskName);
splitTasks.push(taskName); splitTasks.push(taskName);
}); });
taskName = "build-translation-core"; taskName = "build-translation-core";
gulp.task( gulp.task(taskName, function() {
taskName, // Remove the fragment translations from the core translation.
gulp.series("build-merged-translations", function() { return gulp
// Remove the fragment translations from the core translation. .src(fullDir + "/*.json")
return gulp .pipe(
.src(fullDir + "/*.json") transform((data) => {
.pipe( TRANSLATION_FRAGMENTS.forEach((fragment) => {
transform((data) => { delete data.ui.panel[fragment];
TRANSLATION_FRAGMENTS.forEach((fragment) => { });
delete data.ui.panel[fragment]; return data;
}); })
return data; )
}) .pipe(gulp.dest(coreDir));
) });
.pipe(gulp.dest(coreDir));
})
);
tasks.push(taskName);
splitTasks.push(taskName); splitTasks.push(taskName);
taskName = "build-flattened-translations"; gulp.task("build-flattened-translations", function() {
gulp.task( // Flatten the split versions of our translations, and move them into outDir
taskName, return gulp
gulp.series(...splitTasks, function() { .src(
// Flatten the split versions of our translations, and move them into outDir TRANSLATION_FRAGMENTS.map(
return gulp (fragment) => workDir + "/" + fragment + "/*.json"
.src( ).concat(coreDir + "/*.json"),
TRANSLATION_FRAGMENTS.map( { base: workDir }
(fragment) => workDir + "/" + fragment + "/*.json" )
).concat(coreDir + "/*.json"), .pipe(
{ base: workDir } transform(function(data) {
) // Polymer.AppLocalizeBehavior requires flattened json
.pipe( return flatten(data);
transform(function(data) { })
// Polymer.AppLocalizeBehavior requires flattened json )
return flatten(data); .pipe(minify())
}) .pipe(
) rename((filePath) => {
.pipe(minify()) if (filePath.dirname === "core") {
.pipe(hashFilename()) filePath.dirname = "";
.pipe( }
rename((filePath) => { })
if (filePath.dirname === "core") { )
filePath.dirname = ""; .pipe(gulp.dest(outDir));
} });
})
)
.pipe(gulp.dest(outDir));
})
);
tasks.push(taskName);
taskName = "build-translation-fingerprints"; const fingerprints = {};
gulp.task(
taskName,
gulp.series("build-flattened-translations", function() {
return gulp
.src(outDir + "/**/*.json")
.pipe(
rename({
extname: "",
})
)
.pipe(
hash({
algorithm: "md5",
hashLength: 32,
template: "<%= name %>.json",
})
)
.pipe(hash.manifest("translationFingerprints.json"))
.pipe(
transform(function(data) {
// After generating fingerprints of our translation files, consolidate
// all translation fragment fingerprints under the translation name key
const newData = {};
Object.entries(data).forEach(([key, value]) => {
const [path, _md5] = key.rsplit("-", 1);
// let translation = key;
let translation = path;
const parts = translation.split("/");
if (parts.length === 2) {
translation = parts[1];
}
if (!(translation in newData)) {
newData[translation] = {
fingerprints: {},
};
}
newData[translation].fingerprints[path] = value;
});
return newData;
})
)
.pipe(gulp.dest(workDir));
})
);
tasks.push(taskName);
taskName = "build-translations";
gulp.task( gulp.task(
taskName, "build-translation-fingerprints",
gulp.series("build-translation-fingerprints", function() { function fingerprintTranslationFiles() {
return gulp // Fingerprint full file of each language
.src( const files = fs.readdirSync(fullDir);
[ for (let i = 0; i < files.length; i++) {
"src/translations/translationMetadata.json", fingerprints[files[i].split(".")[0]] = {
workDir + "/testMetadata.json", // In dev we create fake hashes
workDir + "/translationFingerprints.json", hash: env.isProdBuild
], ? crypto
{ allowEmpty: true } .createHash("md5")
) .update(fs.readFileSync(path.join(fullDir, files[i]), "utf-8"))
.pipe(merge({})) .digest("hex")
.pipe( : "dev",
transform(function(data) { };
const newData = {}; }
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
})
);
tasks.push(taskName);
module.exports = tasks; mapFiles(outDir, ".json", (filename) => {
const parsed = path.parse(filename);
// nl.json -> nl-<hash>.json
if (!(parsed.name in fingerprints)) {
throw new Error(`Unable to find hash for ${filename}`);
}
fs.renameSync(
filename,
`${parsed.dir}/${parsed.name}-${fingerprints[parsed.name].hash}${
parsed.ext
}`
);
});
const stream = source("translationFingerprints.json");
stream.write(JSON.stringify(fingerprints));
process.nextTick(() => stream.end());
return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir));
}
);
gulp.task(
"build-translations",
gulp.series(
"clean-translations",
"ensure-translations-build-dir",
env.isProdBuild ? (done) => done() : "create-test-translation",
"build-master-translation",
"build-merged-translations",
gulp.parallel(...splitTasks),
"build-flattened-translations",
"build-translation-fingerprints",
function writeMetadata() {
return gulp
.src(
[
path.join(paths.translations_src, "translationMetadata.json"),
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform(function(data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
}
)
);

View File

@ -3,6 +3,7 @@ const gulp = require("gulp");
const webpack = require("webpack"); const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server"); const WebpackDevServer = require("webpack-dev-server");
const log = require("fancy-log"); const log = require("fancy-log");
const path = require("path");
const paths = require("../paths"); const paths = require("../paths");
const { const {
createAppConfig, createAppConfig,
@ -58,9 +59,13 @@ const handler = (done) => (err, stats) => {
gulp.task("webpack-watch-app", () => { gulp.task("webpack-watch-app", () => {
// we are not calling done, so this command will run forever // we are not calling done, so this command will run forever
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch( webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
{}, { ignored: /build-translations/ },
handler() handler()
); );
gulp.watch(
path.join(paths.translations_src, "en.json"),
gulp.series("build-translations", "copy-translations")
);
}); });
gulp.task( gulp.task(

View File

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

16
build-scripts/util.js Normal file
View File

@ -0,0 +1,16 @@
const path = require("path");
const fs = require("fs");
// Helper function to map recursively over files in a folder and it's subfolders
module.exports.mapFiles = function mapFiles(startPath, filter, mapFunc) {
const files = fs.readdirSync(startPath);
for (let i = 0; i < files.length; i++) {
const filename = path.join(startPath, files[i]);
const stat = fs.lstatSync(filename);
if (stat.isDirectory()) {
mapFiles(filename, filter, mapFunc);
} else if (filename.indexOf(filter) >= 0) {
mapFunc(filename);
}
}
};

View File

@ -148,11 +148,17 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
// Create an object mapping browser urls to their paths during build // Create an object mapping browser urls to their paths during build
const translationMetadata = require("../build-translations/translationMetadata.json"); const translationMetadata = require("../build-translations/translationMetadata.json");
const workBoxTranslationsTemplatedURLs = {}; const workBoxTranslationsTemplatedURLs = {};
const englishFP = translationMetadata.translations.en.fingerprints; const englishFilename = `en-${translationMetadata.translations.en.hash}.json`;
Object.keys(englishFP).forEach((key) => {
// core
workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFilename}`
] = `build-translations/output/${englishFilename}`;
Object.keys(translationMetadata.fragments).forEach((fragment) => {
workBoxTranslationsTemplatedURLs[ workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFP[key]}` `/static/translations/${fragment}/${englishFilename}`
] = `build-translations/output/${key}.json`; ] = `build-translations/output/${fragment}/${englishFilename}`;
}); });
config.plugins.push( config.plugins.push(

View File

@ -26,10 +26,12 @@ import { CastManager } from "../../../../src/cast/cast_manager";
import { import {
LovelaceConfig, LovelaceConfig,
getLovelaceCollection, getLovelaceCollection,
getLegacyLovelaceCollection,
} from "../../../../src/data/lovelace"; } from "../../../../src/data/lovelace";
import "./hc-layout"; import "./hc-layout";
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
import { atLeastVersion } from "../../../../src/common/config/version";
@customElement("hc-cast") @customElement("hc-cast")
class HcCast extends LitElement { class HcCast extends LitElement {
@ -133,7 +135,9 @@ class HcCast extends LitElement {
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
const llColl = getLovelaceCollection(this.connection); const llColl = atLeastVersion(this.connection.haVersion, 0, 107)
? getLovelaceCollection(this.connection)
: getLegacyLovelaceCollection(this.connection);
// We first do a single refresh because we need to check if there is LL // We first do a single refresh because we need to check if there is LL
// configuration. // configuration.
llColl.refresh().then( llColl.refresh().then(

View File

@ -16,6 +16,8 @@ import {
LovelaceConfig, LovelaceConfig,
getLovelaceCollection, getLovelaceCollection,
fetchResources, fetchResources,
LegacyLovelaceConfig,
getLegacyLovelaceCollection,
} from "../../../../src/data/lovelace"; } from "../../../../src/data/lovelace";
import "./hc-launch-screen"; import "./hc-launch-screen";
import { castContext } from "../cast_context"; import { castContext } from "../cast_context";
@ -23,6 +25,7 @@ import { CAST_NS } from "../../../../src/cast/const";
import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages"; import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources"; import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click"; import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
import { atLeastVersion } from "../../../../src/common/config/version";
let resourcesLoaded = false; let resourcesLoaded = false;
@ -168,19 +171,14 @@ export class HcMain extends HassElement {
this._error = "Cannot show Lovelace because we're not connected."; this._error = "Cannot show Lovelace because we're not connected.";
return; return;
} }
if (!resourcesLoaded) {
resourcesLoaded = true;
loadLovelaceResources(
await fetchResources(this.hass!.connection),
this.hass!.auth.data.hassUrl
);
}
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) { if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
this._urlPath = msg.urlPath; this._urlPath = msg.urlPath;
if (this._unsubLovelace) { if (this._unsubLovelace) {
this._unsubLovelace(); this._unsubLovelace();
} }
const llColl = getLovelaceCollection(this.hass!.connection, msg.urlPath); const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107)
? getLovelaceCollection(this.hass!.connection, msg.urlPath)
: getLegacyLovelaceCollection(this.hass!.connection);
// We first do a single refresh because we need to check if there is LL // We first do a single refresh because we need to check if there is LL
// configuration. // configuration.
try { try {
@ -199,6 +197,15 @@ export class HcMain extends HassElement {
); );
} }
} }
if (!resourcesLoaded) {
resourcesLoaded = true;
const resources = atLeastVersion(this.hass.connection.haVersion, 0, 107)
? await fetchResources(this.hass!.connection)
: (this._lovelaceConfig as LegacyLovelaceConfig).resources;
if (resources) {
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
}
}
this._showDemo = false; this._showDemo = false;
this._lovelacePath = msg.viewPath; this._lovelacePath = msg.viewPath;
if (castContext.getDeviceCapabilities().touch_input_supported) { if (castContext.getDeviceCapabilities().touch_input_supported) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -18,6 +18,7 @@ import {
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import { filterAndSort } from "../components/hassio-filter-addons"; import { filterAndSort } from "../components/hassio-filter-addons";
import { atLeastVersion } from "../../../src/common/config/version";
class HassioAddonRepositoryEl extends LitElement { class HassioAddonRepositoryEl extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@ -39,7 +40,6 @@ class HassioAddonRepositoryEl extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
const repo = this.repo; const repo = this.repo;
const addons = this._getAddons(this.addons, this.filter); const addons = this._getAddons(this.addons, this.filter);
const ha105pluss = this._computeHA105plus;
if (this.filter && addons.length < 1) { if (this.filter && addons.length < 1) {
return html` return html`
@ -57,7 +57,9 @@ class HassioAddonRepositoryEl extends LitElement {
</h1> </h1>
<p class="description"> <p class="description">
Maintained by ${repo.maintainer}<br /> Maintained by ${repo.maintainer}<br />
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a> <a class="repo" href=${repo.url} target="_blank" rel="noreferrer">
${repo.url}
</a>
</p> </p>
<div class="card-group"> <div class="card-group">
${addons.map( ${addons.map(
@ -90,7 +92,11 @@ class HassioAddonRepositoryEl extends LitElement {
: !addon.available : !addon.available
? "not_available" ? "not_available"
: ""} : ""}
.iconImage=${ha105pluss && addon.icon .iconImage=${atLeastVersion(
this.hass.connection.haVersion,
0,
105
) && addon.icon
? `/api/hassio/addons/${addon.slug}/icon` ? `/api/hassio/addons/${addon.slug}/icon`
: undefined} : undefined}
.showTopbar=${addon.installed || !addon.available} .showTopbar=${addon.installed || !addon.available}
@ -115,11 +121,6 @@ class HassioAddonRepositoryEl extends LitElement {
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`); navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
} }
private get _computeHA105plus(): boolean {
const [major, minor] = this.hass.config.version.split(".", 2);
return Number(major) > 0 || (major === "0" && Number(minor) >= 105);
}
static get styles(): CSSResultArray { static get styles(): CSSResultArray {
return [ return [
hassioStyle, hassioStyle,

View File

@ -36,6 +36,7 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { atLeastVersion } from "../../../src/common/config/version";
const PERMIS_DESC = { const PERMIS_DESC = {
rating: { rating: {
@ -185,14 +186,19 @@ class HassioAddonInfo extends LitElement {
<div class="description light-color"> <div class="description light-color">
${this.addon.description}.<br /> ${this.addon.description}.<br />
Visit Visit
<a href="${this.addon.url}" target="_blank"> <a href="${this.addon.url}" target="_blank" rel="noreferrer">
${this.addon.name} page</a ${this.addon.name} page</a
> >
for details. for details.
</div> </div>
${this.addon.logo ${this.addon.logo
? html` ? html`
<a href="${this.addon.url}" target="_blank" class="logo"> <a
href="${this.addon.url}"
target="_blank"
class="logo"
rel="noreferrer"
>
<img src="/api/hassio/addons/${this.addon.slug}/logo" /> <img src="/api/hassio/addons/${this.addon.slug}/logo" />
</a> </a>
` `
@ -428,6 +434,7 @@ class HassioAddonInfo extends LitElement {
tabindex="-1" tabindex="-1"
target="_blank" target="_blank"
class="right" class="right"
rel="noopener"
> >
<mwc-button> <mwc-button>
Open web UI Open web UI
@ -653,7 +660,10 @@ class HassioAddonInfo extends LitElement {
} }
private get _computeCannotIngressSidebar(): boolean { private get _computeCannotIngressSidebar(): boolean {
return !this.addon.ingress || !this._computeHA92plus; return (
!this.addon.ingress ||
!atLeastVersion(this.hass.connection.haVersion, 0, 92)
);
} }
private get _computeUsesProtectedOptions(): boolean { private get _computeUsesProtectedOptions(): boolean {
@ -662,11 +672,6 @@ class HassioAddonInfo extends LitElement {
); );
} }
private get _computeHA92plus(): boolean {
const [major, minor] = this.hass.config.version.split(".", 2);
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
}
private async _startOnBootToggled(): Promise<void> { private async _startOnBootToggled(): Promise<void> {
this._error = undefined; this._error = undefined;
const data: HassioAddonSetOptionParams = { const data: HassioAddonSetOptionParams = {

View File

@ -15,6 +15,7 @@ import { navigate } from "../../../src/common/navigate";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import "../components/hassio-card-content"; import "../components/hassio-card-content";
import { atLeastVersion } from "../../../src/common/config/version";
@customElement("hassio-addons") @customElement("hassio-addons")
class HassioAddons extends LitElement { class HassioAddons extends LitElement {
@ -22,9 +23,6 @@ class HassioAddons extends LitElement {
@property() public addons?: HassioAddonInfo[]; @property() public addons?: HassioAddonInfo[];
protected render(): TemplateResult { protected render(): TemplateResult {
const [major, minor] = this.hass.config.version.split(".", 2);
const ha105pluss =
Number(major) > 0 || (major === "0" && Number(minor) >= 105);
return html` return html`
<div class="content"> <div class="content">
<h1>Add-ons</h1> <h1>Add-ons</h1>
@ -68,7 +66,11 @@ class HassioAddons extends LitElement {
: addon.installed && addon.state === "started" : addon.installed && addon.state === "started"
? "running" ? "running"
: "stopped"} : "stopped"}
.iconImage=${ha105pluss && addon.icon .iconImage=${atLeastVersion(
this.hass.connection.haVersion,
0,
105
) && addon.icon
? `/api/hassio/addons/${addon.slug}/icon` ? `/api/hassio/addons/${addon.slug}/icon`
: undefined} : undefined}
></hassio-card-content> ></hassio-card-content>

View File

@ -123,7 +123,7 @@ export class HassioUpdate extends LitElement {
</div> </div>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a href="${releaseNotesUrl}" target="_blank"> <a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
<mwc-button>Release notes</mwc-button> <mwc-button>Release notes</mwc-button>
</a> </a>
<ha-call-api-button <ha-call-api-button

View File

@ -19,8 +19,6 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@material/chips": "^5.0.0", "@material/chips": "^5.0.0",
"@material/data-table": "^5.0.0",
"@material/mwc-base": "^0.13.0",
"@material/mwc-button": "^0.13.0", "@material/mwc-button": "^0.13.0",
"@material/mwc-checkbox": "^0.13.0", "@material/mwc-checkbox": "^0.13.0",
"@material/mwc-dialog": "^0.13.0", "@material/mwc-dialog": "^0.13.0",
@ -70,6 +68,7 @@
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0", "@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.3.7", "@thomasloven/round-slider": "0.3.7",
"@types/resize-observer-browser": "^0.1.3",
"@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7", "@vaadin/vaadin-date-picker": "^4.0.7",
"@webcomponents/shadycss": "^1.9.0", "@webcomponents/shadycss": "^1.9.0",
@ -85,7 +84,7 @@
"fuse.js": "^3.4.4", "fuse.js": "^3.4.4",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^0.12.4", "hls.js": "^0.12.4",
"home-assistant-js-websocket": "4.4.1", "home-assistant-js-websocket": "4.5.0",
"intl-messageformat": "^2.2.0", "intl-messageformat": "^2.2.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"leaflet": "^1.4.0", "leaflet": "^1.4.0",
@ -97,10 +96,12 @@
"mdn-polyfills": "^5.16.0", "mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2", "memoize-one": "^5.0.2",
"moment": "^2.24.0", "moment": "^2.24.0",
"node-vibrant": "^3.1.5",
"preact": "^8.4.2", "preact": "^8.4.2",
"preact-compat": "^3.18.4", "preact-compat": "^3.18.4",
"react-big-calendar": "^0.20.4", "react-big-calendar": "^0.20.4",
"regenerator-runtime": "^0.13.2", "regenerator-runtime": "^0.13.2",
"resize-observer": "^1.0.0",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"superstruct": "^0.6.1", "superstruct": "^0.6.1",
"tslib": "^1.10.0", "tslib": "^1.10.0",
@ -145,13 +146,11 @@
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-foreach": "^0.1.0", "gulp-foreach": "^0.1.0",
"gulp-hash": "^4.2.2",
"gulp-hash-filename": "^2.0.1",
"gulp-insert": "^0.5.0", "gulp-insert": "^0.5.0",
"gulp-json-transform": "^0.4.6", "gulp-json-transform": "^0.4.6",
"gulp-jsonminify": "^1.1.0", "gulp-jsonminify": "^1.1.0",
"gulp-merge-json": "^1.3.1", "gulp-merge-json": "^1.3.1",
"gulp-rename": "^1.4.0", "gulp-rename": "^2.0.0",
"gulp-zopfli-green": "^3.0.1", "gulp-zopfli-green": "^3.0.1",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
@ -174,6 +173,8 @@
"tslint-eslint-rules": "^5.4.0", "tslint-eslint-rules": "^5.4.0",
"tslint-plugin-prettier": "^2.0.1", "tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.7.2", "typescript": "^3.7.2",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"web-component-tester": "^6.9.2", "web-component-tester": "^6.9.2",
"webpack": "^4.40.2", "webpack": "^4.40.2",
"webpack-cli": "^3.3.9", "webpack-cli": "^3.3.9",

View File

@ -11,17 +11,13 @@
"src/panels/dev-template/ha-panel-dev-template.js", "src/panels/dev-template/ha-panel-dev-template.js",
"src/panels/history/ha-panel-history.js", "src/panels/history/ha-panel-history.js",
"src/panels/iframe/ha-panel-iframe.js", "src/panels/iframe/ha-panel-iframe.js",
"src/panels/kiosk/ha-panel-kiosk.js",
"src/panels/logbook/ha-panel-logbook.js", "src/panels/logbook/ha-panel-logbook.js",
"src/panels/map/ha-panel-map.js", "src/panels/map/ha-panel-map.js",
"src/panels/shopping-list/ha-panel-shopping-list.js", "src/panels/shopping-list/ha-panel-shopping-list.js",
"src/panels/mailbox/ha-panel-mailbox.js", "src/panels/mailbox/ha-panel-mailbox.js",
"hassio/src/entrypoint.js" "hassio/src/entrypoint.js"
], ],
"sources": [ "sources": ["src/**/*", "!src/translations/*"],
"src/**/*",
"!src/translations/*"
],
"lint": { "lint": {
"rules": ["polymer-3"], "rules": ["polymer-3"],
"ignoreWarnings": ["could-not-resolve-reference", "could-not-load"], "ignoreWarnings": ["could-not-resolve-reference", "could-not-load"],

View File

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

View File

@ -1,45 +0,0 @@
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 {
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

@ -1,127 +0,0 @@
import "@polymer/paper-styles/element-styles/paper-material-styles";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateName } from "../common/entity/compute_state_name";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
import { fetchThumbnailUrlWithCache } from "../data/camera";
const UPDATE_INTERVAL = 10000; // ms
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="paper-material-styles">
:host {
@apply --paper-material-elevation-1;
display: block;
position: relative;
font-size: 0px;
border-radius: 2px;
cursor: pointer;
min-height: 48px;
line-height: 0;
}
.camera-feed {
width: 100%;
height: auto;
border-radius: 2px;
}
.caption {
@apply --paper-font-common-nowrap;
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
background-color: rgba(0, 0, 0, 0.3);
padding: 16px;
font-size: 16px;
font-weight: 500;
line-height: 16px;
color: white;
}
</style>
<template is="dom-if" if="[[cameraFeedSrc]]">
<img
src="[[cameraFeedSrc]]"
class="camera-feed"
alt="[[_computeStateName(stateObj)]]"
on-load="_imageLoaded"
on-error="_imageError"
/>
</template>
<div class="caption">
[[_computeStateName(stateObj)]]
<template is="dom-if" if="[[!imageLoaded]]">
([[localize('ui.card.camera.not_available')]])
</template>
</div>
`;
}
static get properties() {
return {
hass: Object,
stateObj: {
type: Object,
observer: "updateCameraFeedSrc",
},
cameraFeedSrc: {
type: String,
value: "",
},
imageLoaded: {
type: Boolean,
value: true,
},
};
}
ready() {
super.ready();
this.addEventListener("click", () => this.cardTapped());
}
connectedCallback() {
super.connectedCallback();
this.timer = setInterval(() => this.updateCameraFeedSrc(), UPDATE_INTERVAL);
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timer);
}
_imageLoaded() {
this.imageLoaded = true;
}
_imageError() {
this.imageLoaded = false;
}
cardTapped() {
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
}
async updateCameraFeedSrc() {
this.cameraFeedSrc = await fetchThumbnailUrlWithCache(
this.hass,
this.stateObj.entity_id
);
}
_computeStateName(stateObj) {
return computeStateName(stateObj);
}
}
customElements.define("ha-camera-card", HaCameraCard);

View File

@ -1,81 +0,0 @@
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-camera-card";
import "./ha-entities-card";
import "./ha-history_graph-card";
import "./ha-media_player-card";
import "./ha-persistent_notification-card";
import "./ha-plant-card";
import "./ha-weather-card";
import dynamicContentUpdater from "../common/dom/dynamic_content_updater";
class HaCardChooser extends PolymerElement {
static get properties() {
return {
cardData: {
type: Object,
observer: "cardDataChanged",
},
};
}
_updateCard(newData) {
dynamicContentUpdater(
this,
"HA-" + newData.cardType.toUpperCase() + "-CARD",
newData
);
}
createObserver() {
this._updatesAllowed = false;
this.observer = new IntersectionObserver((entries) => {
if (!entries.length) return;
if (entries[0].isIntersecting) {
this.style.height = "";
if (this._detachedChild) {
this.appendChild(this._detachedChild);
this._detachedChild = null;
}
this._updateCard(this.cardData); // Don't use 'newData' as it might have changed.
this._updatesAllowed = true;
} else {
// Set the card to be 48px high. Otherwise if the card is kept as 0px height then all
// following cards would trigger the observer at once.
const offsetHeight = this.offsetHeight;
this.style.height = `${offsetHeight || 48}px`;
if (this.lastChild) {
this._detachedChild = this.lastChild;
this.removeChild(this.lastChild);
}
this._updatesAllowed = false;
}
});
this.observer.observe(this);
}
cardDataChanged(newData) {
if (!newData) return;
// ha-entities-card is exempt from observer as it doesn't load heavy resources.
// and usually doesn't load external resources (except for entity_picture).
const eligibleToObserver =
window.IntersectionObserver && newData.cardType !== "entities";
if (!eligibleToObserver) {
if (this.observer) {
this.observer.unobserve(this);
this.observer = null;
}
this.style.height = "";
this._updateCard(newData);
return;
}
if (!this.observer) {
this.createObserver();
}
if (this._updatesAllowed) {
this._updateCard(newData);
}
}
}
customElements.define("ha-card-chooser", HaCardChooser);

View File

@ -1,182 +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/entity/ha-entity-toggle";
import "../components/ha-card";
import "../state-summary/state-card-content";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { stateMoreInfoType } from "../common/entity/state_more_info_type";
import { canToggleState } from "../common/entity/can_toggle_state";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
class HaEntitiesCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex"></style>
<style>
ha-card {
padding: 16px;
}
.states {
margin: -4px 0;
}
.state {
padding: 4px 0;
}
.header {
@apply --paper-font-headline;
/* overwriting line-height +8 because entity-toggle can be 40px height,
compensating this with reduced padding */
line-height: 40px;
color: var(--primary-text-color);
padding: 4px 0 12px;
}
.header .name {
@apply --paper-font-common-nowrap;
}
ha-entity-toggle {
margin-left: 16px;
}
.more-info {
cursor: pointer;
}
</style>
<ha-card>
<template is="dom-if" if="[[title]]">
<div
class$="[[computeTitleClass(groupEntity)]]"
on-click="entityTapped"
>
<div class="flex name">[[title]]</div>
<template is="dom-if" if="[[showGroupToggle(groupEntity, states)]]">
<ha-entity-toggle
hass="[[hass]]"
state-obj="[[groupEntity]]"
></ha-entity-toggle>
</template>
</div>
</template>
<div class="states">
<template
is="dom-repeat"
items="[[states]]"
on-dom-change="addTapEvents"
>
<div class$="[[computeStateClass(item)]]">
<state-card-content
hass="[[hass]]"
class="state-card"
state-obj="[[item]]"
></state-card-content>
</div>
</template>
</div>
</ha-card>
`;
}
static get properties() {
return {
hass: Object,
states: Array,
groupEntity: Object,
title: {
type: String,
computed: "computeTitle(states, groupEntity, localize)",
},
};
}
constructor() {
super();
// We need to save a single bound function reference to ensure the event listener
// can identify it properly.
this.entityTapped = this.entityTapped.bind(this);
}
computeTitle(states, groupEntity, localize) {
if (groupEntity) {
return computeStateName(groupEntity).trim();
}
const domain = computeStateDomain(states[0]);
return (
(localize && localize(`domain.${domain}`)) || domain.replace(/_/g, " ")
);
}
computeTitleClass(groupEntity) {
let classes = "header horizontal layout center ";
if (groupEntity) {
classes += "more-info";
}
return classes;
}
computeStateClass(stateObj) {
return stateMoreInfoType(stateObj) !== "hidden"
? "state more-info"
: "state";
}
addTapEvents() {
const cards = this.root.querySelectorAll(".state");
cards.forEach((card) => {
if (card.classList.contains("more-info")) {
card.addEventListener("click", this.entityTapped);
} else {
card.removeEventListener("click", this.entityTapped);
}
});
}
entityTapped(ev) {
const item = this.root
.querySelector("dom-repeat")
.itemForElement(ev.target);
let entityId;
if (!item && !this.groupEntity) {
return;
}
ev.stopPropagation();
if (item) {
entityId = item.entity_id;
} else {
entityId = this.groupEntity.entity_id;
}
this.fire("hass-more-info", { entityId: entityId });
}
showGroupToggle(groupEntity, states) {
if (
!groupEntity ||
!states ||
groupEntity.attributes.control === "hidden" ||
(groupEntity.state !== "on" && groupEntity.state !== "off")
) {
return false;
}
// Only show if we can toggle 2+ entities in group
let canToggleCount = 0;
for (let i = 0; i < states.length; i++) {
if (!canToggleState(this.hass, states[i])) {
continue;
}
canToggleCount++;
if (canToggleCount > 1) {
break;
}
}
return canToggleCount > 1;
}
}
customElements.define("ha-entities-card", HaEntitiesCard);

View File

@ -1,409 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-progress/paper-progress";
import "@polymer/paper-styles/element-styles/paper-material-styles";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import HassMediaPlayerEntity from "../util/hass-media-player-model";
import { fetchMediaPlayerThumbnailWithCache } from "../data/media-player";
import { computeStateName } from "../common/entity/compute_state_name";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style
include="paper-material-styles iron-flex iron-flex-alignment iron-positioning"
>
:host {
@apply --paper-material-elevation-1;
display: block;
position: relative;
font-size: 0px;
border-radius: 2px;
}
.banner {
position: relative;
background-color: white;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.banner:before {
display: block;
content: "";
width: 100%;
/* removed .25% from 16:9 ratio to fix YT black bars */
padding-top: 56%;
transition: padding-top 0.8s;
}
.banner.no-cover {
background-position: center center;
background-image: url(/static/images/card_media_player_bg.png);
background-repeat: no-repeat;
background-color: var(--primary-color);
}
.banner.content-type-music:before {
padding-top: 100%;
}
.banner.content-type-game:before {
padding-top: 100%;
}
.banner.no-cover:before {
padding-top: 88px;
}
.banner > .cover {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
background-position: center center;
background-size: cover;
transition: opacity 0.8s;
opacity: 1;
}
.banner.is-off > .cover {
opacity: 0;
}
.banner > .caption {
@apply --paper-font-caption;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, var(--dark-secondary-opacity));
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
color: white;
transition: background-color 0.5s;
}
.banner.is-off > .caption {
background-color: initial;
}
.banner > .caption .title {
@apply --paper-font-common-nowrap;
font-size: 1.2em;
margin: 8px 0 4px;
}
.progress {
width: 100%;
height: var(--paper-progress-height, 4px);
margin-top: calc(-1 * var(--paper-progress-height, 4px));
--paper-progress-active-color: var(--accent-color);
--paper-progress-container-color: rgba(200, 200, 200, 0.5);
}
.controls {
position: relative;
@apply --paper-font-body1;
padding: 8px;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
background-color: var(--paper-card-background-color, white);
}
.controls paper-icon-button {
width: 44px;
height: 44px;
}
.playback-controls {
direction: ltr;
}
paper-icon-button {
opacity: var(--dark-primary-opacity);
}
paper-icon-button[disabled] {
opacity: var(--dark-disabled-opacity);
}
paper-icon-button.primary {
width: 56px !important;
height: 56px !important;
background-color: var(--primary-color);
color: white;
border-radius: 50%;
padding: 8px;
transition: background-color 0.5s;
}
paper-icon-button.primary[disabled] {
background-color: rgba(0, 0, 0, var(--dark-disabled-opacity));
}
[invisible] {
visibility: hidden !important;
}
</style>
<div
class$="[[computeBannerClasses(playerObj, _coverShowing, _coverLoadError)]]"
>
<div class="cover" id="cover"></div>
<div class="caption">
[[_computeStateName(stateObj)]]
<div class="title">[[computePrimaryText(localize, playerObj)]]</div>
[[playerObj.secondaryTitle]]<br />
</div>
</div>
<paper-progress
max="[[stateObj.attributes.media_duration]]"
value="[[playbackPosition]]"
hidden$="[[computeHideProgress(playerObj)]]"
class="progress"
></paper-progress>
<div class="controls layout horizontal justified">
<paper-icon-button
aria-label="Turn off"
icon="hass:power"
on-click="handleTogglePower"
invisible$="[[computeHidePowerButton(playerObj)]]"
class="self-center secondary"
></paper-icon-button>
<div class="playback-controls">
<paper-icon-button
aria-label="Previous track"
icon="hass:skip-previous"
invisible$="[[!playerObj.supportsPreviousTrack]]"
disabled="[[playerObj.isOff]]"
on-click="handlePrevious"
></paper-icon-button>
<paper-icon-button
aria-label="Play or Pause"
class="primary"
icon="[[computePlaybackControlIcon(playerObj)]]"
invisible$="[[!computePlaybackControlIcon(playerObj)]]"
disabled="[[playerObj.isOff]]"
on-click="handlePlaybackControl"
></paper-icon-button>
<paper-icon-button
aria-label="Next track"
icon="hass:skip-next"
invisible$="[[!playerObj.supportsNextTrack]]"
disabled="[[playerObj.isOff]]"
on-click="handleNext"
></paper-icon-button>
</div>
<paper-icon-button
aria-label="More information."
icon="hass:dots-vertical"
on-click="handleOpenMoreInfo"
class="self-center secondary"
></paper-icon-button>
</div>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
playerObj: {
type: Object,
computed: "computePlayerObj(hass, stateObj)",
observer: "playerObjChanged",
},
playbackControlIcon: {
type: String,
computed: "computePlaybackControlIcon(playerObj)",
},
playbackPosition: Number,
_coverShowing: {
type: Boolean,
value: false,
},
_coverLoadError: {
type: Boolean,
value: false,
},
};
}
async playerObjChanged(playerObj, oldPlayerObj) {
if (playerObj.isPlaying && playerObj.showProgress) {
if (!this._positionTracking) {
this._positionTracking = setInterval(
() => this.updatePlaybackPosition(),
1000
);
}
} else if (this._positionTracking) {
clearInterval(this._positionTracking);
this._positionTracking = null;
}
if (playerObj.showProgress) {
this.updatePlaybackPosition();
}
const picture = playerObj.stateObj.attributes.entity_picture;
const oldPicture =
oldPlayerObj && oldPlayerObj.stateObj.attributes.entity_picture;
if (picture !== oldPicture && !picture) {
this.$.cover.style.backgroundImage = "";
return;
}
if (picture === oldPicture) {
return;
}
// We have a new picture url
// If entity picture is non-relative, we use that url directly.
if (picture.substr(0, 1) !== "/") {
this._coverShowing = true;
this._coverLoadError = false;
this.$.cover.style.backgroundImage = `url(${picture})`;
return;
}
try {
const {
content_type: contentType,
content,
} = await fetchMediaPlayerThumbnailWithCache(
this.hass,
playerObj.stateObj.entity_id
);
this._coverShowing = true;
this._coverLoadError = false;
this.$.cover.style.backgroundImage = `url(data:${contentType};base64,${content})`;
} catch (err) {
this._coverShowing = false;
this._coverLoadError = true;
this.$.cover.style.backgroundImage = "";
}
}
updatePlaybackPosition() {
this.playbackPosition = this.playerObj.currentProgress;
}
computeBannerClasses(playerObj, coverShowing, coverLoadError) {
var cls = "banner";
if (!playerObj) {
return cls;
}
if (playerObj.isOff || playerObj.isIdle) {
cls += " is-off no-cover";
} else if (
!playerObj.stateObj.attributes.entity_picture ||
coverLoadError ||
!coverShowing
) {
cls += " no-cover";
} else if (playerObj.stateObj.attributes.media_content_type === "music") {
cls += " content-type-music";
} else if (playerObj.stateObj.attributes.media_content_type === "game") {
cls += " content-type-game";
}
return cls;
}
computeHideProgress(playerObj) {
return !playerObj.showProgress;
}
computeHidePowerButton(playerObj) {
return playerObj.isOff
? !playerObj.supportsTurnOn
: !playerObj.supportsTurnOff;
}
computePlayerObj(hass, stateObj) {
return new HassMediaPlayerEntity(hass, stateObj);
}
computePrimaryText(localize, playerObj) {
return (
playerObj.primaryTitle ||
localize(`state.media_player.${playerObj.stateObj.state}`) ||
localize(`state.default.${playerObj.stateObj.state}`) ||
playerObj.stateObj.state
);
}
computePlaybackControlIcon(playerObj) {
if (playerObj.isPlaying) {
return playerObj.supportsPause ? "hass:pause" : "hass:stop";
}
if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
if (
playerObj.hasMediaControl &&
playerObj.supportsPause &&
!playerObj.isPaused
) {
return "hass:play-pause";
}
return playerObj.supportsPlay ? "hass:play" : null;
}
return "";
}
_computeStateName(stateObj) {
return computeStateName(stateObj);
}
handleNext(ev) {
ev.stopPropagation();
this.playerObj.nextTrack();
}
handleOpenMoreInfo(ev) {
ev.stopPropagation();
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
}
handlePlaybackControl(ev) {
ev.stopPropagation();
this.playerObj.mediaPlayPause();
}
handlePrevious(ev) {
ev.stopPropagation();
this.playerObj.previousTrack();
}
handleTogglePower(ev) {
ev.stopPropagation();
this.playerObj.togglePower();
}
}
customElements.define("ha-media_player-card", HaMediaPlayerCard);

View File

@ -1,76 +0,0 @@
import "@material/mwc-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/ha-card";
import "../components/ha-markdown";
import { computeStateName } from "../common/entity/compute_state_name";
import LocalizeMixin from "../mixins/localize-mixin";
import { computeObjectId } from "../common/entity/compute_object_id";
/*
* @appliesMixin LocalizeMixin
*/
class HaPersistentNotificationCard extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style>
:host {
@apply --paper-font-body1;
}
ha-markdown {
display: block;
padding: 0 16px;
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
ha-markdown p:first-child {
margin-top: 0;
}
ha-markdown p:last-child {
margin-bottom: 0;
}
ha-markdown a {
color: var(--primary-color);
}
ha-markdown img {
max-width: 100%;
}
mwc-button {
margin: 8px;
}
</style>
<ha-card header="[[computeTitle(stateObj)]]">
<ha-markdown content="[[stateObj.attributes.message]]"></ha-markdown>
<mwc-button on-click="dismissTap"
>[[localize('ui.card.persistent_notification.dismiss')]]</mwc-button
>
</ha-card>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
};
}
computeTitle(stateObj) {
return stateObj.attributes.title || computeStateName(stateObj);
}
dismissTap(ev) {
ev.preventDefault();
this.hass.callService("persistent_notification", "dismiss", {
notification_id: computeObjectId(this.stateObj.entity_id),
});
}
}
customElements.define(
"ha-persistent_notification-card",
HaPersistentNotificationCard
);

View File

@ -1,165 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/ha-card";
import "../components/ha-icon";
import { computeStateName } from "../common/entity/compute_state_name";
import { EventsMixin } from "../mixins/events-mixin";
class HaPlantCard extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
.banner {
display: flex;
align-items: flex-end;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
padding-top: 12px;
}
.has-plant-image .banner {
padding-top: 30%;
}
.header {
@apply --paper-font-headline;
line-height: 40px;
padding: 8px 16px;
}
.has-plant-image .header {
font-size: 16px;
font-weight: 500;
line-height: 16px;
padding: 16px;
color: white;
width: 100%;
background: rgba(0, 0, 0, var(--dark-secondary-opacity));
}
.content {
display: flex;
justify-content: space-between;
padding: 16px 32px 24px 32px;
}
.has-plant-image .content {
padding-bottom: 16px;
}
ha-icon {
color: var(--paper-item-icon-color);
margin-bottom: 8px;
}
.attributes {
cursor: pointer;
}
.attributes div {
text-align: center;
}
.problem {
color: var(--google-red-500);
font-weight: bold;
}
.uom {
color: var(--secondary-text-color);
}
</style>
<ha-card
class$="[[computeImageClass(stateObj.attributes.entity_picture)]]"
>
<div
class="banner"
style="background-image:url([[stateObj.attributes.entity_picture]])"
>
<div class="header">[[computeTitle(stateObj)]]</div>
</div>
<div class="content">
<template
is="dom-repeat"
items="[[computeAttributes(stateObj.attributes)]]"
>
<div class="attributes" on-click="attributeClicked">
<div>
<ha-icon
icon="[[computeIcon(item, stateObj.attributes.battery)]]"
></ha-icon>
</div>
<div
class$="[[computeAttributeClass(stateObj.attributes.problem, item)]]"
>
[[computeValue(stateObj.attributes, item)]]
</div>
<div class="uom">
[[computeUom(stateObj.attributes.unit_of_measurement_dict,
item)]]
</div>
</div>
</template>
</div>
</ha-card>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
config: Object,
};
}
constructor() {
super();
this.sensors = {
moisture: "hass:water",
temperature: "hass:thermometer",
brightness: "hass:white-balance-sunny",
conductivity: "hass:emoticon-poop",
battery: "hass:battery",
};
}
computeTitle(stateObj) {
return (this.config && this.config.name) || computeStateName(stateObj);
}
computeAttributes(data) {
return Object.keys(this.sensors).filter((key) => key in data);
}
computeIcon(attr, batLvl) {
const icon = this.sensors[attr];
if (attr === "battery") {
if (batLvl <= 5) {
return `${icon}-alert`;
}
if (batLvl < 95) {
return `${icon}-${Math.round(batLvl / 10 - 0.01) * 10}`;
}
}
return icon;
}
computeValue(attributes, attr) {
return attributes[attr];
}
computeUom(dict, attr) {
return dict[attr] || "";
}
computeAttributeClass(problem, attr) {
return problem.indexOf(attr) === -1 ? "" : "problem";
}
computeImageClass(entityPicture) {
return entityPicture ? "has-plant-image" : "";
}
attributeClicked(ev) {
this.fire("hass-more-info", {
entityId: this.stateObj.attributes.sensors[ev.model.item],
});
}
}
customElements.define("ha-plant-card", HaPlantCard);

View File

@ -1,383 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateName } from "../common/entity/compute_state_name";
import "../components/ha-card";
import "../components/ha-icon";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
import { computeRTL } from "../common/util/compute_rtl";
/*
* @appliesMixin LocalizeMixin
*/
class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style>
:host {
cursor: pointer;
}
.content {
padding: 0 20px 20px;
}
ha-icon {
color: var(--paper-item-icon-color);
}
.header {
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
text-rendering: var(
--paper-font-common-expensive-kerning_-_text-rendering
);
opacity: var(--dark-primary-opacity);
padding: 24px 16px 16px;
display: flex;
align-items: baseline;
}
.name {
margin-left: 16px;
font-size: 16px;
color: var(--secondary-text-color);
}
:host([rtl]) .name {
margin-left: 0px;
margin-right: 16px;
}
.now {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.main {
display: flex;
align-items: center;
margin-right: 32px;
}
:host([rtl]) .main {
margin-right: 0px;
}
.main ha-icon {
--iron-icon-height: 72px;
--iron-icon-width: 72px;
margin-right: 8px;
}
:host([rtl]) .main ha-icon {
margin-right: 0px;
}
.main .temp {
font-size: 52px;
line-height: 1em;
position: relative;
}
:host([rtl]) .main .temp {
direction: ltr;
margin-right: 28px;
}
.main .temp span {
font-size: 24px;
line-height: 1em;
position: absolute;
top: 4px;
}
.measurand {
display: inline-block;
}
:host([rtl]) .measurand {
direction: ltr;
}
.forecast {
margin-top: 16px;
display: flex;
justify-content: space-between;
}
.forecast div {
flex: 0 0 auto;
text-align: center;
}
.forecast .icon {
margin: 4px 0;
text-align: center;
}
:host([rtl]) .forecast .temp {
direction: ltr;
}
.weekday {
font-weight: bold;
}
.attributes,
.templow,
.precipitation {
color: var(--secondary-text-color);
}
:host([rtl]) .precipitation {
direction: ltr;
}
</style>
<ha-card>
<div class="header">
[[computeState(stateObj.state, localize)]]
<div class="name">[[computeName(stateObj)]]</div>
</div>
<div class="content">
<div class="now">
<div class="main">
<template is="dom-if" if="[[showWeatherIcon(stateObj.state)]]">
<ha-icon icon="[[getWeatherIcon(stateObj.state)]]"></ha-icon>
</template>
<div class="temp">
[[stateObj.attributes.temperature]]<span
>[[getUnit('temperature')]]</span
>
</div>
</div>
<div class="attributes">
<template
is="dom-if"
if="[[_showValue(stateObj.attributes.pressure)]]"
>
<div>
[[localize('ui.card.weather.attributes.air_pressure')]]:
<span class="measurand">
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
</span>
</div>
</template>
<template
is="dom-if"
if="[[_showValue(stateObj.attributes.humidity)]]"
>
<div>
[[localize('ui.card.weather.attributes.humidity')]]:
<span class="measurand"
>[[stateObj.attributes.humidity]] %</span
>
</div>
</template>
<template
is="dom-if"
if="[[_showValue(stateObj.attributes.wind_speed)]]"
>
<div>
[[localize('ui.card.weather.attributes.wind_speed')]]:
<span class="measurand">
[[getWindSpeed(stateObj.attributes.wind_speed)]]
</span>
[[getWindBearing(stateObj.attributes.wind_bearing, localize)]]
</div>
</template>
</div>
</div>
<template is="dom-if" if="[[forecast]]">
<div class="forecast">
<template is="dom-repeat" items="[[forecast]]">
<div>
<div class="weekday">
[[computeDate(item.datetime)]]<br />
<template is="dom-if" if="[[!_showValue(item.templow)]]">
[[computeTime(item.datetime)]]
</template>
</div>
<template is="dom-if" if="[[_showValue(item.condition)]]">
<div class="icon">
<ha-icon
icon="[[getWeatherIcon(item.condition)]]"
></ha-icon>
</div>
</template>
<template is="dom-if" if="[[_showValue(item.temperature)]]">
<div class="temp">
[[item.temperature]] [[getUnit('temperature')]]
</div>
</template>
<template is="dom-if" if="[[_showValue(item.templow)]]">
<div class="templow">
[[item.templow]] [[getUnit('temperature')]]
</div>
</template>
<template is="dom-if" if="[[_showValue(item.precipitation)]]">
<div class="precipitation">
[[item.precipitation]] [[getUnit('precipitation')]]
</div>
</template>
</div>
</template>
</div>
</template>
</div>
</ha-card>
`;
}
static get properties() {
return {
hass: Object,
config: Object,
stateObj: Object,
forecast: {
type: Array,
computed: "computeForecast(stateObj.attributes.forecast)",
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
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",
};
}
ready() {
this.addEventListener("click", this._onClick);
super.ready();
}
_onClick() {
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
}
computeForecast(forecast) {
return forecast && forecast.slice(0, 5);
}
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] || "";
}
}
computeState(state, localize) {
return localize(`state.weather.${state}`) || state;
}
computeName(stateObj) {
return (this.config && this.config.name) || computeStateName(stateObj);
}
showWeatherIcon(condition) {
return condition in this.weatherIcons;
}
getWeatherIcon(condition) {
return this.weatherIcons[condition];
}
windBearingToText(degree) {
const degreenum = parseInt(degree);
if (isFinite(degreenum)) {
return this.cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
}
return degree;
}
getWindSpeed(speed) {
return `${speed} ${this.getUnit("length")}/h`;
}
getWindBearing(bearing, localize) {
if (bearing != null) {
const cardinalDirection = this.windBearingToText(bearing);
return `(${localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection})`;
}
return ``;
}
_showValue(item) {
return typeof item !== "undefined" && item !== null;
}
computeDate(data) {
const date = new Date(data);
return date.toLocaleDateString(this.hass.language, { weekday: "short" });
}
computeTime(data) {
const date = new Date(data);
return date.toLocaleTimeString(this.hass.language, { hour: "numeric" });
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("ha-weather-card", HaWeatherCard);

View File

@ -0,0 +1,11 @@
export const atLeastVersion = (
version: string,
major: number,
minor: number
): boolean => {
const [haMajor, haMinor] = version.split(".", 2);
return (
Number(haMajor) > major ||
(Number(haMajor) === major && Number(haMinor) >= minor)
);
};

View File

@ -0,0 +1,107 @@
// From https://github.com/epoberezkin/fast-deep-equal
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
export const deepEqual = (a: any, b: any): boolean => {
if (a === b) {
return true;
}
if (a && b && typeof a === "object" && typeof b === "object") {
if (a.constructor !== b.constructor) {
return false;
}
let i: number | [any, any];
let length: number;
if (Array.isArray(a)) {
length = a.length;
if (length !== b.length) {
return false;
}
for (i = length; i-- !== 0; ) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
return true;
}
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) {
return false;
}
for (i of a.entries()) {
if (!b.has(i[0])) {
return false;
}
}
for (i of a.entries()) {
if (!deepEqual(i[1], b.get(i[0]))) {
return false;
}
}
return true;
}
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) {
return false;
}
for (i of a.entries()) {
if (!b.has(i[0])) {
return false;
}
}
return true;
}
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
// @ts-ignore
length = a.length;
// @ts-ignore
if (length !== b.length) {
return false;
}
for (i = length; i-- !== 0; ) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
if (a.constructor === RegExp) {
return a.source === b.source && a.flags === b.flags;
}
if (a.valueOf !== Object.prototype.valueOf) {
return a.valueOf() === b.valueOf();
}
if (a.toString !== Object.prototype.toString) {
return a.toString() === b.toString();
}
let keys: string[];
keys = Object.keys(a);
length = keys.length;
if (length !== Object.keys(b).length) {
return false;
}
for (i = length; i-- !== 0; ) {
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) {
return false;
}
}
for (i = length; i-- !== 0; ) {
const key = keys[i];
if (!deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
// true if both NaN, false otherwise
return a !== a && b !== b;
};

View File

@ -1,27 +1,21 @@
import { repeat } from "lit-html/directives/repeat";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import {
MDCDataTableAdapter,
MDCDataTableFoundation,
} from "@material/data-table";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { scroll } from "lit-virtualizer";
import { import {
html, html,
query, query,
queryAll,
CSSResult, CSSResult,
css, css,
customElement, customElement,
property, property,
TemplateResult, TemplateResult,
PropertyValues, PropertyValues,
LitElement,
} from "lit-element"; } from "lit-element";
import { BaseElement } from "@material/mwc-base/base-element";
// eslint-disable-next-line import/no-webpack-loader-syntax // eslint-disable-next-line import/no-webpack-loader-syntax
// @ts-ignore // @ts-ignore
// tslint:disable-next-line: no-implicit-dependencies // tslint:disable-next-line: no-implicit-dependencies
@ -35,6 +29,8 @@ import { HaCheckbox } from "../ha-checkbox";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { nextRender } from "../../common/util/render-status"; import { nextRender } from "../../common/util/render-status";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { styleMap } from "lit-html/directives/style-map";
import { ifDefined } from "lit-html/directives/if-defined";
declare global { declare global {
// for fire event // for fire event
@ -50,8 +46,7 @@ export interface RowClickedEvent {
} }
export interface SelectionChangedEvent { export interface SelectionChangedEvent {
id: string; value: string[];
selected: boolean;
} }
export interface SortingChangedEvent { export interface SortingChangedEvent {
@ -76,6 +71,8 @@ export interface DataTableColumnData extends DataTableSortColumnData {
title: string; title: string;
type?: "numeric" | "icon"; type?: "numeric" | "icon";
template?: <T>(data: any, row: T) => TemplateResult | string; template?: <T>(data: any, row: T) => TemplateResult | string;
width?: string;
grows?: boolean;
} }
export interface DataTableRowData { export interface DataTableRowData {
@ -84,26 +81,23 @@ export interface DataTableRowData {
} }
@customElement("ha-data-table") @customElement("ha-data-table")
export class HaDataTable extends BaseElement { export class HaDataTable extends LitElement {
@property({ type: Object }) public columns: DataTableColumnContainer = {}; @property({ type: Object }) public columns: DataTableColumnContainer = {};
@property({ type: Array }) public data: DataTableRowData[] = []; @property({ type: Array }) public data: DataTableRowData[] = [];
@property({ type: Boolean }) public selectable = false; @property({ type: Boolean }) public selectable = false;
@property({ type: Boolean, attribute: "auto-height" })
public autoHeight = false;
@property({ type: String }) public id = "id"; @property({ type: String }) public id = "id";
@property({ type: String }) public filter = ""; @property({ type: String }) public filter = "";
protected mdcFoundation!: MDCDataTableFoundation;
protected readonly mdcFoundationClass = MDCDataTableFoundation;
@query(".mdc-data-table") protected mdcRoot!: HTMLElement;
@queryAll(".mdc-data-table__row") protected rowElements!: HTMLElement[];
@property({ type: Boolean }) private _filterable = false; @property({ type: Boolean }) private _filterable = false;
@property({ type: Boolean }) private _headerChecked = false;
@property({ type: Boolean }) private _headerIndeterminate = false;
@property({ type: Array }) private _checkedRows: string[] = [];
@property({ type: String }) private _filter = ""; @property({ type: String }) private _filter = "";
@property({ type: String }) private _sortColumn?: string; @property({ type: String }) private _sortColumn?: string;
@property({ type: String }) private _sortDirection: SortingDirection = null; @property({ type: String }) private _sortDirection: SortingDirection = null;
@property({ type: Array }) private _filteredData: DataTableRowData[] = []; @property({ type: Array }) private _filteredData: DataTableRowData[] = [];
@query("slot[name='header']") private _header!: HTMLSlotElement; @query("slot[name='header']") private _header!: HTMLSlotElement;
@query(".scroller") private _scroller!: HTMLDivElement; @query(".mdc-data-table__table") private _table!: HTMLDivElement;
private _checkableRowsCount?: number;
private _checkedRows: string[] = [];
private _sortColumns: { private _sortColumns: {
[key: string]: DataTableSortColumnData; [key: string]: DataTableSortColumnData;
} = {}; } = {};
@ -114,18 +108,17 @@ export class HaDataTable extends BaseElement {
(value: string) => { (value: string) => {
this._filter = value; this._filter = value;
}, },
200, 100,
false false
); );
public clearSelection(): void { public clearSelection(): void {
this._headerChecked = false; this._checkedRows = [];
this._headerIndeterminate = false; this._checkedRowsChanged();
this.mdcFoundation.handleHeaderRowCheckboxChange();
} }
protected firstUpdated() { protected firstUpdated(properties: PropertyValues) {
super.firstUpdated(); super.firstUpdated(properties);
this._worker = sortFilterWorker(); this._worker = sortFilterWorker();
} }
@ -159,6 +152,12 @@ export class HaDataTable extends BaseElement {
this._debounceSearch(this.filter); this._debounceSearch(this.filter);
} }
if (properties.has("data")) {
this._checkableRowsCount = this.data.filter(
(row) => row.selectable !== false
).length;
}
if ( if (
properties.has("data") || properties.has("data") ||
properties.has("columns") || properties.has("columns") ||
@ -173,7 +172,7 @@ export class HaDataTable extends BaseElement {
protected render() { protected render() {
return html` return html`
<div class="mdc-data-table"> <div class="mdc-data-table">
<slot name="header" @slotchange=${this._calcScrollHeight}> <slot name="header" @slotchange=${this._calcTableHeight}>
${this._filterable ${this._filterable
? html` ? html`
<div class="table-header"> <div class="table-header">
@ -184,168 +183,151 @@ export class HaDataTable extends BaseElement {
` `
: ""} : ""}
</slot> </slot>
<div class="scroller"> <div
<table class="mdc-data-table__table"> class="mdc-data-table__table ${classMap({
<thead> "auto-height": this.autoHeight,
<tr class="mdc-data-table__header-row"> })}"
${this.selectable style=${styleMap({
? html` height: this.autoHeight
<th ? `${this._filteredData.length * 53 + 57}px`
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox" : `calc(100% - ${this._header?.clientHeight}px)`,
role="columnheader" })}
scope="col" >
> <div class="mdc-data-table__header-row">
<ha-checkbox ${this.selectable
class="mdc-data-table__row-checkbox" ? html`
@change=${this._handleHeaderRowCheckboxChange} <div
.indeterminate=${this._headerIndeterminate} class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
.checked=${this._headerChecked} role="columnheader"
> scope="col"
</ha-checkbox>
</th>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__header-cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__header-cell--icon": Boolean(
column.type && column.type === "icon"
),
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
};
return html`
<th
class="mdc-data-table__header-cell ${classMap(classes)}"
role="columnheader"
scope="col"
@click=${this._handleHeaderClick}
data-column-id="${key}"
>
${column.sortable
? html`
<ha-icon
.icon=${sorted && this._sortDirection === "desc"
? "hass:arrow-down"
: "hass:arrow-up"}
></ha-icon>
`
: ""}
<span>${column.title}</span>
</th>
`;
})}
</tr>
</thead>
<tbody class="mdc-data-table__content">
${repeat(
this._filteredData!,
(row: DataTableRowData) => row[this.id],
(row: DataTableRowData) => html`
<tr
data-row-id="${row[this.id]}"
@click=${this._handleRowClick}
class="mdc-data-table__row"
.selectable=${row.selectable !== false}
> >
${this.selectable <ha-checkbox
? html` class="mdc-data-table__row-checkbox"
<td @change=${this._handleHeaderRowCheckboxClick}
class="mdc-data-table__cell mdc-data-table__cell--checkbox" .indeterminate=${this._checkedRows.length &&
> this._checkedRows.length !== this._checkableRowsCount}
<ha-checkbox .checked=${this._checkedRows.length ===
class="mdc-data-table__row-checkbox" this._checkableRowsCount}
@change=${this._handleRowCheckboxChange} >
.disabled=${row.selectable === false} </ha-checkbox>
.checked=${this._checkedRows.includes( </div>
String(row[this.id])
)}
>
</ha-checkbox>
</td>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
return html`
<td
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
})}"
>
${column.template
? column.template(row[key], row)
: row[key]}
</td>
`;
})}
</tr>
` `
)} : ""}
</tbody> ${Object.entries(this.columns).map((columnEntry) => {
</table> const [key, column] = columnEntry;
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__header-cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__header-cell--icon": Boolean(
column.type && column.type === "icon"
),
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
grows: Boolean(column.grows),
};
return html`
<div
class="mdc-data-table__header-cell ${classMap(classes)}"
style=${column.width
? styleMap({
[column.grows ? "minWidth" : "width"]: String(
column.width
),
})
: ""}
role="columnheader"
scope="col"
@click=${this._handleHeaderClick}
.columnId=${key}
>
${column.sortable
? html`
<ha-icon
.icon=${sorted && this._sortDirection === "desc"
? "hass:arrow-down"
: "hass:arrow-up"}
></ha-icon>
`
: ""}
<span>${column.title}</span>
</div>
`;
})}
</div>
<div class="mdc-data-table__content scroller">
${scroll({
items: this._filteredData,
renderItem: (row: DataTableRowData) => html`
<div
.rowId="${row[this.id]}"
@click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected": this._checkedRows.includes(
String(row[this.id])
),
})}"
aria-selected=${ifDefined(
this._checkedRows.includes(String(row[this.id]))
? true
: undefined
)}
.selectable=${row.selectable !== false}
>
${this.selectable
? html`
<div
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxClick}
.disabled=${row.selectable === false}
.checked=${this._checkedRows.includes(
String(row[this.id])
)}
>
</ha-checkbox>
</div>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
return html`
<div
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
grows: Boolean(column.grows),
})}"
style=${column.width
? styleMap({
[column.grows ? "minWidth" : "width"]: String(
column.width
),
})
: ""}
>
${column.template
? column.template(row[key], row)
: row[key]}
</div>
`;
})}
</div>
`,
})}
</div>
</div> </div>
</div> </div>
`; `;
} }
protected createAdapter(): MDCDataTableAdapter {
return {
addClassAtRowIndex: (rowIndex: number, cssClasses: string) => {
if (!(this.rowElements[rowIndex] as any).selectable) {
return;
}
this.rowElements[rowIndex].classList.add(cssClasses);
},
getRowCount: () => this.rowElements.length,
getRowElements: () => this.rowElements,
getRowIdAtIndex: (rowIndex: number) => this._getRowIdAtIndex(rowIndex),
getRowIndexByChildElement: (el: Element) =>
Array.prototype.indexOf.call(this.rowElements, el.closest("tr")),
getSelectedRowCount: () => this._checkedRows.length,
isCheckboxAtRowIndexChecked: (rowIndex: number) =>
this._checkedRows.includes(this._getRowIdAtIndex(rowIndex)),
isHeaderRowCheckboxChecked: () => this._headerChecked,
isRowsSelectable: () => this.selectable,
notifyRowSelectionChanged: () => undefined,
notifySelectedAll: () => undefined,
notifyUnselectedAll: () => undefined,
registerHeaderRowCheckbox: () => undefined,
registerRowCheckboxes: () => undefined,
removeClassAtRowIndex: (rowIndex: number, cssClasses: string) => {
this.rowElements[rowIndex].classList.remove(cssClasses);
},
setAttributeAtRowIndex: (
rowIndex: number,
attr: string,
value: string
) => {
this.rowElements[rowIndex].setAttribute(attr, value);
},
setHeaderRowCheckboxChecked: (checked: boolean) => {
this._headerChecked = checked;
},
setHeaderRowCheckboxIndeterminate: (indeterminate: boolean) => {
this._headerIndeterminate = indeterminate;
},
setRowCheckboxCheckedAtIndex: (rowIndex: number, checked: boolean) => {
if (!(this.rowElements[rowIndex] as any).selectable) {
return;
}
this._setRowChecked(this._getRowIdAtIndex(rowIndex), checked);
},
};
}
private async _filterData() { private async _filterData() {
const startTime = new Date().getTime(); const startTime = new Date().getTime();
this.curRequest++; this.curRequest++;
@ -373,14 +355,10 @@ export class HaDataTable extends BaseElement {
this._filteredData = data; this._filteredData = data;
} }
private _getRowIdAtIndex(rowIndex: number): string {
return this.rowElements[rowIndex].getAttribute("data-row-id")!;
}
private _handleHeaderClick(ev: Event) { private _handleHeaderClick(ev: Event) {
const columnId = (ev.target as HTMLElement) const columnId = ((ev.target as HTMLElement).closest(
.closest("th")! ".mdc-data-table__header-cell"
.getAttribute("data-column-id")!; ) as any).columnId;
if (!this.columns[columnId].sortable) { if (!this.columns[columnId].sortable) {
return; return;
} }
@ -400,19 +378,32 @@ export class HaDataTable extends BaseElement {
}); });
} }
private _handleHeaderRowCheckboxChange(ev: Event) { private _handleHeaderRowCheckboxClick(ev: Event) {
const checkbox = ev.target as HaCheckbox; const checkbox = ev.target as HaCheckbox;
this._headerChecked = checkbox.checked; if (checkbox.checked) {
this._headerIndeterminate = checkbox.indeterminate; this._checkedRows = this._filteredData
this.mdcFoundation.handleHeaderRowCheckboxChange(); .filter((data) => data.selectable !== false)
.map((data) => data[this.id]);
this._checkedRowsChanged();
} else {
this._checkedRows = [];
this._checkedRowsChanged();
}
} }
private _handleRowCheckboxChange(ev: Event) { private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.target as HaCheckbox; const checkbox = ev.target as HaCheckbox;
const rowId = checkbox.closest("tr")!.getAttribute("data-row-id"); const rowId = (checkbox.closest(".mdc-data-table__row") as any).rowId;
this._setRowChecked(rowId!, checkbox.checked); if (checkbox.checked) {
this.mdcFoundation.handleRowCheckboxChange(ev); if (this._checkedRows.includes(rowId)) {
return;
}
this._checkedRows = [...this._checkedRows, rowId];
} else {
this._checkedRows = this._checkedRows.filter((row) => row !== rowId);
}
this._checkedRowsChanged();
} }
private _handleRowClick(ev: Event) { private _handleRowClick(ev: Event) {
@ -420,26 +411,15 @@ export class HaDataTable extends BaseElement {
if (target.tagName === "HA-CHECKBOX") { if (target.tagName === "HA-CHECKBOX") {
return; return;
} }
const rowId = target.closest("tr")!.getAttribute("data-row-id")!; const rowId = (target.closest(".mdc-data-table__row") as any).rowId;
fireEvent(this, "row-click", { id: rowId }, { bubbles: false }); fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
} }
private _setRowChecked(rowId: string, checked: boolean) { private _checkedRowsChanged() {
if (checked) { // force scroller to update, change it's items
if (this._checkedRows.includes(rowId)) { this._filteredData = [...this._filteredData];
return;
}
this._checkedRows = [...this._checkedRows, rowId];
} else {
const index = this._checkedRows.indexOf(rowId);
if (index === -1) {
return;
}
this._checkedRows.splice(index, 1);
}
fireEvent(this, "selection-changed", { fireEvent(this, "selection-changed", {
id: rowId, value: this._checkedRows,
selected: checked,
}); });
} }
@ -447,15 +427,20 @@ export class HaDataTable extends BaseElement {
this._debounceSearch(ev.detail.value); this._debounceSearch(ev.detail.value);
} }
private async _calcScrollHeight() { private async _calcTableHeight() {
if (this.autoHeight) {
return;
}
await this.updateComplete; await this.updateComplete;
this._scroller.style.maxHeight = `calc(100% - ${this._header.clientHeight}px)`; this._table.style.height = `calc(100% - ${this._header.clientHeight}px)`;
} }
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
/* default mdc styles, colors changed, without checkbox styles */ /* default mdc styles, colors changed, without checkbox styles */
:host {
height: 100%;
}
.mdc-data-table__content { .mdc-data-table__content {
font-family: Roboto, sans-serif; font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@ -477,7 +462,7 @@ export class HaDataTable extends BaseElement {
display: inline-flex; display: inline-flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
overflow-x: auto; overflow: hidden;
} }
.mdc-data-table__row--selected { .mdc-data-table__row--selected {
@ -485,12 +470,13 @@ export class HaDataTable extends BaseElement {
} }
.mdc-data-table__row { .mdc-data-table__row {
border-top-color: rgba(var(--rgb-primary-text-color), 0.12); display: flex;
width: 100%;
height: 52px;
} }
.mdc-data-table__row { .mdc-data-table__row ~ .mdc-data-table__row {
border-top-width: 1px; border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
border-top-style: solid;
} }
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover { .mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
@ -507,16 +493,24 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__header-row { .mdc-data-table__header-row {
height: 56px; height: 56px;
display: flex;
width: 100%;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
overflow-x: auto;
} }
.mdc-data-table__row { .mdc-data-table__header-row::-webkit-scrollbar {
height: 52px; display: none;
} }
.mdc-data-table__cell, .mdc-data-table__cell,
.mdc-data-table__header-cell { .mdc-data-table__header-cell {
padding-right: 16px; padding-right: 16px;
padding-left: 16px; padding-left: 16px;
align-self: center;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
} }
.mdc-data-table__header-cell--checkbox, .mdc-data-table__header-cell--checkbox,
@ -538,10 +532,10 @@ export class HaDataTable extends BaseElement {
} }
.mdc-data-table__table { .mdc-data-table__table {
height: 100%;
width: 100%; width: 100%;
border: 0; border: 0;
white-space: nowrap; white-space: nowrap;
border-collapse: collapse;
} }
.mdc-data-table__cell { .mdc-data-table__cell {
@ -568,12 +562,20 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__cell--icon { .mdc-data-table__cell--icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
text-align: center; text-align: center;
}
.mdc-data-table__header-cell--icon,
.mdc-data-table__cell--icon {
width: 24px; width: 24px;
} }
.mdc-data-table__header-cell--icon { .mdc-data-table__header-cell.mdc-data-table__header-cell--icon {
text-align: center; text-align: center;
} }
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:hover,
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(.not-sorted) {
text-align: left;
}
.mdc-data-table__cell--icon:first-child ha-icon { .mdc-data-table__cell--icon:first-child ha-icon {
margin-left: 8px; margin-left: 8px;
@ -604,6 +606,10 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__header-cell--numeric { .mdc-data-table__header-cell--numeric {
text-align: right; text-align: right;
} }
.mdc-data-table__header-cell--numeric.sortable:hover,
.mdc-data-table__header-cell--numeric.sortable:not(.not-sorted) {
text-align: left;
}
[dir="rtl"] .mdc-data-table__header-cell--numeric, [dir="rtl"] .mdc-data-table__header-cell--numeric,
.mdc-data-table__header-cell--numeric[dir="rtl"] { .mdc-data-table__header-cell--numeric[dir="rtl"] {
/* @noflip */ /* @noflip */
@ -634,27 +640,21 @@ export class HaDataTable extends BaseElement {
cursor: pointer; cursor: pointer;
} }
.mdc-data-table__header-cell > * { .mdc-data-table__header-cell > * {
transition: left 0.2s ease 0s; transition: left 0.2s ease;
} }
.mdc-data-table__header-cell ha-icon { .mdc-data-table__header-cell ha-icon {
top: 15px; top: -3px;
position: absolute; position: absolute;
} }
.mdc-data-table__header-cell.not-sorted ha-icon { .mdc-data-table__header-cell.not-sorted ha-icon {
left: -20px; left: -20px;
} }
.mdc-data-table__header-cell:not(.not-sorted) span, .mdc-data-table__header-cell.sortable:not(.not-sorted) span,
.mdc-data-table__header-cell.not-sorted:hover span { .mdc-data-table__header-cell.sortable.not-sorted:hover span {
left: 24px; left: 24px;
} }
.mdc-data-table__header-cell.mdc-data-table__header-cell--numeric:not(.not-sorted) .mdc-data-table__header-cell.sortable:not(.not-sorted) ha-icon,
span, .mdc-data-table__header-cell.sortable:hover.not-sorted ha-icon {
.mdc-data-table__header-cell.mdc-data-table__header-cell--numeric.not-sorted:hover
span {
left: 12px;
}
.mdc-data-table__header-cell:not(.not-sorted) ha-icon,
.mdc-data-table__header-cell:hover.not-sorted ha-icon {
left: 12px; left: 12px;
} }
.table-header { .table-header {
@ -664,12 +664,25 @@ export class HaDataTable extends BaseElement {
position: relative; position: relative;
top: 2px; top: 2px;
} }
.scroller {
overflow: auto;
}
slot[name="header"] { slot[name="header"] {
display: block; display: block;
} }
.center {
text-align: center;
}
.scroller {
display: flex;
position: relative;
contain: strict;
height: calc(100% - 57px);
}
.mdc-data-table__table:not(.auto-height) .scroller {
overflow: auto;
}
.grows {
flex-grow: 1;
flex-shrink: 1;
}
`; `;
} }
} }

View File

@ -1,374 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
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 "../cards/ha-badges-card";
import "../cards/ha-card-chooser";
import "./ha-demo-badge";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { splitByGroups } from "../common/entity/split_by_groups";
import { getGroupEntities } from "../common/entity/get_group_entities";
// mapping domain to size of the card.
const DOMAINS_WITH_CARD = {
camera: 4,
history_graph: 4,
media_player: 3,
persistent_notification: 0,
plant: 3,
weather: 4,
};
// 4 types:
// badges: 0 .. 10
// before groups < 0
// groups: X
// rest: 100
const PRIORITY = {
// before groups < 0
configurator: -20,
persistent_notification: -15,
// badges have priority >= 0
updater: 0,
sun: 1,
person: 2,
device_tracker: 3,
alarm_control_panel: 4,
timer: 5,
sensor: 6,
binary_sensor: 7,
mailbox: 8,
};
const getPriority = (domain) => (domain in PRIORITY ? PRIORITY[domain] : 100);
const sortPriority = (domainA, domainB) => domainA.priority - domainB.priority;
const entitySortBy = (entityA, entityB) => {
const nameA = (
entityA.attributes.friendly_name || entityA.entity_id
).toLowerCase();
const nameB = (
entityB.attributes.friendly_name || entityB.entity_id
).toLowerCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
};
const iterateDomainSorted = (collection, func) => {
Object.keys(collection)
.map((key) => collection[key])
.sort(sortPriority)
.forEach((domain) => {
domain.states.sort(entitySortBy);
func(domain);
});
};
class HaCards extends PolymerElement {
static get template() {
return html`
<style include="iron-flex iron-flex-factors"></style>
<style>
:host {
display: block;
padding: 4px 4px 0;
transform: translateZ(0);
position: relative;
}
.badges {
font-size: 85%;
text-align: center;
padding-top: 16px;
}
.column {
max-width: 500px;
overflow-x: hidden;
}
ha-card-chooser {
display: block;
margin: 4px 4px 8px;
}
@media (max-width: 500px) {
:host {
padding-left: 0;
padding-right: 0;
}
ha-card-chooser {
margin-left: 0;
margin-right: 0;
}
}
@media (max-width: 599px) {
.column {
max-width: 600px;
}
}
</style>
<div id="main">
<template is="dom-if" if="[[cards.badges.length]]">
<div class="badges">
<template is="dom-if" if="[[cards.demo]]">
<ha-demo-badge></ha-demo-badge>
</template>
<ha-badges-card
states="[[cards.badges]]"
hass="[[hass]]"
></ha-badges-card>
</div>
</template>
<div class="horizontal layout center-justified">
<template is="dom-repeat" items="[[cards.columns]]" as="column">
<div class="column flex-1">
<template is="dom-repeat" items="[[column]]" as="card">
<ha-card-chooser card-data="[[card]]"></ha-card-chooser>
</template>
</div>
</template>
</div>
</div>
`;
}
static get properties() {
return {
hass: Object,
columns: {
type: Number,
value: 2,
},
states: Object,
viewVisible: {
type: Boolean,
value: false,
},
orderedGroupEntities: Array,
cards: Object,
};
}
static get observers() {
return ["updateCards(columns, states, viewVisible, orderedGroupEntities)"];
}
updateCards(columns, states, viewVisible, orderedGroupEntities) {
if (!viewVisible) {
if (this.$.main.parentNode) {
this.$.main._parentNode = this.$.main.parentNode;
this.$.main.parentNode.removeChild(this.$.main);
}
return;
}
if (!this.$.main.parentNode && this.$.main._parentNode) {
this.$.main._parentNode.appendChild(this.$.main);
}
this._debouncer = Debouncer.debounce(
this._debouncer,
timeOut.after(10),
() => {
// Things might have changed since it got scheduled.
if (this.viewVisible) {
this.cards = this.computeCards(columns, states, orderedGroupEntities);
}
}
);
}
emptyCards() {
return {
demo: false,
badges: [],
columns: [],
};
}
computeCards(columns, states, orderedGroupEntities) {
const hass = this.hass;
const cards = this.emptyCards();
const entityCount = [];
for (let i = 0; i < columns; i++) {
cards.columns.push([]);
entityCount.push(0);
}
// Find column with < 5 entities, else column with lowest count
function getIndex(size) {
let minIndex = 0;
for (let i = 0; i < entityCount.length; i++) {
if (entityCount[i] < 5) {
minIndex = i;
break;
}
if (entityCount[i] < entityCount[minIndex]) {
minIndex = i;
}
}
entityCount[minIndex] += size;
return minIndex;
}
function addEntitiesCard(name, entities, groupEntity) {
if (entities.length === 0) return;
const owncard = [];
const other = [];
let size = 0;
entities.forEach((entity) => {
const domain = computeStateDomain(entity);
if (
domain in DOMAINS_WITH_CARD &&
!entity.attributes.custom_ui_state_card
) {
owncard.push(entity);
size += DOMAINS_WITH_CARD[domain];
} else {
other.push(entity);
size++;
}
});
// Add 1 to the size if we're rendering entities card
size += other.length > 0;
const curIndex = getIndex(size);
if (other.length > 0) {
cards.columns[curIndex].push({
hass: hass,
cardType: "entities",
states: other,
groupEntity: groupEntity || false,
});
}
owncard.forEach((entity) => {
cards.columns[curIndex].push({
hass: hass,
cardType: computeStateDomain(entity),
stateObj: entity,
});
});
}
const splitted = splitByGroups(states);
if (orderedGroupEntities) {
splitted.groups.sort(
(gr1, gr2) =>
orderedGroupEntities[gr1.entity_id] -
orderedGroupEntities[gr2.entity_id]
);
} else {
splitted.groups.sort(
(gr1, gr2) => gr1.attributes.order - gr2.attributes.order
);
}
const badgesColl = {};
const beforeGroupColl = {};
const afterGroupedColl = {};
Object.keys(splitted.ungrouped).forEach((key) => {
const state = splitted.ungrouped[key];
const domain = computeStateDomain(state);
if (domain === "a") {
cards.demo = true;
return;
}
const priority = getPriority(domain);
let coll;
if (priority < 0) {
coll = beforeGroupColl;
} else if (priority < 10) {
coll = badgesColl;
} else {
coll = afterGroupedColl;
}
if (!(domain in coll)) {
coll[domain] = {
domain: domain,
priority: priority,
states: [],
};
}
coll[domain].states.push(state);
});
if (orderedGroupEntities) {
Object.keys(badgesColl)
.map((key) => badgesColl[key])
.forEach((domain) => {
cards.badges.push.apply(cards.badges, domain.states);
});
cards.badges.sort(
(e1, e2) =>
orderedGroupEntities[e1.entity_id] -
orderedGroupEntities[e2.entity_id]
);
} else {
iterateDomainSorted(badgesColl, (domain) => {
cards.badges.push.apply(cards.badges, domain.states);
});
}
iterateDomainSorted(beforeGroupColl, (domain) => {
addEntitiesCard(domain.domain, domain.states);
});
splitted.groups.forEach((groupState) => {
const entities = getGroupEntities(states, groupState);
addEntitiesCard(
groupState.entity_id,
Object.keys(entities).map((key) => entities[key]),
groupState
);
});
iterateDomainSorted(afterGroupedColl, (domain) => {
addEntitiesCard(domain.domain, domain.states);
});
// Remove empty columns
cards.columns = cards.columns.filter((val) => val.length > 0);
return cards;
}
}
customElements.define("ha-cards", HaCards);

View File

@ -8,7 +8,7 @@ import {
customElement, customElement,
unsafeCSS, unsafeCSS,
} from "lit-element"; } from "lit-element";
import { ripple } from "@material/mwc-ripple/ripple-directive";
// @ts-ignore // @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css"; import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@ -33,22 +33,27 @@ export class HaChips extends LitElement {
${this.items.map( ${this.items.map(
(item, idx) => (item, idx) =>
html` html`
<button <div class="mdc-chip" .index=${idx} @click=${this._handleClick}>
class="mdc-chip" <div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
.index=${idx} <span role="gridcell">
@click=${this._handleClick} <span
> role="button"
<span class="mdc-chip__text">${item}</span> tabindex="0"
</button> class="mdc-chip__primary-action"
>
<span class="mdc-chip__text">${item}</span>
</span>
</span>
</div>
` `
)} )}
</div> </div>
`; `;
} }
private _handleClick(ev) { private _handleClick(ev): void {
fireEvent(this, "chip-clicked", { fireEvent(this, "chip-clicked", {
index: ev.target.closest("button").index, index: ev.currentTarget.index,
}); });
} }

View File

@ -1,6 +1,6 @@
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { html, customElement } from "lit-element"; import { html, customElement } from "lit-element";
import { ripple } from "@material/mwc-ripple/ripple-directive.js"; import { ripple } from "@material/mwc-ripple/ripple-directive";
import "@material/mwc-fab"; import "@material/mwc-fab";
import { Constructor } from "../types"; import { Constructor } from "../types";

View File

@ -5,6 +5,8 @@ import {
property, property,
TemplateResult, TemplateResult,
query, query,
CSSResult,
css,
} from "lit-element"; } from "lit-element";
import { import {
HaFormElement, HaFormElement,
@ -19,13 +21,14 @@ import "@polymer/paper-input/paper-input";
// tslint:disable-next-line // tslint:disable-next-line
import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { PaperSliderElement } from "@polymer/paper-slider/paper-slider"; import { PaperSliderElement } from "@polymer/paper-slider/paper-slider";
import { HaCheckbox } from "../ha-checkbox";
@customElement("ha-form-integer") @customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement { export class HaFormInteger extends LitElement implements HaFormElement {
@property() public schema!: HaFormIntegerSchema; @property() public schema!: HaFormIntegerSchema;
@property() public data!: HaFormIntegerData; @property() public data?: HaFormIntegerData;
@property() public label!: string; @property() public label?: string;
@property() public suffix!: string; @property() public suffix?: string;
@query("paper-input ha-paper-slider") private _input?: HTMLElement; @query("paper-input ha-paper-slider") private _input?: HTMLElement;
public focus() { public focus() {
@ -39,20 +42,31 @@ export class HaFormInteger extends LitElement implements HaFormElement {
? html` ? html`
<div> <div>
${this.label} ${this.label}
<ha-paper-slider <div class="flex">
pin="" ${this.schema.optional && this.schema.default === undefined
.value=${this._value} ? html`
.min=${this.schema.valueMin} <ha-checkbox
.max=${this.schema.valueMax} @change=${this._handleCheckboxChange}
@value-changed=${this._valueChanged} .checked=${this.data !== undefined}
></ha-paper-slider> ></ha-checkbox>
`
: ""}
<ha-paper-slider
pin=""
.value=${this._value}
.min=${this.schema.valueMin}
.max=${this.schema.valueMax}
.disabled=${this.data === undefined}
@value-changed=${this._valueChanged}
></ha-paper-slider>
</div>
</div> </div>
` `
: html` : html`
<paper-input <paper-input
type="number" type="number"
.label=${this.label} .label=${this.label}
.value=${this.data} .value=${this._value}
.required=${this.schema.required} .required=${this.schema.required}
.autoValidate=${this.schema.required} .autoValidate=${this.schema.required}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@ -61,7 +75,14 @@ export class HaFormInteger extends LitElement implements HaFormElement {
} }
private get _value() { private get _value() {
return this.data || 0; return this.data || this.schema.default || 0;
}
private _handleCheckboxChange(ev: Event) {
const checked = (ev.target as HaCheckbox).checked;
fireEvent(this, "value-changed", {
value: checked ? this._value : undefined,
});
} }
private _valueChanged(ev: Event) { private _valueChanged(ev: Event) {
@ -75,6 +96,14 @@ export class HaFormInteger extends LitElement implements HaFormElement {
value, value,
}); });
} }
static get styles(): CSSResult {
return css`
.flex {
display: flex;
}
`;
}
} }
declare global { declare global {

View File

@ -95,7 +95,7 @@ export interface HaFormTimeData {
export interface HaFormElement extends LitElement { export interface HaFormElement extends LitElement {
schema: HaFormSchema; schema: HaFormSchema;
data: HaFormDataContainer | HaFormData; data?: HaFormDataContainer | HaFormData;
label?: string; label?: string;
suffix?: string; suffix?: string;
} }

View File

@ -53,6 +53,7 @@ class HaMarkdown extends UpdatingElement {
node.host !== document.location.host node.host !== document.location.host
) { ) {
node.target = "_blank"; node.target = "_blank";
node.rel = "noreferrer";
// protect referrer on external links and deny window.opener access for security reasons // protect referrer on external links and deny window.opener access for security reasons
// (see https://mathiasbynens.github.io/rel-noopener/) // (see https://mathiasbynens.github.io/rel-noopener/)

View File

@ -18,6 +18,11 @@ class HaPaperSlider extends PaperSliderClass {
line-height: normal; line-height: normal;
} }
.disabled.ring > .slider-knob > .slider-knob-inner {
background-color: var(--paper-slider-disabled-knob-color, var(--paper-grey-400));
border: 2px solid var(--paper-slider-disabled-knob-color, var(--paper-grey-400));
}
.pin > .slider-knob > .slider-knob-inner::before { .pin > .slider-knob > .slider-knob-inner::before {
top: unset; top: unset;
margin-left: unset; margin-left: unset;

View File

@ -32,6 +32,7 @@ import { classMap } from "lit-html/directives/class-map";
// tslint:disable-next-line: no-duplicate-imports // tslint:disable-next-line: no-duplicate-imports
import { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item"; import { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTL } from "../common/util/compute_rtl";
import { compare } from "../common/string/compare";
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"]; const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
@ -51,6 +52,9 @@ const panelSorter = (a: PanelInfo, b: PanelInfo) => {
const aLovelace = a.component_name === "lovelace"; const aLovelace = a.component_name === "lovelace";
const bLovelace = b.component_name === "lovelace"; const bLovelace = b.component_name === "lovelace";
if (aLovelace && bLovelace) {
return compare(a.title!, b.title!);
}
if (aLovelace && !bLovelace) { if (aLovelace && !bLovelace) {
return -1; return -1;
} }
@ -71,14 +75,9 @@ const panelSorter = (a: PanelInfo, b: PanelInfo) => {
return 1; return 1;
} }
// both not built in, sort by title // both not built in, sort by title
if (a.title! < b.title!) { return compare(a.title!, b.title!);
return -1;
}
if (a.title! > b.title!) {
return 1;
}
return 0;
}; };
const DEFAULT_PAGE = localStorage.defaultPage || DEFAULT_PANEL;
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => { const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
const panels = hass.panels; const panels = hass.panels;
@ -90,7 +89,7 @@ const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
const afterSpacer: PanelInfo[] = []; const afterSpacer: PanelInfo[] = [];
Object.values(panels).forEach((panel) => { Object.values(panels).forEach((panel) => {
if (!panel.title) { if (!panel.title || panel.url_path === DEFAULT_PAGE) {
return; return;
} }
(SHOW_AFTER_SPACER.includes(panel.url_path) (SHOW_AFTER_SPACER.includes(panel.url_path)
@ -114,8 +113,7 @@ class HaSidebar extends LitElement {
@property({ type: Boolean }) public alwaysExpand = false; @property({ type: Boolean }) public alwaysExpand = false;
@property({ type: Boolean, reflect: true }) public expanded = false; @property({ type: Boolean, reflect: true }) public expanded = false;
@property() public _defaultPage?: string =
localStorage.defaultPage || DEFAULT_PANEL;
@property() private _externalConfig?: ExternalConfig; @property() private _externalConfig?: ExternalConfig;
@property() private _notifications?: PersistentNotification[]; @property() private _notifications?: PersistentNotification[];
// property used only in css // property used only in css
@ -144,6 +142,9 @@ class HaSidebar extends LitElement {
} }
} }
const defaultPanel =
this.hass.panels[DEFAULT_PAGE] || this.hass.panels[DEFAULT_PANEL];
return html` return html`
<div class="menu"> <div class="menu">
${!this.narrow ${!this.narrow
@ -168,9 +169,9 @@ class HaSidebar extends LitElement {
@keydown=${this._listboxKeydown} @keydown=${this._listboxKeydown}
> >
${this._renderPanel( ${this._renderPanel(
this._defaultPage, defaultPanel.url_path,
"hass:apps", defaultPanel.icon || "hass:view-dashboard",
hass.localize("panel.states") defaultPanel.title || hass.localize("panel.states")
)} )}
${beforeSpacer.map((panel) => ${beforeSpacer.map((panel) =>
this._renderPanel( this._renderPanel(

View File

@ -41,15 +41,6 @@ export const fetchThumbnailUrl = async (
return hass.hassUrl(path.path); return hass.hassUrl(path.path);
}; };
export const fetchThumbnail = (hass: HomeAssistant, entityId: string) => {
// tslint:disable-next-line: no-console
console.warn("This method has been deprecated.");
return hass.callWS<CameraThumbnail>({
type: "camera_thumbnail",
entity_id: entityId,
});
};
export const fetchStreamUrl = async ( export const fetchStreamUrl = async (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string, entityId: string,

View File

@ -66,7 +66,15 @@ export const fetchDeviceTriggerCapabilities = (
trigger, trigger,
}); });
const whitelist = ["above", "below", "code", "for"]; const whitelist = [
"above",
"below",
"brightness",
"code",
"for",
"position",
"set_brightness",
];
export const deviceAutomationsEqual = ( export const deviceAutomationsEqual = (
a: DeviceAutomation, a: DeviceAutomation,

View File

@ -6,12 +6,20 @@ import {
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { HASSDomEvent } from "../common/dom/fire_event"; import { HASSDomEvent } from "../common/dom/fire_event";
export interface LovelacePanelConfig {
mode: "yaml" | "storage";
}
export interface LovelaceConfig { export interface LovelaceConfig {
title?: string; title?: string;
views: LovelaceViewConfig[]; views: LovelaceViewConfig[];
background?: string; background?: string;
} }
export interface LegacyLovelaceConfig extends LovelaceConfig {
resources?: LovelaceResource[];
}
export interface LovelaceResource { export interface LovelaceResource {
id: string; id: string;
type: "css" | "js" | "module" | "html"; type: "css" | "js" | "module" | "html";
@ -31,7 +39,9 @@ interface LovelaceGenericDashboard {
id: string; id: string;
url_path: string; url_path: string;
require_admin: boolean; require_admin: boolean;
sidebar?: { icon: string; title: string }; show_in_sidebar: boolean;
icon?: string;
title: string;
} }
export interface LovelaceYamlDashboard extends LovelaceGenericDashboard { export interface LovelaceYamlDashboard extends LovelaceGenericDashboard {
@ -45,7 +55,9 @@ export interface LovelaceStorageDashboard extends LovelaceGenericDashboard {
export interface LovelaceDashboardMutableParams { export interface LovelaceDashboardMutableParams {
require_admin: boolean; require_admin: boolean;
sidebar: { icon: string; title: string } | null; show_in_sidebar: boolean;
icon?: string;
title: string;
} }
export interface LovelaceDashboardCreateParams export interface LovelaceDashboardCreateParams
@ -270,6 +282,34 @@ export const getLovelaceCollection = (
) )
); );
// Legacy functions to support cast for Home Assistion < 0.107
const fetchLegacyConfig = (
conn: Connection,
force: boolean
): Promise<LovelaceConfig> =>
conn.sendMessagePromise({
type: "lovelace/config",
force,
});
const subscribeLegacyLovelaceUpdates = (
conn: Connection,
onChange: () => void
) => conn.subscribeEvents(onChange, "lovelace_updated");
export const getLegacyLovelaceCollection = (conn: Connection) =>
getCollection(
conn,
"_lovelace",
(conn2) => fetchLegacyConfig(conn2, false),
(_conn, store) =>
subscribeLegacyLovelaceUpdates(conn, () =>
fetchLegacyConfig(conn, false).then((config) =>
store.setState(config, true)
)
)
);
export interface WindowWithLovelaceProm extends Window { export interface WindowWithLovelaceProm extends Window {
llConfProm?: Promise<LovelaceConfig>; llConfProm?: Promise<LovelaceConfig>;
llResProm?: Promise<LovelaceResource[]>; llResProm?: Promise<LovelaceResource[]>;

View File

@ -1,6 +1,4 @@
import { HomeAssistant } from "../types"; import { HassEntity } from "home-assistant-js-websocket";
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
export const SUPPORT_PAUSE = 1; export const SUPPORT_PAUSE = 1;
export const SUPPORT_SEEK = 2; export const SUPPORT_SEEK = 2;
@ -17,30 +15,47 @@ export const SUPPORT_STOP = 4096;
export const SUPPORTS_PLAY = 16384; export const SUPPORTS_PLAY = 16384;
export const SUPPORT_SELECT_SOUND_MODE = 65536; export const SUPPORT_SELECT_SOUND_MODE = 65536;
export const OFF_STATES = ["off", "idle"]; export const OFF_STATES = ["off", "idle"];
export const CONTRAST_RATIO = 3.5;
export interface MediaPlayerThumbnail { export interface MediaPlayerThumbnail {
content_type: string; content_type: string;
content: string; content: string;
} }
export const fetchMediaPlayerThumbnailWithCache = ( export const getCurrentProgress = (stateObj: HassEntity): number => {
hass: HomeAssistant, let progress = stateObj.attributes.media_position;
entityId: string progress +=
) => (Date.now() -
timeCachePromiseFunc( new Date(stateObj.attributes.media_position_updated_at).getTime()) /
"_media_playerTmb", 1000.0;
9000, return progress;
fetchMediaPlayerThumbnail, };
hass,
entityId export const computeMediaDescription = (stateObj: HassEntity): string => {
); let secondaryTitle: string;
export const fetchMediaPlayerThumbnail = ( switch (stateObj.attributes.media_content_type) {
hass: HomeAssistant, case "music":
entityId: string secondaryTitle = stateObj.attributes.media_artist;
) => { break;
return hass.callWS<MediaPlayerThumbnail>({ case "playlist":
type: "media_player_thumbnail", secondaryTitle = stateObj.attributes.media_playlist;
entity_id: entityId, break;
}); case "tvshow":
secondaryTitle = stateObj.attributes.media_series_title;
if (stateObj.attributes.media_season) {
secondaryTitle += " S" + stateObj.attributes.media_season;
if (stateObj.attributes.media_episode) {
secondaryTitle += "E" + stateObj.attributes.media_episode;
}
}
break;
default:
secondaryTitle = stateObj.attributes.app_name
? stateObj.attributes.app_name
: "";
}
return secondaryTitle;
}; };

View File

@ -2,9 +2,9 @@ import { HomeAssistant } from "../types";
export interface LoggedError { export interface LoggedError {
name: string; name: string;
message: string; message: [string];
level: string; level: string;
source: string; source: [string, number];
// unix timestamp in seconds // unix timestamp in seconds
timestamp: number; timestamp: number;
exception: string; exception: string;

View File

@ -38,7 +38,7 @@ class StepFlowExternal extends LitElement {
<div class="content"> <div class="content">
${this.flowConfig.renderExternalStepDescription(this.hass, this.step)} ${this.flowConfig.renderExternalStepDescription(this.hass, this.step)}
<div class="open-button"> <div class="open-button">
<a href=${this.step.url} target="_blank"> <a href=${this.step.url} target="_blank" rel="noreferrer">
<mwc-button raised> <mwc-button raised>
${localize( ${localize(
"ui.panel.config.integrations.config_flow.external_step.open_site" "ui.panel.config.integrations.config_flow.external_step.open_site"

View File

@ -97,6 +97,7 @@ class StepFlowPickHandler extends LitElement {
)}<a )}<a
href="https://www.home-assistant.io/integrations/" href="https://www.home-assistant.io/integrations/"
target="_blank" target="_blank"
rel="noreferrer"
>${this.hass.localize( >${this.hass.localize(
"ui.panel.config.integrations.home_assistant_website" "ui.panel.config.integrations.home_assistant_website"
)}</a )}</a

View File

@ -113,6 +113,7 @@ export class HaVoiceCommandDialog extends LitElement {
class="button" class="button"
href="${this._agentInfo.onboarding.url}" href="${this._agentInfo.onboarding.url}"
target="_blank" target="_blank"
rel="noreferrer"
><mwc-button unelevated>Yes!</mwc-button></a ><mwc-button unelevated>Yes!</mwc-button></a
> >
<mwc-button outlined>No</mwc-button> <mwc-button outlined>No</mwc-button>
@ -185,6 +186,7 @@ export class HaVoiceCommandDialog extends LitElement {
href=${this._agentInfo.attribution.url} href=${this._agentInfo.attribution.url}
class="attribution" class="attribution"
target="_blank" target="_blank"
rel="noreferrer"
>${this._agentInfo.attribution.name}</a >${this._agentInfo.attribution.name}</a
> >
` `

View File

@ -15,13 +15,6 @@ export const demoPanels: Panels = {
config: null, config: null,
url_path: "dev-state", url_path: "dev-state",
}, },
states: {
component_name: "states",
icon: null,
title: null,
config: null,
url_path: "states",
},
"dev-event": { "dev-event": {
component_name: "dev-event", component_name: "dev-event",
icon: null, icon: null,
@ -43,13 +36,6 @@ export const demoPanels: Panels = {
config: null, config: null,
url_path: "profile", url_path: "profile",
}, },
kiosk: {
component_name: "kiosk",
icon: null,
title: null,
config: null,
url_path: "kiosk",
},
"dev-info": { "dev-info": {
component_name: "dev-info", component_name: "dev-info",
icon: null, icon: null,

View File

@ -135,6 +135,7 @@ export class HaTabsSubpageDataTable extends LitElement {
return css` return css`
ha-data-table { ha-data-table {
width: 100%; width: 100%;
height: 100%;
--data-table-border-width: 0; --data-table-border-width: 0;
} }
:host(:not([narrow])) ha-data-table { :host(:not([narrow])) ha-data-table {

View File

@ -23,7 +23,7 @@ import { AppDrawerLayoutElement } from "@polymer/app-layout/app-drawer-layout/ap
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer"; import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
import { toggleAttribute } from "../common/dom/toggle_attribute"; import { toggleAttribute } from "../common/dom/toggle_attribute";
const NON_SWIPABLE_PANELS = ["kiosk", "map"]; const NON_SWIPABLE_PANELS = ["map"];
declare global { declare global {
// for fire event // for fire event

View File

@ -8,8 +8,9 @@ import {
RouteOptions, RouteOptions,
} from "./hass-router-page"; } from "./hass-router-page";
import { removeInitSkeleton } from "../util/init-skeleton"; import { removeInitSkeleton } from "../util/init-skeleton";
import { deepEqual } from "../common/util/deep-equal";
const CACHE_URL_PATHS = ["lovelace", "states", "developer-tools"]; const CACHE_URL_PATHS = ["lovelace", "developer-tools"];
const COMPONENTS = { const COMPONENTS = {
calendar: () => calendar: () =>
import( import(
@ -31,10 +32,6 @@ const COMPONENTS = {
import( import(
/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace" /* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"
), ),
states: () =>
import(
/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states"
),
history: () => history: () =>
import( import(
/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history" /* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history"
@ -43,10 +40,6 @@ const COMPONENTS = {
import( import(
/* webpackChunkName: "panel-iframe" */ "../panels/iframe/ha-panel-iframe" /* webpackChunkName: "panel-iframe" */ "../panels/iframe/ha-panel-iframe"
), ),
kiosk: () =>
import(
/* webpackChunkName: "panel-kiosk" */ "../panels/kiosk/ha-panel-kiosk"
),
logbook: () => logbook: () =>
import( import(
/* webpackChunkName: "panel-logbook" */ "../panels/logbook/ha-panel-logbook" /* webpackChunkName: "panel-logbook" */ "../panels/logbook/ha-panel-logbook"
@ -88,7 +81,7 @@ const getRoutes = (panels: Panels): RouterOptions => {
@customElement("partial-panel-resolver") @customElement("partial-panel-resolver")
class PartialPanelResolver extends HassRouterPage { class PartialPanelResolver extends HassRouterPage {
@property() public hass?: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public narrow?: boolean; @property() public narrow?: boolean;
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
@ -100,11 +93,8 @@ class PartialPanelResolver extends HassRouterPage {
const oldHass = changedProps.get("hass") as this["hass"]; const oldHass = changedProps.get("hass") as this["hass"];
if ( if (this.hass.panels && (!oldHass || oldHass.panels !== this.hass.panels)) {
this.hass!.panels && this._updateRoutes(oldHass?.panels);
(!oldHass || oldHass.panels !== this.hass!.panels)
) {
this._updateRoutes();
} }
} }
@ -117,7 +107,7 @@ class PartialPanelResolver extends HassRouterPage {
} }
protected updatePageEl(el) { protected updatePageEl(el) {
const hass = this.hass!; const hass = this.hass;
if ("setProperties" in el) { if ("setProperties" in el) {
// As long as we have Polymer panels // As long as we have Polymer panels
@ -135,11 +125,20 @@ class PartialPanelResolver extends HassRouterPage {
} }
} }
private async _updateRoutes() { private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) {
this.routerOptions = getRoutes(this.hass!.panels); this.routerOptions = getRoutes(this.hass.panels);
await this.rebuild();
await this.pageRendered; if (
removeInitSkeleton(); !oldPanels ||
!deepEqual(
oldPanels[this._currentPage],
this.hass.panels[this._currentPage]
)
) {
await this.rebuild();
await this.pageRendered;
removeInitSkeleton();
}
} }
} }

View File

@ -10,6 +10,7 @@ import {
import { LitElement, customElement, property, html } from "lit-element"; import { LitElement, customElement, property, html } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import memoizeOne from "memoize-one";
@customElement("ha-automation-action-device_id") @customElement("ha-automation-action-device_id")
export class HaDeviceAction extends LitElement { export class HaDeviceAction extends LitElement {
@ -27,14 +28,20 @@ export class HaDeviceAction extends LitElement {
}; };
} }
private _extraFieldsData = memoizeOne((capabilities, action: DeviceAction) =>
capabilities && capabilities.extra_fields
? capabilities.extra_fields.map((item) => {
return { [item.name]: action[item.name] };
})
: undefined
);
protected render() { protected render() {
const deviceId = this._deviceId || this.action.device_id; const deviceId = this._deviceId || this.action.device_id;
const extraFieldsData = const extraFieldsData = this._extraFieldsData(
this._capabilities && this._capabilities.extra_fields this._capabilities,
? this._capabilities.extra_fields.map((item) => { this.action
return { [item.name]: this.action[item.name] }; );
})
: undefined;
return html` return html`
<ha-device-picker <ha-device-picker
@ -82,10 +89,8 @@ export class HaDeviceAction extends LitElement {
} }
private async _getCapabilities() { private async _getCapabilities() {
const action = this.action; this._capabilities = this.action.domain
? await fetchDeviceActionCapabilities(this.hass, this.action)
this._capabilities = action.domain
? await fetchDeviceActionCapabilities(this.hass, action)
: null; : null;
} }

View File

@ -156,6 +156,7 @@ export class HaAutomationEditor extends LitElement {
<a <a
href="https://home-assistant.io/docs/automation/trigger/" href="https://home-assistant.io/docs/automation/trigger/"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more" "ui.panel.config.automation.editor.triggers.learn_more"
@ -184,6 +185,7 @@ export class HaAutomationEditor extends LitElement {
<a <a
href="https://home-assistant.io/docs/scripts/conditions/" href="https://home-assistant.io/docs/scripts/conditions/"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more" "ui.panel.config.automation.editor.conditions.learn_more"
@ -212,6 +214,7 @@ export class HaAutomationEditor extends LitElement {
<a <a
href="https://home-assistant.io/docs/automation/action/" href="https://home-assistant.io/docs/automation/action/"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more" "ui.panel.config.automation.editor.actions.learn_more"

View File

@ -63,6 +63,7 @@ class HaAutomationPicker extends LitElement {
<a <a
href="https://home-assistant.io/docs/automation/editor/" href="https://home-assistant.io/docs/automation/editor/"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.picker.learn_more" "ui.panel.config.automation.picker.learn_more"

View File

@ -123,6 +123,7 @@ class DialogThingtalk extends LitElement {
<a <a
href="https://almond.stanford.edu/" href="https://almond.stanford.edu/"
target="_blank" target="_blank"
rel="noreferrer"
class="attribution" class="attribution"
>Powered by Almond</a >Powered by Almond</a
> >

View File

@ -95,11 +95,15 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a href="https://account.nabucasa.com" target="_blank" <a
><mwc-button href="https://account.nabucasa.com"
>[[localize('ui.panel.config.cloud.account.manage_account')]]</mwc-button target="_blank"
></a rel="noreferrer"
> >
<mwc-button
>[[localize('ui.panel.config.cloud.account.manage_account')]]</mwc-button
>
</a>
<mwc-button style="float: right" on-click="handleLogout" <mwc-button style="float: right" on-click="handleLogout"
>[[localize('ui.panel.config.cloud.account.sign_out')]]</mwc-button >[[localize('ui.panel.config.cloud.account.sign_out')]]</mwc-button
> >
@ -117,8 +121,12 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
</p> </p>
<p> <p>
[[localize('ui.panel.config.cloud.account.integrations_introduction2')]] [[localize('ui.panel.config.cloud.account.integrations_introduction2')]]
<a href="https://www.nabucasa.com" target="_blank" <a
>[[localize('ui.panel.config.cloud.account.integrations_link_all_features')]]</a href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
>
[[localize('ui.panel.config.cloud.account.integrations_link_all_features')]] </a
>. >.
</p> </p>
</div> </div>

View File

@ -49,6 +49,7 @@ export class CloudAlexaPref extends LitElement {
<a <a
href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app" href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.enable_ha_skill" "ui.panel.config.cloud.account.alexa.enable_ha_skill"
@ -59,6 +60,7 @@ export class CloudAlexaPref extends LitElement {
<a <a
href="https://www.nabucasa.com/config/amazon_alexa/" href="https://www.nabucasa.com/config/amazon_alexa/"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.config_documentation" "ui.panel.config.cloud.account.alexa.config_documentation"

View File

@ -55,6 +55,7 @@ export class CloudGooglePref extends LitElement {
<a <a
href="https://assistant.google.com/services/a/uid/00000091fd5fb875?hl=en-US" href="https://assistant.google.com/services/a/uid/00000091fd5fb875?hl=en-US"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.cloud.account.google.enable_ha_skill" "ui.panel.config.cloud.account.google.enable_ha_skill"
@ -65,6 +66,7 @@ export class CloudGooglePref extends LitElement {
<a <a
href="https://www.nabucasa.com/config/google_assistant/" href="https://www.nabucasa.com/config/google_assistant/"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.cloud.account.google.config_documentation" "ui.panel.config.cloud.account.google.config_documentation"

View File

@ -77,12 +77,21 @@ export class CloudRemotePref extends LitElement {
: this.hass!.localize( : this.hass!.localize(
"ui.panel.config.cloud.account.remote.instance_will_be_available" "ui.panel.config.cloud.account.remote.instance_will_be_available"
)} )}
<a href="https://${remote_domain}" target="_blank" class="break-word"> <a
href="https://${remote_domain}"
target="_blank"
class="break-word"
rel="noreferrer"
>
https://${remote_domain}</a https://${remote_domain}</a
>. >.
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a href="https://www.nabucasa.com/config/remote/" target="_blank"> <a
href="https://www.nabucasa.com/config/remote/"
target="_blank"
rel="noreferrer"
>
<mwc-button <mwc-button
>${this.hass!.localize( >${this.hass!.localize(
"ui.panel.config.cloud.account.remote.link_learn_how_it_works" "ui.panel.config.cloud.account.remote.link_learn_how_it_works"

View File

@ -46,7 +46,11 @@ export class CloudWebhooks extends LitElement {
${this._renderBody()} ${this._renderBody()}
<div class="footer"> <div class="footer">
<a href="https://www.nabucasa.com/config/webhooks" target="_blank"> <a
href="https://www.nabucasa.com/config/webhooks"
target="_blank"
rel="noreferrer"
>
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.link_learn_more" "ui.panel.config.cloud.account.webhooks.link_learn_more"
)} )}

View File

@ -78,7 +78,7 @@ export class DialogManageCloudhook extends LitElement {
</div> </div>
<div class="paper-dialog-buttons"> <div class="paper-dialog-buttons">
<a href="${docsUrl}" target="_blank"> <a href="${docsUrl}" target="_blank" rel="noreferrer">
<mwc-button <mwc-button
>${this.hass!.localize( >${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.view_documentation" "ui.panel.config.cloud.dialog_cloudhook.view_documentation"

View File

@ -84,18 +84,26 @@ class CloudLogin extends LocalizeMixin(
</p> </p>
<p> <p>
[[localize('ui.panel.config.cloud.login.introduction2')]] [[localize('ui.panel.config.cloud.login.introduction2')]]
<a href="https://www.nabucasa.com" target="_blank" <a
>Nabu&nbsp;Casa,&nbsp;Inc</a href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
> >
Nabu&nbsp;Casa,&nbsp;Inc
</a>
[[localize('ui.panel.config.cloud.login.introduction2a')]] [[localize('ui.panel.config.cloud.login.introduction2a')]]
</p> </p>
<p> <p>
[[localize('ui.panel.config.cloud.login.introduction3')]] [[localize('ui.panel.config.cloud.login.introduction3')]]
</p> </p>
<p> <p>
<a href="https://www.nabucasa.com" target="_blank" <a
>[[localize('ui.panel.config.cloud.login.learn_more_link')]]</a href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer"
> >
[[localize('ui.panel.config.cloud.login.learn_more_link')]]
</a>
</p> </p>
</div> </div>

View File

@ -74,8 +74,8 @@ class CloudRegister extends LocalizeMixin(EventsMixin(PolymerElement)) {
<p> <p>
[[localize('ui.panel.config.cloud.register.information4')]] [[localize('ui.panel.config.cloud.register.information4')]]
</p><ul> </p><ul>
<li><a href="https://home-assistant.io/tos/" target="_blank">[[localize('ui.panel.config.cloud.register.link_terms_conditions')]]</a></li> <li><a href="https://home-assistant.io/tos/" target="_blank" rel="noreferrer">[[localize('ui.panel.config.cloud.register.link_terms_conditions')]]</a></li>
<li><a href="https://home-assistant.io/privacy/" target="_blank">[[localize('ui.panel.config.cloud.register.link_privacy_policy')]]</a></li> <li><a href="https://home-assistant.io/privacy/" target="_blank" rel="noreferrer">[[localize('ui.panel.config.cloud.register.link_privacy_policy')]]</a></li>
</ul> </ul>
</p> </p>
</div> </div>

View File

@ -31,6 +31,7 @@ class HaFormCustomize extends LocalizeMixin(PolymerElement) {
<a <a
href="https://www.home-assistant.io/docs/configuration/customizing-devices/#customization-using-the-ui" href="https://www.home-assistant.io/docs/configuration/customizing-devices/#customization-using-the-ui"
target="_blank" target="_blank"
rel="noreferrer"
>[[localize('ui.panel.config.customize.warning.include_link')]]</a >[[localize('ui.panel.config.customize.warning.include_link')]]</a
>.<br /> >.<br />
[[localize('ui.panel.config.customize.warning.not_applied')]] [[localize('ui.panel.config.customize.warning.not_applied')]]

View File

@ -26,6 +26,7 @@ import {
RowClickedEvent, RowClickedEvent,
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("ha-config-devices-dashboard") @customElement("ha-config-devices-dashboard")
export class HaConfigDeviceDashboard extends LitElement { export class HaConfigDeviceDashboard extends LitElement {
@ -127,6 +128,7 @@ export class HaConfigDeviceDashboard extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
template: (name, device: DataTableRowData) => { template: (name, device: DataTableRowData) => {
const battery = device.battery_entity const battery = device.battery_entity
? this.hass.states[device.battery_entity] ? this.hass.states[device.battery_entity]
@ -155,6 +157,7 @@ export class HaConfigDeviceDashboard extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true,
direction: "asc", direction: "asc",
}, },
manufacturer: { manufacturer: {
@ -163,6 +166,7 @@ export class HaConfigDeviceDashboard extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
model: { model: {
title: this.hass.localize( title: this.hass.localize(
@ -170,6 +174,7 @@ export class HaConfigDeviceDashboard extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
area: { area: {
title: this.hass.localize( title: this.hass.localize(
@ -177,6 +182,7 @@ export class HaConfigDeviceDashboard extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
integration: { integration: {
title: this.hass.localize( title: this.hass.localize(
@ -184,6 +190,7 @@ export class HaConfigDeviceDashboard extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
battery_entity: { battery_entity: {
title: this.hass.localize( title: this.hass.localize(
@ -191,6 +198,7 @@ export class HaConfigDeviceDashboard extends LitElement {
), ),
sortable: true, sortable: true,
type: "numeric", type: "numeric",
width: "60px",
template: (batteryEntity: string) => { template: (batteryEntity: string) => {
const battery = batteryEntity const battery = batteryEntity
? this.hass.states[batteryEntity] ? this.hass.states[batteryEntity]
@ -247,8 +255,8 @@ export class HaConfigDeviceDashboard extends LitElement {
return batteryEntity ? batteryEntity.entity_id : undefined; return batteryEntity ? batteryEntity.entity_id : undefined;
} }
private _handleRowClicked(ev: CustomEvent) { private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const deviceId = (ev.detail as RowClickedEvent).id; const deviceId = ev.detail.id;
navigate(this, `/config/devices/device/${deviceId}`); navigate(this, `/config/devices/device/${deviceId}`);
} }
} }

View File

@ -134,6 +134,7 @@ export class HaDevicesDataTable extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
template: (name, device: DataTableRowData) => { template: (name, device: DataTableRowData) => {
const battery = device.battery_entity const battery = device.battery_entity
? this.hass.states[device.battery_entity] ? this.hass.states[device.battery_entity]
@ -163,6 +164,7 @@ export class HaDevicesDataTable extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
}, },
manufacturer: { manufacturer: {
title: this.hass.localize( title: this.hass.localize(
@ -170,6 +172,7 @@ export class HaDevicesDataTable extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
model: { model: {
title: this.hass.localize( title: this.hass.localize(
@ -177,6 +180,7 @@ export class HaDevicesDataTable extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
area: { area: {
title: this.hass.localize( title: this.hass.localize(
@ -184,6 +188,7 @@ export class HaDevicesDataTable extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
integration: { integration: {
title: this.hass.localize( title: this.hass.localize(
@ -191,6 +196,7 @@ export class HaDevicesDataTable extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
battery_entity: { battery_entity: {
title: this.hass.localize( title: this.hass.localize(
@ -198,6 +204,7 @@ export class HaDevicesDataTable extends LitElement {
), ),
sortable: true, sortable: true,
type: "numeric", type: "numeric",
width: "60px",
template: (batteryEntity: string) => { template: (batteryEntity: string) => {
const battery = batteryEntity const battery = batteryEntity
? this.hass.states[batteryEntity] ? this.hass.states[batteryEntity]

View File

@ -13,7 +13,10 @@ import { isComponentLoaded } from "../../../../../common/config/is_component_loa
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { HaPaperDialog } from "../../../../../components/dialog/ha-paper-dialog"; import { HaPaperDialog } from "../../../../../components/dialog/ha-paper-dialog";
import { ExtEntityRegistryEntry } from "../../../../../data/entity_registry"; import {
ExtEntityRegistryEntry,
removeEntityRegistryEntry,
} from "../../../../../data/entity_registry";
import { import {
deleteInputBoolean, deleteInputBoolean,
fetchInputBoolean, fetchInputBoolean,
@ -109,23 +112,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
if (this._item === undefined) { if (this._item === undefined) {
return html``; return html``;
} }
if (!this._componentLoaded) { const stateObj = this.hass.states[this.entry.entity_id];
return html`
<paper-dialog-scrollable .dialogElement=${this.dialogElement}>
The ${this.entry.platform} component is not loaded, please add it your
configuration. Either by adding 'default_config:' or
'${this.entry.platform}:'.
</paper-dialog-scrollable>
`;
}
if (this._item === null) {
return html`
<paper-dialog-scrollable .dialogElement=${this.dialogElement}>
This entity can not be edited from the UI. Only entities setup from
the UI are editable.
</paper-dialog-scrollable>
`;
}
return html` return html`
<paper-dialog-scrollable .dialogElement=${this.dialogElement}> <paper-dialog-scrollable .dialogElement=${this.dialogElement}>
${this._error ${this._error
@ -134,13 +121,23 @@ export class EntityRegistrySettingsHelper extends LitElement {
` `
: ""} : ""}
<div class="form"> <div class="form">
<div @value-changed=${this._valueChanged}> ${!this._componentLoaded
${dynamicElement(`ha-${this.entry.platform}-form`, { ? this.hass.localize(
hass: this.hass, "ui.dialogs.helper_settings.platform_not_loaded",
item: this._item, "platform",
entry: this.entry, this.entry.platform
})} )
</div> : this._item === null
? this.hass.localize("ui.dialogs.helper_settings.yaml_not_editable")
: html`
<div @value-changed=${this._valueChanged}>
${dynamicElement(`ha-${this.entry.platform}-form`, {
hass: this.hass,
item: this._item,
entry: this.entry,
})}
</div>
`}
<ha-registry-basic-editor <ha-registry-basic-editor
.hass=${this.hass} .hass=${this.hass}
.entry=${this.entry} .entry=${this.entry}
@ -151,13 +148,14 @@ export class EntityRegistrySettingsHelper extends LitElement {
<mwc-button <mwc-button
class="warning" class="warning"
@click=${this._confirmDeleteItem} @click=${this._confirmDeleteItem}
.disabled=${this._submitting} .disabled=${this._submitting ||
(!this._item && !stateObj?.attributes.restored)}
> >
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")} ${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
</mwc-button> </mwc-button>
<mwc-button <mwc-button
@click=${this._updateItem} @click=${this._updateItem}
.disabled=${this._submitting || !this._item.name} .disabled=${this._submitting || (this._item && !this._item.name)}
> >
${this.hass.localize("ui.dialogs.entity_registry.editor.update")} ${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
</mwc-button> </mwc-button>
@ -178,16 +176,15 @@ export class EntityRegistrySettingsHelper extends LitElement {
} }
private async _updateItem(): Promise<void> { private async _updateItem(): Promise<void> {
if (!this._item) {
return;
}
this._submitting = true; this._submitting = true;
try { try {
await HELPERS[this.entry.platform].update( if (this._componentLoaded && this._item) {
this.hass!, await HELPERS[this.entry.platform].update(
this._item.id, this.hass!,
this._item this._item.id,
); this._item
);
}
await this._registryEditor?.updateEntry(); await this._registryEditor?.updateEntry();
fireEvent(this, "close-dialog"); fireEvent(this, "close-dialog");
} catch (err) { } catch (err) {
@ -198,9 +195,6 @@ export class EntityRegistrySettingsHelper extends LitElement {
} }
private async _confirmDeleteItem(): Promise<void> { private async _confirmDeleteItem(): Promise<void> {
if (!this._item) {
return;
}
if ( if (
!(await showConfirmationDialog(this, { !(await showConfirmationDialog(this, {
text: this.hass.localize( text: this.hass.localize(
@ -214,7 +208,15 @@ export class EntityRegistrySettingsHelper extends LitElement {
this._submitting = true; this._submitting = true;
try { try {
await HELPERS[this.entry.platform].delete(this.hass!, this._item.id); if (this._componentLoaded && this._item) {
await HELPERS[this.entry.platform].delete(this.hass!, this._item.id);
} else {
const stateObj = this.hass.states[this.entry.entity_id];
if (!stateObj?.attributes.restored) {
return;
}
await removeEntityRegistryEntry(this.hass!, this.entry.entity_id);
}
fireEvent(this, "close-dialog"); fireEvent(this, "close-dialog");
} finally { } finally {
this._submitting = false; this._submitting = false;

View File

@ -3,7 +3,7 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { UnsubscribeFunc, HassEntities } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResult, CSSResult,
@ -49,6 +49,7 @@ import { classMap } from "lit-html/directives/class-map";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
// tslint:disable-next-line: no-duplicate-imports // tslint:disable-next-line: no-duplicate-imports
import { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table"; import { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import { HASSDomEvent } from "../../../common/dom/fire_event";
export interface StateEntity extends EntityRegistryEntry { export interface StateEntity extends EntityRegistryEntry {
readonly?: boolean; readonly?: boolean;
@ -58,6 +59,7 @@ export interface StateEntity extends EntityRegistryEntry {
export interface EntityRow extends StateEntity { export interface EntityRow extends StateEntity {
icon: string; icon: string;
unavailable: boolean; unavailable: boolean;
restored: boolean;
status: string; status: string;
} }
@ -68,6 +70,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@property() public narrow!: boolean; @property() public narrow!: boolean;
@property() public route!: Route; @property() public route!: Route;
@property() private _entities?: EntityRegistryEntry[]; @property() private _entities?: EntityRegistryEntry[];
@property() private _stateEntities: StateEntity[] = [];
@property() private _showDisabled = false; @property() private _showDisabled = false;
@property() private _showUnavailable = true; @property() private _showUnavailable = true;
@property() private _showReadOnly = true; @property() private _showReadOnly = true;
@ -94,6 +97,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
}, },
}; };
@ -104,6 +108,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
type: "icon", type: "icon",
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "55px",
template: (_status, entity: any) => template: (_status, entity: any) =>
entity.unavailable || entity.disabled_by || entity.readonly entity.unavailable || entity.disabled_by || entity.readonly
? html` ? html`
@ -115,14 +120,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
style=${styleMap({ style=${styleMap({
color: entity.unavailable ? "var(--google-red-500)" : "", color: entity.unavailable ? "var(--google-red-500)" : "",
})} })}
.icon=${entity.unavailable .icon=${entity.restored
? "hass:restore-alert"
: entity.unavailable
? "hass:alert-circle" ? "hass:alert-circle"
: entity.disabled_by : entity.disabled_by
? "hass:cancel" ? "hass:cancel"
: "hass:pencil-off"} : "hass:pencil-off"}
></ha-icon> ></ha-icon>
<paper-tooltip position="left"> <paper-tooltip position="left">
${entity.unavailable ${entity.restored
? this.hass.localize(
"ui.panel.config.entities.picker.status.restored"
)
: entity.unavailable
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.entities.picker.status.unavailable" "ui.panel.config.entities.picker.status.unavailable"
) )
@ -158,6 +169,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "20%",
}; };
columns.platform = { columns.platform = {
title: this.hass.localize( title: this.hass.localize(
@ -165,6 +177,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "20%",
template: (platform) => template: (platform) =>
this.hass.localize(`component.${platform}.config.title`) || platform, this.hass.localize(`component.${platform}.config.title`) || platform,
}; };
@ -177,40 +190,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
private _filteredEntities = memoize( private _filteredEntities = memoize(
( (
entities: EntityRegistryEntry[], entities: EntityRegistryEntry[],
states: HassEntities, stateEntities: StateEntity[],
showDisabled: boolean, showDisabled: boolean,
showUnavailable: boolean, showUnavailable: boolean,
showReadOnly: boolean showReadOnly: boolean
): EntityRow[] => { ): EntityRow[] => {
const stateEntities: StateEntity[] = [];
if (showReadOnly) {
const regEntityIds = new Set(
entities.map((entity) => entity.entity_id)
);
for (const entityId of Object.keys(states)) {
if (regEntityIds.has(entityId)) {
continue;
}
stateEntities.push({
name: computeStateName(states[entityId]),
entity_id: entityId,
platform: computeDomain(entityId),
disabled_by: null,
readonly: true,
selectable: false,
});
}
}
if (!showDisabled) { if (!showDisabled) {
entities = entities.filter((entity) => !Boolean(entity.disabled_by)); entities = entities.filter((entity) => !Boolean(entity.disabled_by));
} }
const result: EntityRow[] = []; const result: EntityRow[] = [];
for (const entry of entities.concat(stateEntities)) { for (const entry of showReadOnly
const state = states[entry.entity_id]; ? entities.concat(stateEntities)
const unavailable = state?.state === "unavailable"; : entities) {
const entity = this.hass.states[entry.entity_id];
const unavailable = entity?.state === "unavailable";
const restored = entity?.attributes.restored;
if (!showUnavailable && unavailable) { if (!showUnavailable && unavailable) {
continue; continue;
@ -218,14 +214,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
result.push({ result.push({
...entry, ...entry,
icon: state icon: entity
? stateIcon(state) ? stateIcon(entity)
: domainIcon(computeDomain(entry.entity_id)), : domainIcon(computeDomain(entry.entity_id)),
name: name:
computeEntityRegistryName(this.hass!, entry) || computeEntityRegistryName(this.hass!, entry) ||
this.hass.localize("state.default.unavailable"), this.hass.localize("state.default.unavailable"),
unavailable, unavailable,
status: unavailable restored,
status: restored
? this.hass.localize(
"ui.panel.config.entities.picker.status.restored"
)
: unavailable
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.entities.picker.status.unavailable" "ui.panel.config.entities.picker.status.unavailable"
) )
@ -389,7 +390,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.columns=${this._columns(this.narrow, this.hass.language)} .columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._filteredEntities( .data=${this._filteredEntities(
this._entities, this._entities,
this.hass.states, this._stateEntities,
this._showDisabled, this._showDisabled,
this._showUnavailable, this._showUnavailable,
this._showReadOnly this._showReadOnly
@ -418,6 +419,43 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
loadEntityEditorDialog(); loadEntityEditorDialog();
} }
protected updated(changedProps): void {
super.updated(changedProps);
const oldHass = changedProps.get("hass");
let changed = false;
if (!this.hass || !this._entities) {
return;
}
if (changedProps.has("hass") || changedProps.has("_entities")) {
const stateEntities: StateEntity[] = [];
const regEntityIds = new Set(
this._entities.map((entity) => entity.entity_id)
);
for (const entityId of Object.keys(this.hass.states)) {
if (regEntityIds.has(entityId)) {
continue;
}
if (
!oldHass ||
this.hass.states[entityId] !== oldHass.states[entityId]
) {
changed = true;
}
stateEntities.push({
name: computeStateName(this.hass.states[entityId]),
entity_id: entityId,
platform: computeDomain(entityId),
disabled_by: null,
readonly: true,
selectable: false,
});
}
if (changed) {
this._stateEntities = stateEntities;
}
}
}
private _showDisabledChanged() { private _showDisabledChanged() {
this._showDisabled = !this._showDisabled; this._showDisabled = !this._showDisabled;
} }
@ -434,16 +472,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._filter = ev.detail.value; this._filter = ev.detail.value;
} }
private _handleSelectionChanged(ev: CustomEvent): void { private _handleSelectionChanged(
const changedSelection = ev.detail as SelectionChangedEvent; ev: HASSDomEvent<SelectionChangedEvent>
const entity = changedSelection.id; ): void {
if (changedSelection.selected) { this._selectedEntities = ev.detail.value;
this._selectedEntities = [...this._selectedEntities, entity];
} else {
this._selectedEntities = this._selectedEntities.filter(
(entityId) => entityId !== entity
);
}
} }
private _enableSelected() { private _enableSelected() {
@ -499,13 +531,26 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
}); });
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.confirm_title", `ui.panel.config.entities.picker.remove_selected.confirm_${
removeableEntities.length !== this._selectedEntities.length
? "partly_"
: ""
}title`,
"number", "number",
removeableEntities.length removeableEntities.length
), ),
text: this.hass.localize( text:
"ui.panel.config.entities.picker.remove_selected.confirm_text" removeableEntities.length === this._selectedEntities.length
), ? this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.confirm_text"
)
: this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.confirm_partly_text",
"removable",
removeableEntities.length,
"selected",
this._selectedEntities.length
),
confirmText: this.hass.localize("ui.common.yes"), confirmText: this.hass.localize("ui.common.yes"),
dismissText: this.hass.localize("ui.common.no"), dismissText: this.hass.localize("ui.common.no"),
confirm: () => { confirm: () => {

View File

@ -39,8 +39,8 @@ export class HaConfigHelpers extends LitElement {
@property() private _stateItems: HassEntity[] = []; @property() private _stateItems: HassEntity[] = [];
private _columns = memoize( private _columns = memoize(
(_language): DataTableColumnContainer => { (narrow, _language): DataTableColumnContainer => {
return { const columns: DataTableColumnContainer = {
icon: { icon: {
title: "", title: "",
type: "icon", type: "icon",
@ -54,28 +54,45 @@ export class HaConfigHelpers extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true,
direction: "asc", direction: "asc",
template: (name, item: any) => template: (name, item: any) =>
html` html`
${name} ${name}
<div style="color: var(--secondary-text-color)"> ${narrow
${item.entity_id} ? html`
</div> <div class="secondary">
`, ${item.entity_id}
}, </div>
type: { `
title: this.hass.localize( : ""}
"ui.panel.config.helpers.picker.headers.type"
),
sortable: true,
filterable: true,
template: (type) =>
html`
${this.hass.localize(`ui.panel.config.helpers.types.${type}`) ||
type}
`, `,
}, },
}; };
if (!narrow) {
columns.entity_id = {
title: this.hass.localize(
"ui.panel.config.helpers.picker.headers.entity_id"
),
sortable: true,
filterable: true,
width: "30%",
};
}
columns.type = {
title: this.hass.localize(
"ui.panel.config.helpers.picker.headers.type"
),
sortable: true,
width: "30%",
filterable: true,
template: (type) =>
html`
${this.hass.localize(`ui.panel.config.helpers.types.${type}`) ||
type}
`,
};
return columns;
} }
); );
@ -106,7 +123,7 @@ export class HaConfigHelpers extends LitElement {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.automation} .tabs=${configSections.automation}
.columns=${this._columns(this.hass.language)} .columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._getItems(this._stateItems)} .data=${this._getItems(this._stateItems)}
@row-click=${this._openEditDialog} @row-click=${this._openEditDialog}
> >

View File

@ -194,12 +194,14 @@ class HaConfigEntryPage extends LitElement {
return css` return css`
.content { .content {
padding: 4px; padding: 4px;
height: 100%;
} }
p { p {
text-align: center; text-align: center;
} }
ha-devices-data-table { ha-devices-data-table {
width: 100%; width: 100%;
height: 100%;
} }
`; `;
} }

View File

@ -25,9 +25,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() private _params?: LovelaceDashboardDetailsDialogParams; @property() private _params?: LovelaceDashboardDetailsDialogParams;
@property() private _urlPath!: LovelaceDashboard["url_path"]; @property() private _urlPath!: LovelaceDashboard["url_path"];
@property() private _showSidebar!: boolean; @property() private _showInSidebar!: boolean;
@property() private _sidebarIcon!: string; @property() private _icon!: string;
@property() private _sidebarTitle!: string; @property() private _title!: string;
@property() private _requireAdmin!: LovelaceDashboard["require_admin"]; @property() private _requireAdmin!: LovelaceDashboard["require_admin"];
@property() private _error?: string; @property() private _error?: string;
@ -38,17 +38,16 @@ export class DialogLovelaceDashboardDetail extends LitElement {
): Promise<void> { ): Promise<void> {
this._params = params; this._params = params;
this._error = undefined; this._error = undefined;
this._urlPath = this._params.urlPath || "";
if (this._params.dashboard) { if (this._params.dashboard) {
this._urlPath = this._params.dashboard.url_path || ""; this._showInSidebar = !!this._params.dashboard.show_in_sidebar;
this._showSidebar = !!this._params.dashboard.sidebar; this._icon = this._params.dashboard.icon || "";
this._sidebarIcon = this._params.dashboard.sidebar?.icon || ""; this._title = this._params.dashboard.title || "";
this._sidebarTitle = this._params.dashboard.sidebar?.title || "";
this._requireAdmin = this._params.dashboard.require_admin || false; this._requireAdmin = this._params.dashboard.require_admin || false;
} else { } else {
this._urlPath = ""; this._showInSidebar = true;
this._showSidebar = true; this._icon = "";
this._sidebarIcon = ""; this._title = "";
this._sidebarTitle = "";
this._requireAdmin = false; this._requireAdmin = false;
} }
await this.updateComplete; await this.updateComplete;
@ -59,6 +58,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
return html``; return html``;
} }
const urlInvalid = !/^[a-zA-Z0-9_-]+$/.test(this._urlPath); const urlInvalid = !/^[a-zA-Z0-9_-]+$/.test(this._urlPath);
const titleInvalid = !this._urlPath.trim();
return html` return html`
<ha-dialog <ha-dialog
open open
@ -67,97 +67,130 @@ export class DialogLovelaceDashboardDetail extends LitElement {
escapeKeyAction escapeKeyAction
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
this._params.dashboard this._params.urlPath
? this._sidebarTitle || ? this._title ||
this.hass!.localize( this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.edit_dashboard" "ui.panel.config.lovelace.dashboards.detail.edit_dashboard"
) )
: this.hass!.localize( : this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.new_dashboard" "ui.panel.config.lovelace.dashboards.detail.new_dashboard"
) )
)} )}
> >
<div> <div>
${this._error ${this._params.dashboard && !this._params.dashboard.id
? html` ? this.hass.localize(
<div class="error">${this._error}</div> "ui.panel.config.lovelace.dashboards.cant_edit_yaml"
` )
: ""} : this._params.urlPath === "lovelace"
<div class="form"> ? this.hass.localize(
<ha-switch "ui.panel.config.lovelace.dashboards.cant_edit_default"
.checked=${this._showSidebar} )
@change=${this._showSidebarChanged} : html`
>${this.hass!.localize( ${this._error
"ui.panel.config.lovelace.dashboards.detail.show_sidebar" ? html`
)}</ha-switch <div class="error">${this._error}</div>
> `
${this._showSidebar : ""}
? html` <div class="form">
<ha-icon-input
.value=${this._sidebarIcon}
@value-changed=${this._sidebarIconChanged}
.label=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.icon"
)}
></ha-icon-input>
<paper-input <paper-input
.value=${this._sidebarTitle} .value=${this._title}
@value-changed=${this._sidebarTitleChanged} @value-changed=${this._titleChanged}
.label=${this.hass!.localize( .label=${this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.title" "ui.panel.config.lovelace.dashboards.detail.title"
)} )}
@blur=${this._fillUrlPath} @blur=${this._fillUrlPath}
></paper-input> .invalid=${titleInvalid}
` .errorMessage=${this.hass.localize(
: ""} "ui.panel.config.lovelace.dashboards.detail.title_required"
${!this._params.dashboard
? html`
<paper-input
.value=${this._urlPath}
@value-changed=${this._urlChanged}
.label=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.url"
)} )}
.errorMessage=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.url_error_msg"
)}
.invalid=${urlInvalid}
></paper-input> ></paper-input>
` <ha-icon-input
: ""} .value=${this._icon}
<ha-switch @value-changed=${this._iconChanged}
.checked=${this._requireAdmin} .label=${this.hass.localize(
@change=${this._requireAdminChanged} "ui.panel.config.lovelace.dashboards.detail.icon"
>${this.hass!.localize( )}
"ui.panel.config.lovelace.dashboards.detail.require_admin" ></ha-icon-input>
)}</ha-switch ${!this._params.dashboard
> ? html`
</div> <paper-input
.value=${this._urlPath}
@value-changed=${this._urlChanged}
.label=${this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.url"
)}
.errorMessage=${this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.url_error_msg"
)}
.invalid=${urlInvalid}
></paper-input>
`
: ""}
<ha-switch
.checked=${this._showInSidebar}
@change=${this._showSidebarChanged}
>${this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.show_sidebar"
)}
</ha-switch>
<ha-switch
.checked=${this._requireAdmin}
@change=${this._requireAdminChanged}
>${this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.require_admin"
)}
</ha-switch>
</div>
`}
</div> </div>
${this._params.dashboard ${this._params.urlPath
? html` ? html`
${this._params.dashboard?.id
? html`
<mwc-button
slot="secondaryAction"
class="warning"
@click=${this._deleteDashboard}
.disabled=${this._submitting}
>
${this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.delete"
)}
</mwc-button>
`
: ""}
<mwc-button <mwc-button
slot="secondaryAction" slot="secondaryAction"
class="warning" @click=${this._toggleDefault}
@click="${this._deleteDashboard}" .disabled=${this._params.urlPath === "lovelace" &&
.disabled=${this._submitting} (!localStorage.defaultPage ||
localStorage.defaultPage === "lovelace")}
> >
${this.hass!.localize( ${this._params.urlPath === localStorage.defaultPage ||
"ui.panel.config.lovelace.dashboards.detail.delete" (this._params.urlPath === "lovelace" &&
)} !localStorage.defaultPage)
? this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.remove_default"
)
: this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.set_default"
)}
</mwc-button> </mwc-button>
` `
: html``} : ""}
<mwc-button <mwc-button
slot="primaryAction" slot="primaryAction"
@click="${this._updateDashboard}" @click="${this._updateDashboard}"
.disabled=${urlInvalid || this._submitting} .disabled=${urlInvalid || titleInvalid || this._submitting}
> >
${this._params.dashboard ${this._params.urlPath
? this.hass!.localize( ? this._params.dashboard?.id
"ui.panel.config.lovelace.dashboards.detail.update" ? this.hass.localize(
) "ui.panel.config.lovelace.dashboards.detail.update"
: this.hass!.localize( )
: this.hass.localize("ui.common.close")
: this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.create" "ui.panel.config.lovelace.dashboards.detail.create"
)} )}
</mwc-button> </mwc-button>
@ -170,43 +203,59 @@ export class DialogLovelaceDashboardDetail extends LitElement {
this._urlPath = ev.detail.value; this._urlPath = ev.detail.value;
} }
private _sidebarIconChanged(ev: PolymerChangedEvent<string>) { private _iconChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined; this._error = undefined;
this._sidebarIcon = ev.detail.value; this._icon = ev.detail.value;
} }
private _sidebarTitleChanged(ev: PolymerChangedEvent<string>) { private _titleChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined; this._error = undefined;
this._sidebarTitle = ev.detail.value; this._title = ev.detail.value;
} }
private _fillUrlPath() { private _fillUrlPath() {
if (this._urlPath) { if (this._urlPath) {
return; return;
} }
const parts = this._sidebarTitle.split(" "); const parts = this._title.split(" ");
if (parts.length) { if (parts.length) {
this._urlPath = parts[0].toLowerCase(); this._urlPath = parts.join("_").toLowerCase();
} }
} }
private _showSidebarChanged(ev: Event) { private _showSidebarChanged(ev: Event) {
this._showSidebar = (ev.target as HaSwitch).checked; this._showInSidebar = (ev.target as HaSwitch).checked;
} }
private _requireAdminChanged(ev: Event) { private _requireAdminChanged(ev: Event) {
this._requireAdmin = (ev.target as HaSwitch).checked; this._requireAdmin = (ev.target as HaSwitch).checked;
} }
private _toggleDefault() {
const urlPath = this._params?.urlPath;
if (!urlPath) {
return;
}
if (urlPath === localStorage.defaultPage) {
delete localStorage.defaultPage;
} else {
localStorage.defaultPage = urlPath;
}
location.reload();
}
private async _updateDashboard() { private async _updateDashboard() {
if (this._params?.urlPath && !this._params.dashboard?.id) {
this._close();
}
this._submitting = true; this._submitting = true;
try { try {
const values: Partial<LovelaceDashboardMutableParams> = { const values: Partial<LovelaceDashboardMutableParams> = {
require_admin: this._requireAdmin, require_admin: this._requireAdmin,
sidebar: this._showSidebar show_in_sidebar: this._showInSidebar,
? { icon: this._sidebarIcon, title: this._sidebarTitle } icon: this._icon || undefined,
: null, title: this._title,
}; };
if (this._params!.dashboard) { if (this._params!.dashboard) {
await this._params!.updateDashboard(values); await this._params!.updateDashboard(values);
@ -217,7 +266,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
values as LovelaceDashboardCreateParams values as LovelaceDashboardCreateParams
); );
} }
this._params = undefined; this._close();
} catch (err) { } catch (err) {
this._error = err?.message || "Unknown error"; this._error = err?.message || "Unknown error";
} finally { } finally {

View File

@ -9,6 +9,7 @@ import {
css, css,
} from "lit-element"; } from "lit-element";
import memoize from "memoize-one"; import memoize from "memoize-one";
import "@polymer/paper-tooltip/paper-tooltip";
import { import {
DataTableColumnContainer, DataTableColumnContainer,
RowClickedEvent, RowClickedEvent,
@ -24,13 +25,11 @@ import {
updateDashboard, updateDashboard,
deleteDashboard, deleteDashboard,
LovelaceDashboardCreateParams, LovelaceDashboardCreateParams,
LovelacePanelConfig,
} from "../../../../data/lovelace"; } from "../../../../data/lovelace";
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail"; import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
import { compare } from "../../../../common/string/compare"; import { compare } from "../../../../common/string/compare";
import { import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
showConfirmationDialog,
showAlertDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { lovelaceTabs } from "../ha-config-lovelace"; import { lovelaceTabs } from "../ha-config-lovelace";
import { navigate } from "../../../../common/navigate"; import { navigate } from "../../../../common/navigate";
@ -43,7 +42,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
@property() private _dashboards: LovelaceDashboard[] = []; @property() private _dashboards: LovelaceDashboard[] = [];
private _columns = memoize( private _columns = memoize(
(_language, dashboards): DataTableColumnContainer => { (narrow: boolean, _language, dashboards): DataTableColumnContainer => {
const columns: DataTableColumnContainer = { const columns: DataTableColumnContainer = {
icon: { icon: {
title: "", title: "",
@ -62,89 +61,147 @@ export class HaConfigLovelaceDashboards extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
template: (title, dashboard: any) => {
const titleTemplate = html`
${title}
${dashboard.default
? html`
<ha-icon
style="padding-left: 10px;"
icon="hass:check-circle-outline"
></ha-icon>
<paper-tooltip>
This is the default dashdoard.
</paper-tooltip>
`
: ""}
`;
return narrow
? html`
${titleTemplate}
<div class="secondary">
${this.hass.localize(
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
)}${dashboard.filename
? html`
- ${dashboard.filename}
`
: ""}
</div>
`
: titleTemplate;
},
}, },
mode: { };
if (!narrow) {
columns.mode = {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.headers.conf_mode" "ui.panel.config.lovelace.dashboards.picker.headers.conf_mode"
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
template: (mode) => template: (mode) =>
html` html`
${this.hass.localize( ${this.hass.localize(
`ui.panel.config.lovelace.dashboards.conf_mode.${mode}` `ui.panel.config.lovelace.dashboards.conf_mode.${mode}`
) || mode} ) || mode}
`, `,
},
};
if (dashboards.some((dashboard) => dashboard.mode === "yaml")) {
columns.filename = {
title: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.headers.filename"
),
sortable: true,
filterable: true,
}; };
} if (dashboards.some((dashboard) => dashboard.filename)) {
columns.filename = {
const columns2: DataTableColumnContainer = { title: this.hass.localize(
require_admin: { "ui.panel.config.lovelace.dashboards.picker.headers.filename"
),
width: "15%",
sortable: true,
filterable: true,
};
}
columns.require_admin = {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.headers.require_admin" "ui.panel.config.lovelace.dashboards.picker.headers.require_admin"
), ),
sortable: true, sortable: true,
type: "icon", type: "icon",
width: "100px",
template: (requireAdmin: boolean) => template: (requireAdmin: boolean) =>
requireAdmin requireAdmin
? html` ? html`
<ha-icon icon="hass:check-circle-outline"></ha-icon> <ha-icon icon="hass:check"></ha-icon>
` `
: html` : html`
- -
`, `,
}, };
sidebar: { columns.show_in_sidebar = {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.headers.sidebar" "ui.panel.config.lovelace.dashboards.picker.headers.sidebar"
), ),
type: "icon", type: "icon",
width: "100px",
template: (sidebar) => template: (sidebar) =>
sidebar sidebar
? html` ? html`
<ha-icon icon="hass:check-circle-outline"></ha-icon> <ha-icon icon="hass:check"></ha-icon>
` `
: html` : html`
- -
`, `,
}, };
url_path: { }
title: "",
type: "icon", columns.url_path = {
filterable: true, title: "",
template: (urlPath) => filterable: true,
html` width: "75px",
<mwc-button .urlPath=${urlPath} @click=${this._navigate} template: (urlPath) =>
>${this.hass.localize( narrow
"ui.panel.config.lovelace.dashboards.picker.open" ? html`
)}</mwc-button <paper-icon-button
> icon="hass:open-in-new"
`, .urlPath=${urlPath}
}, @click=${this._navigate}
></paper-icon-button>
`
: html`
<mwc-button .urlPath=${urlPath} @click=${this._navigate}
>${this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.open"
)}</mwc-button
>
`,
}; };
return { ...columns, ...columns2 };
return columns;
} }
); );
private _getItems = memoize((dashboards: LovelaceDashboard[]) => { private _getItems = memoize((dashboards: LovelaceDashboard[]) => {
return dashboards.map((dashboard) => { const defaultMode = (this.hass.panels?.lovelace
return { ?.config as LovelacePanelConfig).mode;
filename: "", const isDefault =
...dashboard, !localStorage.defaultPage || localStorage.defaultPage === "lovelace";
icon: dashboard.sidebar?.icon, return [
title: dashboard.sidebar?.title || dashboard.url_path, {
}; icon: "hass:view-dashboard",
}); title: this.hass.localize("panel.states"),
default: isDefault,
sidebar: isDefault,
require_admin: false,
url_path: "lovelace",
mode: defaultMode,
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
},
...dashboards.map((dashboard) => {
return {
...dashboard,
default: localStorage.defaultPage === dashboard.url_path,
};
}),
];
}); });
protected render(): TemplateResult { protected render(): TemplateResult {
@ -161,9 +218,14 @@ export class HaConfigLovelaceDashboards extends LitElement {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${lovelaceTabs} .tabs=${lovelaceTabs}
.columns=${this._columns(this.hass.language, this._dashboards)} .columns=${this._columns(
this.narrow,
this.hass.language,
this._dashboards
)}
.data=${this._getItems(this._dashboards)} .data=${this._getItems(this._dashboards)}
@row-click=${this._editDashboard} @row-click=${this._editDashboard}
id="url_path"
> >
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
<ha-fab <ha-fab
@ -194,28 +256,22 @@ export class HaConfigLovelaceDashboards extends LitElement {
} }
private _editDashboard(ev: CustomEvent) { private _editDashboard(ev: CustomEvent) {
const id = (ev.detail as RowClickedEvent).id; const urlPath = (ev.detail as RowClickedEvent).id;
const dashboard = id const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
? this._dashboards.find((res) => res.id === id) this._openDialog(dashboard, urlPath);
: undefined;
if (!dashboard) {
showAlertDialog(this, {
text: this.hass!.localize(
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
),
});
return;
}
this._openDialog(dashboard);
} }
private _addDashboard() { private _addDashboard() {
this._openDialog(); this._openDialog();
} }
private async _openDialog(dashboard?: LovelaceDashboard): Promise<void> { private async _openDialog(
dashboard?: LovelaceDashboard,
urlPath?: string
): Promise<void> {
showDashboardDetailDialog(this, { showDashboardDetailDialog(this, {
dashboard, dashboard,
urlPath,
createDashboard: async (values: LovelaceDashboardCreateParams) => { createDashboard: async (values: LovelaceDashboardCreateParams) => {
const created = await createDashboard(this.hass!, values); const created = await createDashboard(this.hass!, values);
this._dashboards = this._dashboards!.concat( this._dashboards = this._dashboards!.concat(

View File

@ -7,6 +7,7 @@ import {
export interface LovelaceDashboardDetailsDialogParams { export interface LovelaceDashboardDetailsDialogParams {
dashboard?: LovelaceDashboard; dashboard?: LovelaceDashboard;
urlPath?: string;
createDashboard: (values: LovelaceDashboardCreateParams) => Promise<unknown>; createDashboard: (values: LovelaceDashboardCreateParams) => Promise<unknown>;
updateDashboard: ( updateDashboard: (
updates: Partial<LovelaceDashboardMutableParams> updates: Partial<LovelaceDashboardMutableParams>

View File

@ -57,6 +57,7 @@ export class HaConfigLovelaceRescources extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
}, },
type: { type: {
title: this.hass.localize( title: this.hass.localize(
@ -64,6 +65,7 @@ export class HaConfigLovelaceRescources extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "30%",
template: (type) => template: (type) =>
html` html`
${this.hass.localize( ${this.hass.localize(

View File

@ -128,6 +128,7 @@ class DialogPersonDetail extends LitElement {
<a <a
href="https://www.home-assistant.io/integrations/#presence-detection" href="https://www.home-assistant.io/integrations/#presence-detection"
target="_blank" target="_blank"
rel="noreferrer"
>${this.hass!.localize( >${this.hass!.localize(
"ui.panel.config.person.detail.link_presence_detection_integrations" "ui.panel.config.person.detail.link_presence_detection_integrations"
)}</a )}</a

View File

@ -54,6 +54,7 @@ class HaSceneDashboard extends LitElement {
<a <a
href="https://home-assistant.io/docs/scene/editor/" href="https://home-assistant.io/docs/scene/editor/"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass.localize("ui.panel.config.scene.picker.learn_more")} ${this.hass.localize("ui.panel.config.scene.picker.learn_more")}
</a> </a>

View File

@ -122,6 +122,7 @@ export class HaScriptEditor extends LitElement {
<a <a
href="https://home-assistant.io/docs/scripts/" href="https://home-assistant.io/docs/scripts/"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions" "ui.panel.config.script.editor.link_available_actions"

View File

@ -54,6 +54,7 @@ class HaScriptPicker extends LitElement {
<a <a
href="https://home-assistant.io/docs/scripts/editor/" href="https://home-assistant.io/docs/scripts/editor/"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.script.picker.learn_more" "ui.panel.config.script.picker.learn_more"

View File

@ -25,6 +25,7 @@ import { PolymerChangedEvent } from "../../../polymer-types";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import "@material/mwc-button"; import "@material/mwc-button";
import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("zha-add-group-page") @customElement("zha-add-group-page")
export class ZHAAddGroupPage extends LitElement { export class ZHAAddGroupPage extends LitElement {
@ -82,7 +83,6 @@ export class ZHAAddGroupPage extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
selectable selectable
@selection-changed=${this._handleAddSelectionChanged} @selection-changed=${this._handleAddSelectionChanged}
class="table"
> >
</zha-devices-data-table> </zha-devices-data-table>
@ -114,21 +114,10 @@ export class ZHAAddGroupPage extends LitElement {
this.devices = await fetchGroupableDevices(this.hass!); this.devices = await fetchGroupableDevices(this.hass!);
} }
private _handleAddSelectionChanged(ev: CustomEvent): void { private _handleAddSelectionChanged(
const changedSelection = ev.detail as SelectionChangedEvent; ev: HASSDomEvent<SelectionChangedEvent>
const entity = changedSelection.id; ): void {
if ( this._selectedDevicesToAdd = ev.detail.value;
changedSelection.selected &&
!this._selectedDevicesToAdd.includes(entity)
) {
this._selectedDevicesToAdd.push(entity);
} else {
const index = this._selectedDevicesToAdd.indexOf(entity);
if (index !== -1) {
this._selectedDevicesToAdd.splice(index, 1);
}
}
this._selectedDevicesToAdd = [...this._selectedDevicesToAdd];
} }
private async _createGroup(): Promise<void> { private async _createGroup(): Promise<void> {
@ -168,11 +157,6 @@ export class ZHAAddGroupPage extends LitElement {
float: right; float: right;
} }
.table {
height: 400px;
overflow: auto;
}
ha-config-section *:last-child { ha-config-section *:last-child {
padding-bottom: 24px; padding-bottom: 24px;
} }

View File

@ -49,6 +49,7 @@ export class ZHAClustersDataTable extends LitElement {
title: "Name", title: "Name",
sortable: true, sortable: true,
direction: "asc", direction: "asc",
grows: true,
}, },
} }
: { : {
@ -56,6 +57,7 @@ export class ZHAClustersDataTable extends LitElement {
title: "Name", title: "Name",
sortable: true, sortable: true,
direction: "asc", direction: "asc",
grows: true,
}, },
id: { id: {
title: "ID", title: "ID",
@ -65,10 +67,12 @@ export class ZHAClustersDataTable extends LitElement {
`; `;
}, },
sortable: true, sortable: true,
width: "15%",
}, },
endpoint_id: { endpoint_id: {
title: "Endpoint ID", title: "Endpoint ID",
sortable: true, sortable: true,
width: "15%",
}, },
} }
); );
@ -80,6 +84,7 @@ export class ZHAClustersDataTable extends LitElement {
.data=${this._clusters(this.clusters)} .data=${this._clusters(this.clusters)}
.id=${"cluster_id"} .id=${"cluster_id"}
selectable selectable
auto-height
></ha-data-table> ></ha-data-table>
`; `;
} }

View File

@ -63,6 +63,7 @@ class ZHAConfigDashboard extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
}, },
} }
: { : {
@ -71,16 +72,19 @@ class ZHAConfigDashboard extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
}, },
nwk: { nwk: {
title: "Nwk", title: "Nwk",
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
ieee: { ieee: {
title: "IEEE", title: "IEEE",
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "25%",
}, },
} }
); );
@ -139,6 +143,7 @@ class ZHAConfigDashboard extends LitElement {
.data=${this._memoizeDevices(this._devices)} .data=${this._memoizeDevices(this._devices)}
@row-click=${this._handleDeviceClicked} @row-click=${this._handleDeviceClicked}
.id=${"ieee"} .id=${"ieee"}
auto-height
></ha-data-table> ></ha-data-table>
</ha-card> </ha-card>
</ha-config-section> </ha-config-section>

View File

@ -53,6 +53,7 @@ export class ZHADevicesDataTable extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
template: (name) => html` template: (name) => html`
<div @click=${this._handleClicked} style="cursor: pointer;"> <div @click=${this._handleClicked} style="cursor: pointer;">
${name} ${name}
@ -66,6 +67,7 @@ export class ZHADevicesDataTable extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
template: (name) => html` template: (name) => html`
<div @click=${this._handleClicked} style="cursor: pointer;"> <div @click=${this._handleClicked} style="cursor: pointer;">
${name} ${name}
@ -76,11 +78,13 @@ export class ZHADevicesDataTable extends LitElement {
title: "Manufacturer", title: "Manufacturer",
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "20%",
}, },
model: { model: {
title: "Model", title: "Model",
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "20%",
}, },
} }
); );
@ -91,14 +95,15 @@ export class ZHADevicesDataTable extends LitElement {
.columns=${this._columns(this.narrow)} .columns=${this._columns(this.narrow)}
.data=${this._devices(this.devices)} .data=${this._devices(this.devices)}
.selectable=${this.selectable} .selectable=${this.selectable}
auto-height
></ha-data-table> ></ha-data-table>
`; `;
} }
private async _handleClicked(ev: CustomEvent) { private async _handleClicked(ev: CustomEvent) {
const ieee = (ev.target as HTMLElement) const ieee = ((ev.target as HTMLElement).closest(
.closest("tr")! ".mdc-data-table__row"
.getAttribute("data-row-id")!; ) as any).rowId;
showZHADeviceInfoDialog(this, { ieee }); showZHADeviceInfoDialog(this, { ieee });
} }
} }

View File

@ -32,6 +32,7 @@ import { HomeAssistant } from "../../../types";
import { ItemSelectedEvent } from "./types"; import { ItemSelectedEvent } from "./types";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("zha-group-binding-control") @customElement("zha-group-binding-control")
export class ZHAGroupBindingControl extends LitElement { export class ZHAGroupBindingControl extends LitElement {
@ -200,21 +201,11 @@ export class ZHAGroupBindingControl extends LitElement {
} }
} }
private _handleClusterSelectionChanged(event: CustomEvent): void { private _handleClusterSelectionChanged(
const changedSelection = event.detail as SelectionChangedEvent; ev: HASSDomEvent<SelectionChangedEvent>
const clusterId = changedSelection.id; ): void {
if ( this._selectedClusters = ev.detail.value;
changedSelection.selected &&
!this._selectedClusters.includes(clusterId)
) {
this._selectedClusters.push(clusterId);
} else {
const index = this._selectedClusters.indexOf(clusterId);
if (index !== -1) {
this._selectedClusters.splice(index, 1);
}
}
this._selectedClusters = [...this._selectedClusters];
this._clustersToBind = []; this._clustersToBind = [];
for (const clusterIndex of this._selectedClusters) { for (const clusterIndex of this._selectedClusters) {
const selectedCluster = this._clusters.find((cluster) => { const selectedCluster = this._clusters.find((cluster) => {

View File

@ -31,6 +31,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import "@material/mwc-button"; import "@material/mwc-button";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("zha-group-page") @customElement("zha-group-page")
export class ZHAGroupPage extends LitElement { export class ZHAGroupPage extends LitElement {
@ -145,7 +146,6 @@ export class ZHAGroupPage extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
selectable selectable
@selection-changed=${this._handleRemoveSelectionChanged} @selection-changed=${this._handleRemoveSelectionChanged}
class="table"
> >
</zha-devices-data-table> </zha-devices-data-table>
@ -180,7 +180,6 @@ export class ZHAGroupPage extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
selectable selectable
@selection-changed=${this._handleAddSelectionChanged} @selection-changed=${this._handleAddSelectionChanged}
class="table"
> >
</zha-devices-data-table> </zha-devices-data-table>
@ -223,38 +222,16 @@ export class ZHAGroupPage extends LitElement {
}); });
} }
private _handleAddSelectionChanged(ev: CustomEvent): void { private _handleAddSelectionChanged(
const changedSelection = ev.detail as SelectionChangedEvent; ev: HASSDomEvent<SelectionChangedEvent>
const entity = changedSelection.id; ): void {
if ( this._selectedDevicesToAdd = ev.detail.value;
changedSelection.selected &&
!this._selectedDevicesToAdd.includes(entity)
) {
this._selectedDevicesToAdd.push(entity);
} else {
const index = this._selectedDevicesToAdd.indexOf(entity);
if (index !== -1) {
this._selectedDevicesToAdd.splice(index, 1);
}
}
this._selectedDevicesToAdd = [...this._selectedDevicesToAdd];
} }
private _handleRemoveSelectionChanged(ev: CustomEvent): void { private _handleRemoveSelectionChanged(
const changedSelection = ev.detail as SelectionChangedEvent; ev: HASSDomEvent<SelectionChangedEvent>
const entity = changedSelection.id; ): void {
if ( this._selectedDevicesToRemove = ev.detail.value;
changedSelection.selected &&
!this._selectedDevicesToRemove.includes(entity)
) {
this._selectedDevicesToRemove.push(entity);
} else {
const index = this._selectedDevicesToRemove.indexOf(entity);
if (index !== -1) {
this._selectedDevicesToRemove.splice(index, 1);
}
}
this._selectedDevicesToRemove = [...this._selectedDevicesToRemove];
} }
private async _addMembersToGroup(): Promise<void> { private async _addMembersToGroup(): Promise<void> {
@ -309,11 +286,6 @@ export class ZHAGroupPage extends LitElement {
float: right; float: right;
} }
.table {
height: 200px;
overflow: auto;
}
mwc-button paper-spinner { mwc-button paper-spinner {
width: 14px; width: 14px;
height: 14px; height: 14px;

View File

@ -19,6 +19,7 @@ import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("zha-groups-dashboard") @customElement("zha-groups-dashboard")
export class ZHAGroupsDashboard extends LitElement { export class ZHAGroupsDashboard extends LitElement {
@ -102,21 +103,12 @@ export class ZHAGroupsDashboard extends LitElement {
this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups); this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups);
} }
private _handleRemoveSelectionChanged(ev: CustomEvent): void { private _handleRemoveSelectionChanged(
const changedSelection = ev.detail as SelectionChangedEvent; ev: HASSDomEvent<SelectionChangedEvent>
const groupId = Number(changedSelection.id); ): void {
if ( this._selectedGroupsToRemove = ev.detail.value.map((value) =>
changedSelection.selected && Number(value)
!this._selectedGroupsToRemove.includes(groupId) );
) {
this._selectedGroupsToRemove.push(groupId);
} else {
const index = this._selectedGroupsToRemove.indexOf(groupId);
if (index !== -1) {
this._selectedGroupsToRemove.splice(index, 1);
}
}
this._selectedGroupsToRemove = [...this._selectedGroupsToRemove];
} }
private async _removeGroup(): Promise<void> { private async _removeGroup(): Promise<void> {

View File

@ -52,6 +52,7 @@ export class ZHAGroupsDataTable extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
template: (name) => html` template: (name) => html`
<div @click=${this._handleRowClicked} style="cursor: pointer;"> <div @click=${this._handleRowClicked} style="cursor: pointer;">
${name} ${name}
@ -65,6 +66,7 @@ export class ZHAGroupsDataTable extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
template: (name) => html` template: (name) => html`
<div @click=${this._handleRowClicked} style="cursor: pointer;"> <div @click=${this._handleRowClicked} style="cursor: pointer;">
${name} ${name}
@ -73,6 +75,8 @@ export class ZHAGroupsDataTable extends LitElement {
}, },
group_id: { group_id: {
title: this.hass.localize("ui.panel.config.zha.groups.group_id"), title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
type: "numeric",
width: "15%",
template: (groupId: number) => { template: (groupId: number) => {
return html` return html`
${formatAsPaddedHex(groupId)} ${formatAsPaddedHex(groupId)}
@ -82,6 +86,8 @@ export class ZHAGroupsDataTable extends LitElement {
}, },
members: { members: {
title: this.hass.localize("ui.panel.config.zha.groups.members"), title: this.hass.localize("ui.panel.config.zha.groups.members"),
type: "numeric",
width: "15%",
template: (members: ZHADevice[]) => { template: (members: ZHADevice[]) => {
return html` return html`
${members.length} ${members.length}
@ -98,14 +104,15 @@ export class ZHAGroupsDataTable extends LitElement {
.columns=${this._columns(this.narrow)} .columns=${this._columns(this.narrow)}
.data=${this._groups(this.groups)} .data=${this._groups(this.groups)}
.selectable=${this.selectable} .selectable=${this.selectable}
auto-height
></ha-data-table> ></ha-data-table>
`; `;
} }
private _handleRowClicked(ev: CustomEvent) { private _handleRowClicked(ev: CustomEvent) {
const groupId = (ev.target as HTMLElement) const groupId = ((ev.target as HTMLElement).closest(
.closest("tr")! ".mdc-data-table__row"
.getAttribute("data-row-id")!; ) as any).rowId;
navigate(this, `/config/zha/group/${groupId}`); navigate(this, `/config/zha/group/${groupId}`);
} }
} }

View File

@ -71,6 +71,7 @@ export class ZwaveNetwork extends LitElement {
<a <a
href="https://www.home-assistant.io/docs/z-wave/control-panel/" href="https://www.home-assistant.io/docs/z-wave/control-panel/"
target="_blank" target="_blank"
rel="noreferrer"
> >
${this.hass!.localize("ui.panel.config.zwave.learn_more")} ${this.hass!.localize("ui.panel.config.zwave.learn_more")}
</a> </a>

View File

@ -61,9 +61,11 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
<a <a
href="https://www.home-assistant.io/docs/configuration/events/" href="https://www.home-assistant.io/docs/configuration/events/"
target="_blank" target="_blank"
>[[localize( 'ui.panel.developer-tools.tabs.events.documentation' rel="noreferrer"
)]]</a
> >
[[localize( 'ui.panel.developer-tools.tabs.events.documentation'
)]]
</a>
</p> </p>
<div class="ha-form"> <div class="ha-form">
<paper-input <paper-input

View File

@ -15,7 +15,6 @@ import "./integrations-card";
const JS_TYPE = __BUILD__; const JS_TYPE = __BUILD__;
const JS_VERSION = __VERSION__; const JS_VERSION = __VERSION__;
const OPT_IN_PANEL = "states";
class HaPanelDevInfo extends LitElement { class HaPanelDevInfo extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@ -25,32 +24,10 @@ class HaPanelDevInfo extends LitElement {
const customUiList: Array<{ name: string; url: string; version: string }> = const customUiList: Array<{ name: string; url: string; version: string }> =
(window as any).CUSTOM_UI_LIST || []; (window as any).CUSTOM_UI_LIST || [];
const nonDefaultLink =
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
? "/lovelace"
: "/states";
const nonDefaultLinkText =
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
? this.hass.localize("ui.panel.developer-tools.tabs.info.lovelace_ui")
: `${this.hass.localize(
"ui.panel.developer-tools.tabs.info.states_ui"
)} (DEPRECATED)`;
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} (DEPRECATED)`
)}`;
return html` return html`
<div class="about"> <div class="about">
<p class="version"> <p class="version">
<a href="https://www.home-assistant.io" target="_blank" <a href="https://www.home-assistant.io" target="_blank" rel="noreferrer"
><img ><img
src="/static/icons/favicon-192x192.png" src="/static/icons/favicon-192x192.png"
height="192" height="192"
@ -59,7 +36,7 @@ class HaPanelDevInfo extends LitElement {
)}" )}"
/></a> /></a>
<br /> <br />
<h2>Home Assistant ${hass.config.version}</h2> <h2>Home Assistant ${hass.connection.haVersion}</h2>
</p> </p>
<p> <p>
${this.hass.localize( ${this.hass.localize(
@ -71,7 +48,7 @@ class HaPanelDevInfo extends LitElement {
<p class="develop"> <p class="develop">
<a <a
href="https://www.home-assistant.io/developers/credits/" href="https://www.home-assistant.io/developers/credits/"
target="_blank" target="_blank" rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.developer-tools.tabs.info.developed_by" "ui.panel.developer-tools.tabs.info.developed_by"
@ -85,7 +62,7 @@ class HaPanelDevInfo extends LitElement {
${this.hass.localize("ui.panel.developer-tools.tabs.info.source")} ${this.hass.localize("ui.panel.developer-tools.tabs.info.source")}
<a <a
href="https://github.com/home-assistant/home-assistant" href="https://github.com/home-assistant/home-assistant"
target="_blank" target="_blank" rel="noreferrer"
>${this.hass.localize( >${this.hass.localize(
"ui.panel.developer-tools.tabs.info.server" "ui.panel.developer-tools.tabs.info.server"
)}</a )}</a
@ -93,7 +70,7 @@ class HaPanelDevInfo extends LitElement {
&mdash; &mdash;
<a <a
href="https://github.com/home-assistant/home-assistant-polymer" href="https://github.com/home-assistant/home-assistant-polymer"
target="_blank" target="_blank" rel="noreferrer"
>${this.hass.localize( >${this.hass.localize(
"ui.panel.developer-tools.tabs.info.frontend" "ui.panel.developer-tools.tabs.info.frontend"
)}</a )}</a
@ -103,14 +80,14 @@ class HaPanelDevInfo extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.developer-tools.tabs.info.built_using" "ui.panel.developer-tools.tabs.info.built_using"
)} )}
<a href="https://www.python.org">Python 3</a>, <a href="https://www.python.org" target="_blank" rel="noreferrer">Python 3</a>,
<a href="https://www.polymer-project.org" target="_blank">Polymer</a>, <a href="https://www.polymer-project.org" target="_blank" rel="noreferrer">Polymer</a>,
${this.hass.localize("ui.panel.developer-tools.tabs.info.icons_by")} ${this.hass.localize("ui.panel.developer-tools.tabs.info.icons_by")}
<a href="https://www.google.com/design/icons/" target="_blank" <a href="https://www.google.com/design/icons/" target="_blank" rel="noreferrer"
>Google</a >Google</a
> >
and and
<a href="https://MaterialDesignIcons.com" target="_blank" <a href="https://MaterialDesignIcons.com" target="_blank" rel="noreferrer"
>MaterialDesignIcons.com</a >MaterialDesignIcons.com</a
>. >.
</p> </p>
@ -142,11 +119,6 @@ class HaPanelDevInfo extends LitElement {
: "" : ""
} }
</p> </p>
<p>
<a href="${nonDefaultLink}">${nonDefaultLinkText}</a><br />
<a href="#" @click="${this._toggleDefaultPage}">${defaultPageText}</a
><br />
</p>
</div> </div>
<div class="content"> <div class="content">
<system-health-card .hass=${this.hass}></system-health-card> <system-health-card .hass=${this.hass}></system-health-card>
@ -167,15 +139,6 @@ class HaPanelDevInfo extends LitElement {
}, 1000); }, 1000);
} }
protected _toggleDefaultPage(): void {
if (localStorage.defaultPage === OPT_IN_PANEL) {
delete localStorage.defaultPage;
} else {
localStorage.defaultPage = OPT_IN_PANEL;
}
this.requestUpdate();
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,

View File

@ -32,12 +32,20 @@ class IntegrationsCard extends LitElement {
<tr> <tr>
<td>${domain}</td> <td>${domain}</td>
<td> <td>
<a href=${integrationDocsUrl(domain)} target="_blank"> <a
href=${integrationDocsUrl(domain)}
target="_blank"
rel="noreferrer"
>
Documentation Documentation
</a> </a>
</td> </td>
<td> <td>
<a href=${integrationIssuesUrl(domain)} target="_blank"> <a
href=${integrationIssuesUrl(domain)}
target="_blank"
rel="noreferrer"
>
Issues Issues
</a> </a>
</td> </td>

View File

@ -54,15 +54,22 @@ class DialogSystemLogDetail extends LitElement {
</h2> </h2>
<paper-dialog-scrollable> <paper-dialog-scrollable>
<p> <p>
Logger: ${item.name} Logger: ${item.name}<br />
Source: ${item.source.join(":")}
${integration ${integration
? html` ? html`
<br /> <br />
Integration: ${domainToName(this.hass.localize, integration)} Integration: ${domainToName(this.hass.localize, integration)}
(<a href=${integrationDocsUrl(integration)} target="_blank" (<a
href=${integrationDocsUrl(integration)}
target="_blank"
rel="noreferrer"
>documentation</a >documentation</a
>, >,
<a href=${integrationIssuesUrl(integration)} target="_blank" <a
href=${integrationIssuesUrl(integration)}
target="_blank"
rel="noreferrer"
>issues</a >issues</a
>) >)
` `
@ -81,11 +88,18 @@ class DialogSystemLogDetail extends LitElement {
Last logged: Last logged:
${formatSystemLogTime(item.timestamp, this.hass!.language)} ${formatSystemLogTime(item.timestamp, this.hass!.language)}
</p> </p>
${item.message ${item.message.length > 1
? html` ? html`
<pre>${item.message}</pre> <ul>
${item.message.map(
(msg) =>
html`
<li>${msg}</li>
`
)}
</ul>
` `
: html``} : item.message[0]}
${item.exception ${item.exception
? html` ? html`
<pre>${item.exception}</pre> <pre>${item.exception}</pre>

View File

@ -62,7 +62,7 @@ export class SystemLogCard extends LitElement {
<paper-item @click=${this._openLog} .logItem=${item}> <paper-item @click=${this._openLog} .logItem=${item}>
<paper-item-body two-line> <paper-item-body two-line>
<div class="row"> <div class="row">
${item.message} ${item.message[0]}
</div> </div>
<div secondary> <div secondary>
${formatSystemLogTime( ${formatSystemLogTime(
@ -75,7 +75,7 @@ export class SystemLogCard extends LitElement {
this.hass!.localize, this.hass!.localize,
integrations[idx]! integrations[idx]!
) )
: item.source} : item.source[0]}
(${item.level}) (${item.level})
${item.count > 1 ${item.count > 1
? html` ? html`

View File

@ -68,6 +68,7 @@ class HaPanelDevTemplate extends LocalizeMixin(PolymerElement) {
<a <a
href="http://jinja.pocoo.org/docs/dev/templates/" href="http://jinja.pocoo.org/docs/dev/templates/"
target="_blank" target="_blank"
rel="noreferrer"
>[[localize('ui.panel.developer-tools.tabs.templates.jinja_documentation')]]</a >[[localize('ui.panel.developer-tools.tabs.templates.jinja_documentation')]]</a
> >
</li> </li>
@ -75,6 +76,7 @@ class HaPanelDevTemplate extends LocalizeMixin(PolymerElement) {
<a <a
href="https://home-assistant.io/docs/configuration/templating/" href="https://home-assistant.io/docs/configuration/templating/"
target="_blank" target="_blank"
rel="noreferrer"
>[[localize('ui.panel.developer-tools.tabs.templates.template_extensions')]]</a >[[localize('ui.panel.developer-tools.tabs.templates.template_extensions')]]</a
> >
</li> </li>

View File

@ -1,27 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../states/ha-panel-states";
class HaPanelKiosk extends PolymerElement {
static get template() {
return html`
<ha-panel-states
id="kiosk-states"
hass="[[hass]]"
show-menu
route="[[route]]"
panel-visible
></ha-panel-states>
`;
}
static get properties() {
return {
hass: Object,
route: Object,
};
}
}
customElements.define("ha-panel-kiosk", HaPanelKiosk);

View File

@ -24,6 +24,8 @@ import {
import { AlarmPanelCardConfig } from "./types"; import { AlarmPanelCardConfig } from "./types";
import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { findEntities } from "../common/find-entites";
import { LovelaceConfig } from "../../../data/lovelace";
const ICONS = { const ICONS = {
armed_away: "hass:shield-lock", armed_away: "hass:shield-lock",
@ -46,8 +48,27 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
return document.createElement("hui-alarm-panel-card-editor"); return document.createElement("hui-alarm-panel-card-editor");
} }
public static getStubConfig() { public static getStubConfig(
return { states: ["arm_home", "arm_away"], entity: "" }; hass: HomeAssistant,
lovelaceConfig: LovelaceConfig,
entities?: string[],
entitiesFill?: string[]
) {
const includeDomains = ["alarm_control_panel"];
const maxEntities = 1;
const foundEntities = findEntities(
hass,
lovelaceConfig,
maxEntities,
entities,
entitiesFill,
includeDomains
);
return {
states: ["arm_home", "arm_away"],
entity: foundEntities[0] || "",
};
} }
@property() public hass?: HomeAssistant; @property() public hass?: HomeAssistant;

View File

@ -30,9 +30,10 @@ import { ButtonCardConfig } from "./types";
import { actionHandler } from "../common/directives/action-handler-directive"; import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action"; import { hasAction } from "../common/has-action";
import { handleAction } from "../common/handle-action"; import { handleAction } from "../common/handle-action";
import { ActionHandlerEvent } from "../../../data/lovelace"; import { ActionHandlerEvent, LovelaceConfig } from "../../../data/lovelace";
import { computeActiveState } from "../../../common/entity/compute_active_state"; import { computeActiveState } from "../../../common/entity/compute_active_state";
import { iconColorCSS } from "../../../common/style/icon_color_css"; import { iconColorCSS } from "../../../common/style/icon_color_css";
import { findEntities } from "../common/find-entites";
@customElement("hui-button-card") @customElement("hui-button-card")
export class HuiButtonCard extends LitElement implements LovelaceCard { export class HuiButtonCard extends LitElement implements LovelaceCard {
@ -43,13 +44,28 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
return document.createElement("hui-button-card-editor"); return document.createElement("hui-button-card-editor");
} }
public static getStubConfig(): object { public static getStubConfig(
hass: HomeAssistant,
lovelaceConfig: LovelaceConfig,
entities?: string[],
entitiesFill?: string[]
): object {
const maxEntities = 1;
const foundEntities = findEntities(
hass,
lovelaceConfig,
maxEntities,
entities,
entitiesFill
);
return { return {
tap_action: { action: "toggle" }, tap_action: { action: "toggle" },
hold_action: { action: "more-info" }, hold_action: { action: "more-info" },
show_icon: true, show_icon: true,
show_name: true, show_name: true,
state_color: true, state_color: true,
entity: foundEntities[0] || "",
}; };
} }

View File

@ -2,12 +2,26 @@ import { customElement } from "lit-element";
import { HuiConditionalBase } from "../components/hui-conditional-base"; import { HuiConditionalBase } from "../components/hui-conditional-base";
import { createCardElement } from "../create-element/create-card-element"; import { createCardElement } from "../create-element/create-card-element";
import { LovelaceCard } from "../types"; import { LovelaceCard, LovelaceCardEditor } from "../types";
import { computeCardSize } from "../common/compute-card-size"; import { computeCardSize } from "../common/compute-card-size";
import { ConditionalCardConfig } from "./types"; import { ConditionalCardConfig } from "./types";
@customElement("hui-conditional-card") @customElement("hui-conditional-card")
class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard { class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(
/* webpackChunkName: "hui-conditional-card-editor" */ "../editor/config-elements/hui-conditional-card-editor"
);
return document.createElement("hui-conditional-card-editor");
}
public static getStubConfig(): object {
return {
conditions: [],
card: {},
};
}
public setConfig(config: ConditionalCardConfig): void { public setConfig(config: ConditionalCardConfig): void {
this.validateConfig(config); this.validateConfig(config);

View File

@ -27,6 +27,8 @@ import { createHeaderFooterElement } from "../create-element/create-header-foote
import { LovelaceHeaderFooterConfig } from "../header-footer/types"; import { LovelaceHeaderFooterConfig } from "../header-footer/types";
import { DOMAINS_TOGGLE } from "../../../common/const"; import { DOMAINS_TOGGLE } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { LovelaceConfig } from "../../../data/lovelace";
import { findEntities } from "../common/find-entites";
@customElement("hui-entities-card") @customElement("hui-entities-card")
class HuiEntitiesCard extends LitElement implements LovelaceCard { class HuiEntitiesCard extends LitElement implements LovelaceCard {
@ -37,8 +39,22 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
return document.createElement("hui-entities-card-editor"); return document.createElement("hui-entities-card-editor");
} }
public static getStubConfig(): object { public static getStubConfig(
return { entities: [] }; hass: HomeAssistant,
lovelaceConfig: LovelaceConfig,
entities?: string[],
entitiesFill?: string[]
) {
const maxEntities = 3;
const foundEntities = findEntities(
hass,
lovelaceConfig,
maxEntities,
entities,
entitiesFill
);
return { entities: foundEntities };
} }
@property() private _config?: EntitiesCardConfig; @property() private _config?: EntitiesCardConfig;

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