mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-25 22:07:20 +00:00
Merge pull request #9915 from home-assistant/dev
This commit is contained in:
commit
49947f3337
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@ -73,8 +73,8 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
|
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
|
||||||
tag:
|
tag:
|
||||||
- "3.8-alpine3.12"
|
|
||||||
- "3.9-alpine3.13"
|
- "3.9-alpine3.13"
|
||||||
|
- "3.9-alpine3.14"
|
||||||
steps:
|
steps:
|
||||||
- name: Download requirements.txt
|
- name: Download requirements.txt
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v2
|
||||||
|
@ -5,32 +5,32 @@ require("./translations");
|
|||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean",
|
"clean",
|
||||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
gulp.parallel("clean-translations", () =>
|
||||||
return del([paths.app_output_root, paths.build_dir]);
|
del([paths.app_output_root, paths.build_dir])
|
||||||
})
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-demo",
|
"clean-demo",
|
||||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
gulp.parallel("clean-translations", () =>
|
||||||
return del([paths.demo_output_root, paths.build_dir]);
|
del([paths.demo_output_root, paths.build_dir])
|
||||||
})
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-cast",
|
"clean-cast",
|
||||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
gulp.parallel("clean-translations", () =>
|
||||||
return del([paths.cast_output_root, paths.build_dir]);
|
del([paths.cast_output_root, paths.build_dir])
|
||||||
})
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task("clean-hassio", function cleanOutputAndBuildDir() {
|
gulp.task("clean-hassio", () =>
|
||||||
return del([paths.hassio_output_root, paths.build_dir]);
|
del([paths.hassio_output_root, paths.build_dir])
|
||||||
});
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-gallery",
|
"clean-gallery",
|
||||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
gulp.parallel("clean-translations", () =>
|
||||||
return del([paths.gallery_output_root, paths.build_dir]);
|
del([paths.gallery_output_root, paths.build_dir])
|
||||||
})
|
)
|
||||||
);
|
);
|
||||||
|
@ -12,8 +12,10 @@ const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts);
|
|||||||
const copyFileDir = (fromFile, toDir) =>
|
const copyFileDir = (fromFile, toDir) =>
|
||||||
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile)));
|
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile)));
|
||||||
|
|
||||||
const genStaticPath = (staticDir) => (...parts) =>
|
const genStaticPath =
|
||||||
path.resolve(staticDir, ...parts);
|
(staticDir) =>
|
||||||
|
(...parts) =>
|
||||||
|
path.resolve(staticDir, ...parts);
|
||||||
|
|
||||||
function copyTranslations(staticDir) {
|
function copyTranslations(staticDir) {
|
||||||
const staticPath = genStaticPath(staticDir);
|
const staticPath = genStaticPath(staticDir);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const del = require("del");
|
const del = require("del");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
@ -26,13 +28,6 @@ gulp.task("translations-enable-merge-backend", (done) => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
String.prototype.rsplit = function (sep, maxsplit) {
|
|
||||||
var split = this.split(sep);
|
|
||||||
return maxsplit
|
|
||||||
? [split.slice(0, -maxsplit).join(sep)].concat(split.slice(-maxsplit))
|
|
||||||
: split;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Panel translations which should be split from the core translations.
|
// Panel translations which should be split from the core translations.
|
||||||
const TRANSLATION_FRAGMENTS = Object.keys(
|
const TRANSLATION_FRAGMENTS = Object.keys(
|
||||||
require("../../src/translations/en.json").ui.panel
|
require("../../src/translations/en.json").ui.panel
|
||||||
@ -40,7 +35,7 @@ const TRANSLATION_FRAGMENTS = Object.keys(
|
|||||||
|
|
||||||
function recursiveFlatten(prefix, data) {
|
function recursiveFlatten(prefix, data) {
|
||||||
let output = {};
|
let output = {};
|
||||||
Object.keys(data).forEach(function (key) {
|
Object.keys(data).forEach((key) => {
|
||||||
if (typeof data[key] === "object") {
|
if (typeof data[key] === "object") {
|
||||||
output = {
|
output = {
|
||||||
...output,
|
...output,
|
||||||
@ -101,15 +96,19 @@ function lokaliseTransform(data, original, file) {
|
|||||||
if (value instanceof Object) {
|
if (value instanceof Object) {
|
||||||
output[key] = lokaliseTransform(value, original, file);
|
output[key] = lokaliseTransform(value, original, file);
|
||||||
} else {
|
} else {
|
||||||
output[key] = value.replace(re_key_reference, (match, key) => {
|
output[key] = value.replace(re_key_reference, (_match, lokalise_key) => {
|
||||||
const replace = key.split("::").reduce((tr, k) => {
|
const replace = lokalise_key.split("::").reduce((tr, k) => {
|
||||||
if (!tr) {
|
if (!tr) {
|
||||||
throw Error(`Invalid key placeholder ${key} in ${file.path}`);
|
throw Error(
|
||||||
|
`Invalid key placeholder ${lokalise_key} in ${file.path}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return tr[k];
|
return tr[k];
|
||||||
}, original);
|
}, original);
|
||||||
if (typeof replace !== "string") {
|
if (typeof replace !== "string") {
|
||||||
throw Error(`Invalid key placeholder ${key} in ${file.path}`);
|
throw Error(
|
||||||
|
`Invalid key placeholder ${lokalise_key} in ${file.path}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return replace;
|
return replace;
|
||||||
});
|
});
|
||||||
@ -118,9 +117,7 @@ function lokaliseTransform(data, original, file) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
gulp.task("clean-translations", function () {
|
gulp.task("clean-translations", () => del([workDir]));
|
||||||
return del([workDir]);
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task("ensure-translations-build-dir", (done) => {
|
gulp.task("ensure-translations-build-dir", (done) => {
|
||||||
if (!fs.existsSync(workDir)) {
|
if (!fs.existsSync(workDir)) {
|
||||||
@ -129,7 +126,7 @@ gulp.task("ensure-translations-build-dir", (done) => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("create-test-metadata", function (cb) {
|
gulp.task("create-test-metadata", (cb) => {
|
||||||
fs.writeFile(
|
fs.writeFile(
|
||||||
workDir + "/testMetadata.json",
|
workDir + "/testMetadata.json",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@ -143,17 +140,13 @@ gulp.task("create-test-metadata", function (cb) {
|
|||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"create-test-translation",
|
"create-test-translation",
|
||||||
gulp.series("create-test-metadata", function createTestTranslation() {
|
gulp.series("create-test-metadata", () =>
|
||||||
return gulp
|
gulp
|
||||||
.src(path.join(paths.translations_src, "en.json"))
|
.src(path.join(paths.translations_src, "en.json"))
|
||||||
.pipe(
|
.pipe(transform((data, _file) => recursiveEmpty(data)))
|
||||||
transform(function (data, file) {
|
|
||||||
return recursiveEmpty(data);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.pipe(rename("test.json"))
|
.pipe(rename("test.json"))
|
||||||
.pipe(gulp.dest(workDir));
|
.pipe(gulp.dest(workDir))
|
||||||
})
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -165,7 +158,7 @@ gulp.task(
|
|||||||
* 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.
|
||||||
*/
|
*/
|
||||||
gulp.task("build-master-translation", function () {
|
gulp.task("build-master-translation", () => {
|
||||||
const src = [path.join(paths.translations_src, "en.json")];
|
const src = [path.join(paths.translations_src, "en.json")];
|
||||||
|
|
||||||
if (mergeBackend) {
|
if (mergeBackend) {
|
||||||
@ -174,11 +167,7 @@ gulp.task("build-master-translation", function () {
|
|||||||
|
|
||||||
return gulp
|
return gulp
|
||||||
.src(src)
|
.src(src)
|
||||||
.pipe(
|
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||||
transform(function (data, file) {
|
|
||||||
return lokaliseTransform(data, data, file);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.pipe(
|
.pipe(
|
||||||
merge({
|
merge({
|
||||||
fileName: "translationMaster.json",
|
fileName: "translationMaster.json",
|
||||||
@ -187,18 +176,14 @@ gulp.task("build-master-translation", function () {
|
|||||||
.pipe(gulp.dest(workDir));
|
.pipe(gulp.dest(workDir));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("build-merged-translations", function () {
|
gulp.task("build-merged-translations", () =>
|
||||||
return gulp
|
gulp
|
||||||
.src([inFrontendDir + "/*.json", workDir + "/test.json"], {
|
.src([inFrontendDir + "/*.json", workDir + "/test.json"], {
|
||||||
allowEmpty: true,
|
allowEmpty: true,
|
||||||
})
|
})
|
||||||
|
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||||
.pipe(
|
.pipe(
|
||||||
transform(function (data, file) {
|
foreach((stream, file) => {
|
||||||
return lokaliseTransform(data, data, file);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.pipe(
|
|
||||||
foreach(function (stream, file) {
|
|
||||||
// For each language generate a merged json file. It begins with the master
|
// For each language generate a merged json file. It begins with the master
|
||||||
// translation as a failsafe for untranslated strings, and merges all parent
|
// translation as a failsafe for untranslated strings, and merges all parent
|
||||||
// tags into one file for each specific subtag
|
// tags into one file for each specific subtag
|
||||||
@ -230,17 +215,17 @@ gulp.task("build-merged-translations", function () {
|
|||||||
)
|
)
|
||||||
.pipe(gulp.dest(fullDir));
|
.pipe(gulp.dest(fullDir));
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
});
|
);
|
||||||
|
|
||||||
var taskName;
|
let 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(taskName, function () {
|
gulp.task(taskName, () =>
|
||||||
// Return only the translations for this fragment.
|
// Return only the translations for this fragment.
|
||||||
return gulp
|
gulp
|
||||||
.src(fullDir + "/*.json")
|
.src(fullDir + "/*.json")
|
||||||
.pipe(
|
.pipe(
|
||||||
transform((data) => ({
|
transform((data) => ({
|
||||||
@ -251,18 +236,18 @@ TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
.pipe(gulp.dest(workDir + "/" + fragment));
|
.pipe(gulp.dest(workDir + "/" + fragment))
|
||||||
});
|
);
|
||||||
splitTasks.push(taskName);
|
splitTasks.push(taskName);
|
||||||
});
|
});
|
||||||
|
|
||||||
taskName = "build-translation-core";
|
taskName = "build-translation-core";
|
||||||
gulp.task(taskName, function () {
|
gulp.task(taskName, () =>
|
||||||
// Remove the fragment translations from the core translation.
|
// Remove the fragment translations from the core translation.
|
||||||
return gulp
|
gulp
|
||||||
.src(fullDir + "/*.json")
|
.src(fullDir + "/*.json")
|
||||||
.pipe(
|
.pipe(
|
||||||
transform((data, file) => {
|
transform((data, _file) => {
|
||||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||||
delete data.ui.panel[fragment];
|
delete data.ui.panel[fragment];
|
||||||
});
|
});
|
||||||
@ -270,14 +255,14 @@ gulp.task(taskName, function () {
|
|||||||
return data;
|
return data;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.pipe(gulp.dest(coreDir));
|
.pipe(gulp.dest(coreDir))
|
||||||
});
|
);
|
||||||
|
|
||||||
splitTasks.push(taskName);
|
splitTasks.push(taskName);
|
||||||
|
|
||||||
gulp.task("build-flattened-translations", function () {
|
gulp.task("build-flattened-translations", () =>
|
||||||
// Flatten the split versions of our translations, and move them into outDir
|
// Flatten the split versions of our translations, and move them into outDir
|
||||||
return gulp
|
gulp
|
||||||
.src(
|
.src(
|
||||||
TRANSLATION_FRAGMENTS.map(
|
TRANSLATION_FRAGMENTS.map(
|
||||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||||
@ -285,41 +270,45 @@ gulp.task("build-flattened-translations", function () {
|
|||||||
{ base: workDir }
|
{ base: workDir }
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
transform(function (data) {
|
transform((data) =>
|
||||||
// Polymer.AppLocalizeBehavior requires flattened json
|
// Polymer.AppLocalizeBehavior requires flattened json
|
||||||
return flatten(data);
|
flatten(data)
|
||||||
})
|
)
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
rename((filePath) => {
|
rename((filePath) => {
|
||||||
if (filePath.dirname === "core") {
|
if (filePath.dirname === "core") {
|
||||||
filePath.dirname = "";
|
filePath.dirname = "";
|
||||||
}
|
}
|
||||||
|
// In dev we create the file with the fake hash in the filename
|
||||||
|
if (!env.isProdBuild()) {
|
||||||
|
filePath.basename += "-dev";
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.pipe(gulp.dest(outDir));
|
.pipe(gulp.dest(outDir))
|
||||||
});
|
);
|
||||||
|
|
||||||
const fingerprints = {};
|
const fingerprints = {};
|
||||||
|
|
||||||
gulp.task(
|
gulp.task("build-translation-fingerprints", () => {
|
||||||
"build-translation-fingerprints",
|
// Fingerprint full file of each language
|
||||||
function fingerprintTranslationFiles() {
|
const files = fs.readdirSync(fullDir);
|
||||||
// Fingerprint full file of each language
|
|
||||||
const files = fs.readdirSync(fullDir);
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
fingerprints[files[i].split(".")[0]] = {
|
fingerprints[files[i].split(".")[0]] = {
|
||||||
// In dev we create fake hashes
|
// In dev we create fake hashes
|
||||||
hash: env.isProdBuild()
|
hash: env.isProdBuild()
|
||||||
? crypto
|
? crypto
|
||||||
.createHash("md5")
|
.createHash("md5")
|
||||||
.update(fs.readFileSync(path.join(fullDir, files[i]), "utf-8"))
|
.update(fs.readFileSync(path.join(fullDir, files[i]), "utf-8"))
|
||||||
.digest("hex")
|
.digest("hex")
|
||||||
: "dev",
|
: "dev",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In dev we create the file with the fake hash in the filename
|
||||||
|
if (env.isProdBuild()) {
|
||||||
mapFiles(outDir, ".json", (filename) => {
|
mapFiles(outDir, ".json", (filename) => {
|
||||||
const parsed = path.parse(filename);
|
const parsed = path.parse(filename);
|
||||||
|
|
||||||
@ -335,35 +324,35 @@ gulp.task(
|
|||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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-translation-fragment-supervisor", function () {
|
const stream = source("translationFingerprints.json");
|
||||||
return gulp
|
stream.write(JSON.stringify(fingerprints));
|
||||||
|
process.nextTick(() => stream.end());
|
||||||
|
return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("build-translation-fragment-supervisor", () =>
|
||||||
|
gulp
|
||||||
.src(fullDir + "/*.json")
|
.src(fullDir + "/*.json")
|
||||||
.pipe(transform((data) => data.supervisor))
|
.pipe(transform((data) => data.supervisor))
|
||||||
.pipe(gulp.dest(workDir + "/supervisor"));
|
.pipe(gulp.dest(workDir + "/supervisor"))
|
||||||
});
|
);
|
||||||
|
|
||||||
gulp.task("build-translation-flatten-supervisor", function () {
|
gulp.task("build-translation-flatten-supervisor", () =>
|
||||||
return gulp
|
gulp
|
||||||
.src(workDir + "/supervisor/*.json")
|
.src(workDir + "/supervisor/*.json")
|
||||||
.pipe(
|
.pipe(
|
||||||
transform(function (data) {
|
transform((data) =>
|
||||||
// Polymer.AppLocalizeBehavior requires flattened json
|
// Polymer.AppLocalizeBehavior requires flattened json
|
||||||
return flatten(data);
|
flatten(data)
|
||||||
})
|
)
|
||||||
)
|
)
|
||||||
.pipe(gulp.dest(outDir));
|
.pipe(gulp.dest(outDir))
|
||||||
});
|
);
|
||||||
|
|
||||||
gulp.task("build-translation-write-metadata", function writeMetadata() {
|
gulp.task("build-translation-write-metadata", () =>
|
||||||
return gulp
|
gulp
|
||||||
.src(
|
.src(
|
||||||
[
|
[
|
||||||
path.join(paths.translations_src, "translationMetadata.json"),
|
path.join(paths.translations_src, "translationMetadata.json"),
|
||||||
@ -374,13 +363,14 @@ gulp.task("build-translation-write-metadata", function writeMetadata() {
|
|||||||
)
|
)
|
||||||
.pipe(merge({}))
|
.pipe(merge({}))
|
||||||
.pipe(
|
.pipe(
|
||||||
transform(function (data) {
|
transform((data) => {
|
||||||
const newData = {};
|
const newData = {};
|
||||||
Object.entries(data).forEach(([key, value]) => {
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
// Filter out translations without native name.
|
// Filter out translations without native name.
|
||||||
if (value.nativeName) {
|
if (value.nativeName) {
|
||||||
newData[key] = value;
|
newData[key] = value;
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.warn(
|
console.warn(
|
||||||
`Skipping language ${key}. Native name was not translated.`
|
`Skipping language ${key}. Native name was not translated.`
|
||||||
);
|
);
|
||||||
@ -396,19 +386,26 @@ gulp.task("build-translation-write-metadata", function writeMetadata() {
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
.pipe(rename("translationMetadata.json"))
|
.pipe(rename("translationMetadata.json"))
|
||||||
.pipe(gulp.dest(workDir));
|
.pipe(gulp.dest(workDir))
|
||||||
});
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"create-translations",
|
||||||
|
gulp.series(
|
||||||
|
env.isProdBuild() ? (done) => done() : "create-test-translation",
|
||||||
|
"build-master-translation",
|
||||||
|
"build-merged-translations",
|
||||||
|
gulp.parallel(...splitTasks),
|
||||||
|
"build-flattened-translations"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"build-translations",
|
"build-translations",
|
||||||
gulp.series(
|
gulp.series(
|
||||||
"clean-translations",
|
"clean-translations",
|
||||||
"ensure-translations-build-dir",
|
"ensure-translations-build-dir",
|
||||||
env.isProdBuild() ? (done) => done() : "create-test-translation",
|
"create-translations",
|
||||||
"build-master-translation",
|
|
||||||
"build-merged-translations",
|
|
||||||
gulp.parallel(...splitTasks),
|
|
||||||
"build-flattened-translations",
|
|
||||||
"build-translation-fingerprints",
|
"build-translation-fingerprints",
|
||||||
"build-translation-write-metadata"
|
"build-translation-write-metadata"
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
// Tasks to run webpack.
|
// Tasks to run webpack.
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const gulp = require("gulp");
|
const gulp = require("gulp");
|
||||||
@ -44,7 +45,7 @@ const runDevServer = ({
|
|||||||
open: true,
|
open: true,
|
||||||
watchContentBase: true,
|
watchContentBase: true,
|
||||||
contentBase,
|
contentBase,
|
||||||
}).listen(port, listenHost, function (err) {
|
}).listen(port, listenHost, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@ -65,6 +66,7 @@ const doneHandler = (done) => (err, stats) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stats.hasErrors() || stats.hasWarnings()) {
|
if (stats.hasErrors() || stats.hasWarnings()) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(stats.toString("minimal"));
|
console.log(stats.toString("minimal"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,16 +92,10 @@ gulp.task("webpack-watch-app", () => {
|
|||||||
process.env.ES5
|
process.env.ES5
|
||||||
? bothBuilds(createAppConfig, { isProdBuild: false })
|
? bothBuilds(createAppConfig, { isProdBuild: false })
|
||||||
: createAppConfig({ isProdBuild: false, latestBuild: true })
|
: createAppConfig({ isProdBuild: false, latestBuild: true })
|
||||||
).watch(
|
).watch({ poll: isWsl }, doneHandler());
|
||||||
{
|
|
||||||
ignored: /build-translations/,
|
|
||||||
poll: isWsl,
|
|
||||||
},
|
|
||||||
doneHandler()
|
|
||||||
);
|
|
||||||
gulp.watch(
|
gulp.watch(
|
||||||
path.join(paths.translations_src, "en.json"),
|
path.join(paths.translations_src, "en.json"),
|
||||||
gulp.series("build-translations", "copy-translations-app")
|
gulp.series("create-translations", "copy-translations-app")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ import { mockTranslations } from "./stubs/translations";
|
|||||||
import { mockEnergy } from "./stubs/energy";
|
import { mockEnergy } from "./stubs/energy";
|
||||||
import { mockConfig } from "./stubs/config";
|
import { mockConfig } from "./stubs/config";
|
||||||
import { energyEntities } from "./stubs/entities";
|
import { energyEntities } from "./stubs/entities";
|
||||||
import { mockForecastSolar } from "./stubs/forecast_solar";
|
|
||||||
|
|
||||||
class HaDemo extends HomeAssistantAppEl {
|
class HaDemo extends HomeAssistantAppEl {
|
||||||
protected async _initializeHass() {
|
protected async _initializeHass() {
|
||||||
@ -52,7 +51,6 @@ class HaDemo extends HomeAssistantAppEl {
|
|||||||
mockMediaPlayer(hass);
|
mockMediaPlayer(hass);
|
||||||
mockFrontend(hass);
|
mockFrontend(hass);
|
||||||
mockEnergy(hass);
|
mockEnergy(hass);
|
||||||
mockForecastSolar(hass);
|
|
||||||
mockConfig(hass);
|
mockConfig(hass);
|
||||||
mockPersistentNotification(hass);
|
mockPersistentNotification(hass);
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { format, startOfToday, startOfTomorrow } from "date-fns";
|
||||||
|
import { EnergySolarForecasts } from "../../../src/data/energy";
|
||||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
export const mockEnergy = (hass: MockHomeAssistant) => {
|
export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||||
@ -44,6 +46,19 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
|||||||
stat_energy_from: "sensor.solar_production",
|
stat_energy_from: "sensor.solar_production",
|
||||||
config_entry_solar_forecast: ["solar_forecast"],
|
config_entry_solar_forecast: ["solar_forecast"],
|
||||||
},
|
},
|
||||||
|
/* {
|
||||||
|
type: "battery",
|
||||||
|
stat_energy_from: "sensor.battery_output",
|
||||||
|
stat_energy_to: "sensor.battery_input",
|
||||||
|
}, */
|
||||||
|
{
|
||||||
|
type: "gas",
|
||||||
|
stat_energy_from: "sensor.energy_gas",
|
||||||
|
stat_cost: "sensor.energy_gas_cost",
|
||||||
|
entity_energy_from: "sensor.energy_gas",
|
||||||
|
entity_energy_price: null,
|
||||||
|
number_energy_price: null,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
device_consumption: [
|
device_consumption: [
|
||||||
{
|
{
|
||||||
@ -67,4 +82,53 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
hass.mockWS("energy/info", () => ({ cost_sensors: [] }));
|
hass.mockWS("energy/info", () => ({ cost_sensors: [] }));
|
||||||
|
const todayString = format(startOfToday(), "yyyy-MM-dd");
|
||||||
|
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
|
||||||
|
hass.mockWS(
|
||||||
|
"energy/solar_forecast",
|
||||||
|
(): EnergySolarForecasts => ({
|
||||||
|
solar_forecast: {
|
||||||
|
wh_hours: {
|
||||||
|
[`${todayString}T06:00:00`]: 0,
|
||||||
|
[`${todayString}T06:23:00`]: 6,
|
||||||
|
[`${todayString}T06:45:00`]: 39,
|
||||||
|
[`${todayString}T07:00:00`]: 28,
|
||||||
|
[`${todayString}T08:00:00`]: 208,
|
||||||
|
[`${todayString}T09:00:00`]: 352,
|
||||||
|
[`${todayString}T10:00:00`]: 544,
|
||||||
|
[`${todayString}T11:00:00`]: 748,
|
||||||
|
[`${todayString}T12:00:00`]: 1259,
|
||||||
|
[`${todayString}T13:00:00`]: 1361,
|
||||||
|
[`${todayString}T14:00:00`]: 1373,
|
||||||
|
[`${todayString}T15:00:00`]: 1370,
|
||||||
|
[`${todayString}T16:00:00`]: 1186,
|
||||||
|
[`${todayString}T17:00:00`]: 937,
|
||||||
|
[`${todayString}T18:00:00`]: 652,
|
||||||
|
[`${todayString}T19:00:00`]: 370,
|
||||||
|
[`${todayString}T20:00:00`]: 155,
|
||||||
|
[`${todayString}T21:48:00`]: 24,
|
||||||
|
[`${todayString}T22:36:00`]: 0,
|
||||||
|
[`${tomorrowString}T06:01:00`]: 0,
|
||||||
|
[`${tomorrowString}T06:23:00`]: 9,
|
||||||
|
[`${tomorrowString}T06:45:00`]: 47,
|
||||||
|
[`${tomorrowString}T07:00:00`]: 48,
|
||||||
|
[`${tomorrowString}T08:00:00`]: 473,
|
||||||
|
[`${tomorrowString}T09:00:00`]: 827,
|
||||||
|
[`${tomorrowString}T10:00:00`]: 1153,
|
||||||
|
[`${tomorrowString}T11:00:00`]: 1413,
|
||||||
|
[`${tomorrowString}T12:00:00`]: 1590,
|
||||||
|
[`${tomorrowString}T13:00:00`]: 1652,
|
||||||
|
[`${tomorrowString}T14:00:00`]: 1612,
|
||||||
|
[`${tomorrowString}T15:00:00`]: 1438,
|
||||||
|
[`${tomorrowString}T16:00:00`]: 1149,
|
||||||
|
[`${tomorrowString}T17:00:00`]: 830,
|
||||||
|
[`${tomorrowString}T18:00:00`]: 542,
|
||||||
|
[`${tomorrowString}T19:00:00`]: 311,
|
||||||
|
[`${tomorrowString}T20:00:00`]: 140,
|
||||||
|
[`${tomorrowString}T21:47:00`]: 22,
|
||||||
|
[`${tomorrowString}T22:34:00`]: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -18,6 +18,24 @@ export const energyEntities = () =>
|
|||||||
unit_of_measurement: "kWh",
|
unit_of_measurement: "kWh",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"sensor.battery_input": {
|
||||||
|
entity_id: "sensor.battery_input",
|
||||||
|
state: "4",
|
||||||
|
attributes: {
|
||||||
|
last_reset: "1970-01-01T00:00:00:00+00",
|
||||||
|
friendly_name: "Battery Input",
|
||||||
|
unit_of_measurement: "kWh",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.battery_output": {
|
||||||
|
entity_id: "sensor.battery_output",
|
||||||
|
state: "3",
|
||||||
|
attributes: {
|
||||||
|
last_reset: "1970-01-01T00:00:00:00+00",
|
||||||
|
friendly_name: "Battery Output",
|
||||||
|
unit_of_measurement: "kWh",
|
||||||
|
},
|
||||||
|
},
|
||||||
"sensor.energy_consumption_tarif_1": {
|
"sensor.energy_consumption_tarif_1": {
|
||||||
entity_id: "sensor.energy_consumption_tarif_1 ",
|
entity_id: "sensor.energy_consumption_tarif_1 ",
|
||||||
state: "88.6",
|
state: "88.6",
|
||||||
@ -86,6 +104,23 @@ export const energyEntities = () =>
|
|||||||
unit_of_measurement: "EUR",
|
unit_of_measurement: "EUR",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"sensor.energy_gas_cost": {
|
||||||
|
entity_id: "sensor.energy_gas_cost",
|
||||||
|
state: "2",
|
||||||
|
attributes: {
|
||||||
|
last_reset: "1970-01-01T00:00:00:00+00",
|
||||||
|
unit_of_measurement: "EUR",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.energy_gas": {
|
||||||
|
entity_id: "sensor.energy_gas",
|
||||||
|
state: "4",
|
||||||
|
attributes: {
|
||||||
|
last_reset: "1970-01-01T00:00:00:00+00",
|
||||||
|
friendly_name: "Gas",
|
||||||
|
unit_of_measurement: "m³",
|
||||||
|
},
|
||||||
|
},
|
||||||
"sensor.energy_car": {
|
"sensor.energy_car": {
|
||||||
entity_id: "sensor.energy_car",
|
entity_id: "sensor.energy_car",
|
||||||
state: "4",
|
state: "4",
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
import { format, startOfToday, startOfTomorrow } from "date-fns";
|
|
||||||
import { ForecastSolarForecast } from "../../../src/data/forecast_solar";
|
|
||||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
|
||||||
|
|
||||||
export const mockForecastSolar = (hass: MockHomeAssistant) => {
|
|
||||||
const todayString = format(startOfToday(), "yyyy-MM-dd");
|
|
||||||
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
|
|
||||||
hass.mockWS(
|
|
||||||
"forecast_solar/forecasts",
|
|
||||||
(): Record<string, ForecastSolarForecast> => ({
|
|
||||||
solar_forecast: {
|
|
||||||
wh_hours: {
|
|
||||||
[`${todayString}T06:00:00`]: 0,
|
|
||||||
[`${todayString}T06:23:00`]: 6,
|
|
||||||
[`${todayString}T06:45:00`]: 39,
|
|
||||||
[`${todayString}T07:00:00`]: 28,
|
|
||||||
[`${todayString}T08:00:00`]: 208,
|
|
||||||
[`${todayString}T09:00:00`]: 352,
|
|
||||||
[`${todayString}T10:00:00`]: 544,
|
|
||||||
[`${todayString}T11:00:00`]: 748,
|
|
||||||
[`${todayString}T12:00:00`]: 1259,
|
|
||||||
[`${todayString}T13:00:00`]: 1361,
|
|
||||||
[`${todayString}T14:00:00`]: 1373,
|
|
||||||
[`${todayString}T15:00:00`]: 1370,
|
|
||||||
[`${todayString}T16:00:00`]: 1186,
|
|
||||||
[`${todayString}T17:00:00`]: 937,
|
|
||||||
[`${todayString}T18:00:00`]: 652,
|
|
||||||
[`${todayString}T19:00:00`]: 370,
|
|
||||||
[`${todayString}T20:00:00`]: 155,
|
|
||||||
[`${todayString}T21:48:00`]: 24,
|
|
||||||
[`${todayString}T22:36:00`]: 0,
|
|
||||||
[`${tomorrowString}T06:01:00`]: 0,
|
|
||||||
[`${tomorrowString}T06:23:00`]: 9,
|
|
||||||
[`${tomorrowString}T06:45:00`]: 47,
|
|
||||||
[`${tomorrowString}T07:00:00`]: 48,
|
|
||||||
[`${tomorrowString}T08:00:00`]: 473,
|
|
||||||
[`${tomorrowString}T09:00:00`]: 827,
|
|
||||||
[`${tomorrowString}T10:00:00`]: 1153,
|
|
||||||
[`${tomorrowString}T11:00:00`]: 1413,
|
|
||||||
[`${tomorrowString}T12:00:00`]: 1590,
|
|
||||||
[`${tomorrowString}T13:00:00`]: 1652,
|
|
||||||
[`${tomorrowString}T14:00:00`]: 1612,
|
|
||||||
[`${tomorrowString}T15:00:00`]: 1438,
|
|
||||||
[`${tomorrowString}T16:00:00`]: 1149,
|
|
||||||
[`${tomorrowString}T17:00:00`]: 830,
|
|
||||||
[`${tomorrowString}T18:00:00`]: 542,
|
|
||||||
[`${tomorrowString}T19:00:00`]: 311,
|
|
||||||
[`${tomorrowString}T20:00:00`]: 140,
|
|
||||||
[`${tomorrowString}T21:47:00`]: 22,
|
|
||||||
[`${tomorrowString}T22:34:00`]: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,4 +1,4 @@
|
|||||||
import { addHours, differenceInHours } from "date-fns";
|
import { addHours, differenceInHours, endOfDay } from "date-fns";
|
||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { StatisticValue } from "../../../src/data/history";
|
import { StatisticValue } from "../../../src/data/history";
|
||||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
@ -222,6 +222,7 @@ const statisticsFunctions: Record<
|
|||||||
"sensor.energy_production_tarif_2": (id, start, end) => {
|
"sensor.energy_production_tarif_2": (id, start, end) => {
|
||||||
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
|
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
|
||||||
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
|
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
|
||||||
|
const dayEnd = new Date(endOfDay(productionEnd));
|
||||||
const production = generateCurvedStatistics(
|
const production = generateCurvedStatistics(
|
||||||
id,
|
id,
|
||||||
productionStart,
|
productionStart,
|
||||||
@ -237,15 +238,17 @@ const statisticsFunctions: Record<
|
|||||||
const evening = generateSumStatistics(
|
const evening = generateSumStatistics(
|
||||||
id,
|
id,
|
||||||
productionEnd,
|
productionEnd,
|
||||||
end,
|
dayEnd,
|
||||||
productionFinalVal,
|
productionFinalVal,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
return [...morning, ...production, ...evening];
|
const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 1);
|
||||||
|
return [...morning, ...production, ...evening, ...rest];
|
||||||
},
|
},
|
||||||
"sensor.solar_production": (id, start, end) => {
|
"sensor.solar_production": (id, start, end) => {
|
||||||
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
|
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
|
||||||
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
|
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
|
||||||
|
const dayEnd = new Date(endOfDay(productionEnd));
|
||||||
const production = generateCurvedStatistics(
|
const production = generateCurvedStatistics(
|
||||||
id,
|
id,
|
||||||
productionStart,
|
productionStart,
|
||||||
@ -261,11 +264,12 @@ const statisticsFunctions: Record<
|
|||||||
const evening = generateSumStatistics(
|
const evening = generateSumStatistics(
|
||||||
id,
|
id,
|
||||||
productionEnd,
|
productionEnd,
|
||||||
end,
|
dayEnd,
|
||||||
productionFinalVal,
|
productionFinalVal,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
return [...morning, ...production, ...evening];
|
const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 2);
|
||||||
|
return [...morning, ...production, ...evening, ...rest];
|
||||||
},
|
},
|
||||||
"sensor.grid_fossil_fuel_percentage": (id, start, end) =>
|
"sensor.grid_fossil_fuel_percentage": (id, start, end) =>
|
||||||
generateMeanStatistics(id, start, end, 35, 1.3),
|
generateMeanStatistics(id, start, end, 35, 1.3),
|
||||||
|
150
gallery/src/demos/demo-ha-alert.ts
Normal file
150
gallery/src/demos/demo-ha-alert.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { html, css, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import "../../../src/components/ha-alert";
|
||||||
|
import "../../../src/components/ha-card";
|
||||||
|
|
||||||
|
const alerts: {
|
||||||
|
title?: string;
|
||||||
|
description: string | TemplateResult;
|
||||||
|
type: "info" | "warning" | "error" | "success";
|
||||||
|
dismissable?: boolean;
|
||||||
|
action?: string;
|
||||||
|
rtl?: boolean;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
title: "Test info alert",
|
||||||
|
description: "This is a test info alert with a title and description",
|
||||||
|
type: "info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Test warning alert",
|
||||||
|
description: "This is a test warning alert with a title and description",
|
||||||
|
type: "warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Test error alert",
|
||||||
|
description: "This is a test error alert with a title and description",
|
||||||
|
type: "error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Test warning with long string",
|
||||||
|
description:
|
||||||
|
"sensor.lorem_ipsum_lorem_ipsum_lorem_ipsum_lorem_ipsum_lorem_ipsum_lorem_ipsum_lorem_ipsum_lorem_ipsum_lorem_ipsum_lorem_ipsum_lorem_ipsum_lorem_ipsum",
|
||||||
|
type: "warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Test success alert",
|
||||||
|
description: "This is a test success alert with a title and description",
|
||||||
|
type: "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "This is a test info alert with description only",
|
||||||
|
type: "info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"This is a test warning alert with a rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really rally really really long description only",
|
||||||
|
type: "warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Error with description and list",
|
||||||
|
description: html`<p>
|
||||||
|
This is a test error alert with a title, description and a list
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>List item #1</li>
|
||||||
|
<li>List item #2</li>
|
||||||
|
<li>List item #3</li>
|
||||||
|
</ul>`,
|
||||||
|
type: "error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Test dismissable alert",
|
||||||
|
description: "This is a test success alert that can be dismissable",
|
||||||
|
type: "success",
|
||||||
|
dismissable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Dismissable information",
|
||||||
|
type: "info",
|
||||||
|
dismissable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Error with action",
|
||||||
|
description: "This is a test error alert with action",
|
||||||
|
type: "error",
|
||||||
|
action: "restart",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Unsaved data",
|
||||||
|
description: "You have unsaved data",
|
||||||
|
type: "warning",
|
||||||
|
action: "save",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Dismissable information (RTL)",
|
||||||
|
type: "info",
|
||||||
|
dismissable: true,
|
||||||
|
rtl: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Error with action",
|
||||||
|
description: "This is a test error alert with action (RTL)",
|
||||||
|
type: "error",
|
||||||
|
action: "restart",
|
||||||
|
rtl: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Test success alert (RTL)",
|
||||||
|
description: "This is a test success alert with a title and description",
|
||||||
|
type: "success",
|
||||||
|
rtl: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("demo-ha-alert")
|
||||||
|
export class DemoHaAlert extends LitElement {
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-card header="ha-alert demo">
|
||||||
|
${alerts.map(
|
||||||
|
(alert) => html`
|
||||||
|
<ha-alert
|
||||||
|
.title=${alert.title || ""}
|
||||||
|
.alertType=${alert.type}
|
||||||
|
.dismissable=${alert.dismissable || false}
|
||||||
|
.actionText=${alert.action || ""}
|
||||||
|
.rtl=${alert.rtl || false}
|
||||||
|
>
|
||||||
|
${alert.description}
|
||||||
|
</ha-alert>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
.condition {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-ha-alert": DemoHaAlert;
|
||||||
|
}
|
||||||
|
}
|
@ -172,6 +172,14 @@ class HaGallery extends PolymerElement {
|
|||||||
this.$.notifications.showDialog({ message: ev.detail.message })
|
this.$.notifications.showDialog({ message: ev.detail.message })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.addEventListener("alert-dismissed-clicked", () =>
|
||||||
|
this.$.notifications.showDialog({ message: "Alert dismissed clicked" })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addEventListener("alert-action-clicked", () =>
|
||||||
|
this.$.notifications.showDialog({ message: "Alert action clicked" })
|
||||||
|
);
|
||||||
|
|
||||||
this.addEventListener("hass-more-info", (ev) => {
|
this.addEventListener("hass-more-info", (ev) => {
|
||||||
if (ev.detail.entityId) {
|
if (ev.detail.entityId) {
|
||||||
this.$.notifications.showDialog({
|
this.$.notifications.showDialog({
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import "web-animations-js/web-animations-next-lite.min";
|
import "web-animations-js/web-animations-next-lite.min";
|
||||||
import "../../../../src/components/buttons/ha-progress-button";
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import {
|
import {
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
@ -53,7 +54,9 @@ class HassioAddonAudio extends LitElement {
|
|||||||
.header=${this.supervisor.localize("addon.configuration.audio.header")}
|
.header=${this.supervisor.localize("addon.configuration.audio.header")}
|
||||||
>
|
>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
|
|
||||||
<paper-dropdown-menu
|
<paper-dropdown-menu
|
||||||
.label=${this.supervisor.localize(
|
.label=${this.supervisor.localize(
|
||||||
@ -117,10 +120,6 @@ class HassioAddonAudio extends LitElement {
|
|||||||
paper-dropdown-menu {
|
paper-dropdown-menu {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.errors {
|
|
||||||
color: var(--error-color);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
paper-item {
|
paper-item {
|
||||||
width: 450px;
|
width: 450px;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import { fireEvent } from "../../../../src/common/dom/fire_event";
|
|||||||
import "../../../../src/components/buttons/ha-progress-button";
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
import "../../../../src/components/ha-button-menu";
|
import "../../../../src/components/ha-button-menu";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-form/ha-form";
|
import "../../../../src/components/ha-form/ha-form";
|
||||||
import type { HaFormSchema } from "../../../../src/components/ha-form/ha-form";
|
import type { HaFormSchema } from "../../../../src/components/ha-form/ha-form";
|
||||||
import "../../../../src/components/ha-formfield";
|
import "../../../../src/components/ha-formfield";
|
||||||
@ -135,17 +136,19 @@ class HassioAddonConfig extends LitElement {
|
|||||||
@value-changed=${this._configChanged}
|
@value-changed=${this._configChanged}
|
||||||
.yamlSchema=${ADDON_YAML_SCHEMA}
|
.yamlSchema=${ADDON_YAML_SCHEMA}
|
||||||
></ha-yaml-editor>`}
|
></ha-yaml-editor>`}
|
||||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
${!this._yamlMode ||
|
${!this._yamlMode ||
|
||||||
(this._canShowSchema && this.addon.schema) ||
|
(this._canShowSchema && this.addon.schema) ||
|
||||||
this._valid
|
this._valid
|
||||||
? ""
|
? ""
|
||||||
: html`
|
: html`
|
||||||
<div class="errors">
|
<ha-alert alert-type="error">
|
||||||
${this.supervisor.localize(
|
${this.supervisor.localize(
|
||||||
"addon.configuration.options.invalid_yaml"
|
"addon.configuration.options.invalid_yaml"
|
||||||
)}
|
)}
|
||||||
</div>
|
</ha-alert>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
${hasHiddenOptions
|
${hasHiddenOptions
|
||||||
@ -324,13 +327,7 @@ class HassioAddonConfig extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.errors {
|
|
||||||
color: var(--error-color);
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
.syntaxerror {
|
|
||||||
color: var(--error-color);
|
|
||||||
}
|
|
||||||
.card-menu {
|
.card-menu {
|
||||||
float: right;
|
float: right;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import "../../../../src/components/buttons/ha-progress-button";
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import {
|
import {
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
@ -62,7 +63,9 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -225,10 +228,6 @@ class HassioAddonNetwork extends LitElement {
|
|||||||
ha-card {
|
ha-card {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.errors {
|
|
||||||
color: var(--error-color);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.card-actions {
|
.card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-circular-progress";
|
import "../../../../src/components/ha-circular-progress";
|
||||||
import "../../../../src/components/ha-markdown";
|
import "../../../../src/components/ha-markdown";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
@ -38,7 +39,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ha-card>
|
<ha-card>
|
||||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${this._content
|
${this._content
|
||||||
? html`<ha-markdown .content=${this._content}></ha-markdown>`
|
? html`<ha-markdown .content=${this._content}></ha-markdown>`
|
||||||
|
@ -23,6 +23,7 @@ import { fireEvent } from "../../../../src/common/dom/fire_event";
|
|||||||
import { navigate } from "../../../../src/common/navigate";
|
import { navigate } from "../../../../src/common/navigate";
|
||||||
import "../../../../src/components/buttons/ha-call-api-button";
|
import "../../../../src/components/buttons/ha-call-api-button";
|
||||||
import "../../../../src/components/buttons/ha-progress-button";
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import "../../../../src/components/ha-label-badge";
|
import "../../../../src/components/ha-label-badge";
|
||||||
import "../../../../src/components/ha-markdown";
|
import "../../../../src/components/ha-markdown";
|
||||||
@ -143,14 +144,14 @@ class HassioAddonInfo extends LitElement {
|
|||||||
this.addon.arch
|
this.addon.arch
|
||||||
)
|
)
|
||||||
? html`
|
? html`
|
||||||
<p class="warning">
|
<ha-alert alert-type="warning">
|
||||||
${this.supervisor.localize(
|
${this.supervisor.localize(
|
||||||
"addon.dashboard.not_available_arch"
|
"addon.dashboard.not_available_arch"
|
||||||
)}
|
)}
|
||||||
</p>
|
</ha-alert>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<p class="warning">
|
<ha-alert alert-type="warning">
|
||||||
${this.supervisor.localize(
|
${this.supervisor.localize(
|
||||||
"addon.dashboard.not_available_arch",
|
"addon.dashboard.not_available_arch",
|
||||||
"core_version_installed",
|
"core_version_installed",
|
||||||
@ -158,7 +159,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
"core_version_needed",
|
"core_version_needed",
|
||||||
addonStoreInfo.homeassistant
|
addonStoreInfo.homeassistant
|
||||||
)}
|
)}
|
||||||
</p>
|
</ha-alert>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
@ -569,21 +570,23 @@ class HassioAddonInfo extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
${!this.addon.version && addonStoreInfo && !this.addon.available
|
${!this.addon.version && addonStoreInfo && !this.addon.available
|
||||||
? !addonArchIsSupported(
|
? !addonArchIsSupported(
|
||||||
this.supervisor.info.supported_arch,
|
this.supervisor.info.supported_arch,
|
||||||
this.addon.arch
|
this.addon.arch
|
||||||
)
|
)
|
||||||
? html`
|
? html`
|
||||||
<p class="warning">
|
<ha-alert alert-type="warning">
|
||||||
${this.supervisor.localize(
|
${this.supervisor.localize(
|
||||||
"addon.dashboard.not_available_arch"
|
"addon.dashboard.not_available_arch"
|
||||||
)}
|
)}
|
||||||
</p>
|
</ha-alert>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<p class="warning">
|
<ha-alert alert-type="warning">
|
||||||
${this.supervisor.localize(
|
${this.supervisor.localize(
|
||||||
"addon.dashboard.not_available_version",
|
"addon.dashboard.not_available_version",
|
||||||
"core_version_installed",
|
"core_version_installed",
|
||||||
@ -591,7 +594,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
"core_version_needed",
|
"core_version_needed",
|
||||||
addonStoreInfo!.homeassistant
|
addonStoreInfo!.homeassistant
|
||||||
)}
|
)}
|
||||||
</p>
|
</ha-alert>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
@ -987,7 +990,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
name: this.addon.name,
|
name: this.addon.name,
|
||||||
version: this.addon.version_latest,
|
version: this.addon.version_latest,
|
||||||
snapshotParams: {
|
backupParams: {
|
||||||
name: `addon_${this.addon.slug}_${this.addon.version}`,
|
name: `addon_${this.addon.slug}_${this.addon.version}`,
|
||||||
addons: [this.addon.slug],
|
addons: [this.addon.slug],
|
||||||
homeassistant: false,
|
homeassistant: false,
|
||||||
@ -1149,6 +1152,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
img.logo {
|
img.logo {
|
||||||
|
max-width: 100%;
|
||||||
max-height: 60px;
|
max-height: 60px;
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
display: block;
|
display: block;
|
||||||
@ -1158,10 +1162,10 @@ class HassioAddonInfo extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
ha-svg-icon.running {
|
ha-svg-icon.running {
|
||||||
color: var(--paper-green-400);
|
color: var(--success-color);
|
||||||
}
|
}
|
||||||
ha-svg-icon.stopped {
|
ha-svg-icon.stopped {
|
||||||
color: var(--google-red-300);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
ha-call-api-button {
|
ha-call-api-button {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import {
|
import {
|
||||||
fetchHassioAddonLogs,
|
fetchHassioAddonLogs,
|
||||||
@ -34,7 +35,9 @@ class HassioAddonLogs extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<h1>${this.addon.name}</h1>
|
<h1>${this.addon.name}</h1>
|
||||||
<ha-card>
|
<ha-card>
|
||||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${this._content
|
${this._content
|
||||||
? html`<hassio-ansi-to-html
|
? html`<hassio-ansi-to-html
|
||||||
@ -60,10 +63,6 @@ class HassioAddonLogs extends LitElement {
|
|||||||
ha-card {
|
ha-card {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.errors {
|
|
||||||
color: var(--error-color);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -25,12 +25,12 @@ import "../../../src/components/ha-button-menu";
|
|||||||
import "../../../src/components/ha-fab";
|
import "../../../src/components/ha-fab";
|
||||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
fetchHassioSnapshots,
|
fetchHassioBackups,
|
||||||
friendlyFolderName,
|
friendlyFolderName,
|
||||||
HassioSnapshot,
|
HassioBackup,
|
||||||
reloadHassioSnapshots,
|
reloadHassioBackups,
|
||||||
removeSnapshot,
|
removeBackup,
|
||||||
} from "../../../src/data/hassio/snapshot";
|
} from "../../../src/data/hassio/backup";
|
||||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
@ -40,14 +40,14 @@ import "../../../src/layouts/hass-tabs-subpage-data-table";
|
|||||||
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
|
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
import { HomeAssistant, Route } from "../../../src/types";
|
import { HomeAssistant, Route } from "../../../src/types";
|
||||||
import { showHassioCreateSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-create-snapshot";
|
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
|
||||||
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
|
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
|
||||||
import { showSnapshotUploadDialog } from "../dialogs/snapshot/show-dialog-snapshot-upload";
|
import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload";
|
||||||
import { supervisorTabs } from "../hassio-tabs";
|
import { supervisorTabs } from "../hassio-tabs";
|
||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
|
||||||
@customElement("hassio-snapshots")
|
@customElement("hassio-backups")
|
||||||
export class HassioSnapshots extends LitElement {
|
export class HassioBackups extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||||
@ -58,9 +58,9 @@ export class HassioSnapshots extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public isWide!: boolean;
|
@property({ type: Boolean }) public isWide!: boolean;
|
||||||
|
|
||||||
@state() private _selectedSnapshots: string[] = [];
|
@state() private _selectedBackups: string[] = [];
|
||||||
|
|
||||||
@state() private _snapshots?: HassioSnapshot[] = [];
|
@state() private _backups?: HassioBackup[] = [];
|
||||||
|
|
||||||
@query("hass-tabs-subpage-data-table", true)
|
@query("hass-tabs-subpage-data-table", true)
|
||||||
private _dataTable!: HaTabsSubpageDataTable;
|
private _dataTable!: HaTabsSubpageDataTable;
|
||||||
@ -75,26 +75,26 @@ export class HassioSnapshots extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async refreshData() {
|
public async refreshData() {
|
||||||
await reloadHassioSnapshots(this.hass);
|
await reloadHassioBackups(this.hass);
|
||||||
await this.fetchSnapshots();
|
await this.fetchBackups();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeSnapshotContent = (snapshot: HassioSnapshot): string => {
|
private _computeBackupContent = (backup: HassioBackup): string => {
|
||||||
if (snapshot.type === "full") {
|
if (backup.type === "full") {
|
||||||
return this.supervisor.localize("snapshot.full_snapshot");
|
return this.supervisor.localize("backup.full_backup");
|
||||||
}
|
}
|
||||||
const content: string[] = [];
|
const content: string[] = [];
|
||||||
if (snapshot.content.homeassistant) {
|
if (backup.content.homeassistant) {
|
||||||
content.push("Home Assistant");
|
content.push("Home Assistant");
|
||||||
}
|
}
|
||||||
if (snapshot.content.folders.length !== 0) {
|
if (backup.content.folders.length !== 0) {
|
||||||
for (const folder of snapshot.content.folders) {
|
for (const folder of backup.content.folders) {
|
||||||
content.push(friendlyFolderName[folder] || folder);
|
content.push(friendlyFolderName[folder] || folder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.content.addons.length !== 0) {
|
if (backup.content.addons.length !== 0) {
|
||||||
for (const addon of snapshot.content.addons) {
|
for (const addon of backup.content.addons) {
|
||||||
content.push(
|
content.push(
|
||||||
this.supervisor.supervisor.addons.find(
|
this.supervisor.supervisor.addons.find(
|
||||||
(entry) => entry.slug === addon
|
(entry) => entry.slug === addon
|
||||||
@ -117,16 +117,16 @@ export class HassioSnapshots extends LitElement {
|
|||||||
private _columns = memoizeOne(
|
private _columns = memoizeOne(
|
||||||
(narrow: boolean): DataTableColumnContainer => ({
|
(narrow: boolean): DataTableColumnContainer => ({
|
||||||
name: {
|
name: {
|
||||||
title: this.supervisor?.localize("snapshot.name") || "",
|
title: this.supervisor?.localize("backup.name") || "",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
grows: true,
|
grows: true,
|
||||||
template: (entry: string, snapshot: any) =>
|
template: (entry: string, backup: any) =>
|
||||||
html`${entry || snapshot.slug}
|
html`${entry || backup.slug}
|
||||||
<div class="secondary">${snapshot.secondary}</div>`,
|
<div class="secondary">${backup.secondary}</div>`,
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
title: this.supervisor?.localize("snapshot.created") || "",
|
title: this.supervisor?.localize("backup.created") || "",
|
||||||
width: "15%",
|
width: "15%",
|
||||||
direction: "desc",
|
direction: "desc",
|
||||||
hidden: narrow,
|
hidden: narrow,
|
||||||
@ -143,10 +143,10 @@ export class HassioSnapshots extends LitElement {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
private _snapshotData = memoizeOne((snapshots: HassioSnapshot[]) =>
|
private _backupData = memoizeOne((backups: HassioBackup[]) =>
|
||||||
snapshots.map((snapshot) => ({
|
backups.map((backup) => ({
|
||||||
...snapshot,
|
...backup,
|
||||||
secondary: this._computeSnapshotContent(snapshot),
|
secondary: this._computeBackupContent(backup),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -160,11 +160,11 @@ export class HassioSnapshots extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.localizeFunc=${this.supervisor.localize}
|
.localizeFunc=${this.supervisor.localize}
|
||||||
.searchLabel=${this.supervisor.localize("search")}
|
.searchLabel=${this.supervisor.localize("search")}
|
||||||
.noDataText=${this.supervisor.localize("snapshot.no_snapshots")}
|
.noDataText=${this.supervisor.localize("backup.no_backups")}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.columns=${this._columns(this.narrow)}
|
.columns=${this._columns(this.narrow)}
|
||||||
.data=${this._snapshotData(this._snapshots || [])}
|
.data=${this._backupData(this._backups || [])}
|
||||||
id="slug"
|
id="slug"
|
||||||
@row-click=${this._handleRowClicked}
|
@row-click=${this._handleRowClicked}
|
||||||
@selection-changed=${this._handleSelectionChanged}
|
@selection-changed=${this._handleSelectionChanged}
|
||||||
@ -187,12 +187,12 @@ export class HassioSnapshots extends LitElement {
|
|||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
${atLeastVersion(this.hass.config.version, 0, 116)
|
${atLeastVersion(this.hass.config.version, 0, 116)
|
||||||
? html`<mwc-list-item>
|
? html`<mwc-list-item>
|
||||||
${this.supervisor?.localize("snapshot.upload_snapshot")}
|
${this.supervisor?.localize("backup.upload_backup")}
|
||||||
</mwc-list-item>`
|
</mwc-list-item>`
|
||||||
: ""}
|
: ""}
|
||||||
</ha-button-menu>
|
</ha-button-menu>
|
||||||
|
|
||||||
${this._selectedSnapshots.length
|
${this._selectedBackups.length
|
||||||
? html`<div
|
? html`<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
"header-toolbar": this.narrow,
|
"header-toolbar": this.narrow,
|
||||||
@ -201,8 +201,8 @@ export class HassioSnapshots extends LitElement {
|
|||||||
slot="header"
|
slot="header"
|
||||||
>
|
>
|
||||||
<p class="selected-txt">
|
<p class="selected-txt">
|
||||||
${this.supervisor.localize("snapshot.selected", {
|
${this.supervisor.localize("backup.selected", {
|
||||||
number: this._selectedSnapshots.length,
|
number: this._selectedBackups.length,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<div class="header-btns">
|
<div class="header-btns">
|
||||||
@ -212,7 +212,7 @@ export class HassioSnapshots extends LitElement {
|
|||||||
@click=${this._deleteSelected}
|
@click=${this._deleteSelected}
|
||||||
class="warning"
|
class="warning"
|
||||||
>
|
>
|
||||||
${this.supervisor.localize("snapshot.delete_selected")}
|
${this.supervisor.localize("backup.delete_selected")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
@ -224,7 +224,7 @@ export class HassioSnapshots extends LitElement {
|
|||||||
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
|
||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
<paper-tooltip animation-delay="0" for="delete-btn">
|
<paper-tooltip animation-delay="0" for="delete-btn">
|
||||||
${this.supervisor.localize("snapshot.delete_selected")}
|
${this.supervisor.localize("backup.delete_selected")}
|
||||||
</paper-tooltip>
|
</paper-tooltip>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
@ -233,8 +233,8 @@ export class HassioSnapshots extends LitElement {
|
|||||||
|
|
||||||
<ha-fab
|
<ha-fab
|
||||||
slot="fab"
|
slot="fab"
|
||||||
@click=${this._createSnapshot}
|
@click=${this._createBackup}
|
||||||
.label=${this.supervisor.localize("snapshot.create_snapshot")}
|
.label=${this.supervisor.localize("backup.create_backup")}
|
||||||
extended
|
extended
|
||||||
>
|
>
|
||||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||||
@ -249,7 +249,7 @@ export class HassioSnapshots extends LitElement {
|
|||||||
this.refreshData();
|
this.refreshData();
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
this._showUploadSnapshotDialog();
|
this._showUploadBackupDialog();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,33 +257,33 @@ export class HassioSnapshots extends LitElement {
|
|||||||
private _handleSelectionChanged(
|
private _handleSelectionChanged(
|
||||||
ev: HASSDomEvent<SelectionChangedEvent>
|
ev: HASSDomEvent<SelectionChangedEvent>
|
||||||
): void {
|
): void {
|
||||||
this._selectedSnapshots = ev.detail.value;
|
this._selectedBackups = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showUploadSnapshotDialog() {
|
private _showUploadBackupDialog() {
|
||||||
showSnapshotUploadDialog(this, {
|
showBackupUploadDialog(this, {
|
||||||
showSnapshot: (slug: string) =>
|
showBackup: (slug: string) =>
|
||||||
showHassioSnapshotDialog(this, {
|
showHassioBackupDialog(this, {
|
||||||
slug,
|
slug,
|
||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
onDelete: () => this.fetchSnapshots(),
|
onDelete: () => this.fetchBackups(),
|
||||||
}),
|
}),
|
||||||
reloadSnapshot: () => this.refreshData(),
|
reloadBackup: () => this.refreshData(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchSnapshots() {
|
private async fetchBackups() {
|
||||||
await reloadHassioSnapshots(this.hass);
|
await reloadHassioBackups(this.hass);
|
||||||
this._snapshots = await fetchHassioSnapshots(this.hass);
|
this._backups = await fetchHassioBackups(this.hass);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _deleteSelected() {
|
private async _deleteSelected() {
|
||||||
const confirm = await showConfirmationDialog(this, {
|
const confirm = await showConfirmationDialog(this, {
|
||||||
title: this.supervisor.localize("snapshot.delete_snapshot_title"),
|
title: this.supervisor.localize("backup.delete_backup_title"),
|
||||||
text: this.supervisor.localize("snapshot.delete_snapshot_text", {
|
text: this.supervisor.localize("backup.delete_backup_text", {
|
||||||
number: this._selectedSnapshots.length,
|
number: this._selectedBackups.length,
|
||||||
}),
|
}),
|
||||||
confirmText: this.supervisor.localize("snapshot.delete_snapshot_confirm"),
|
confirmText: this.supervisor.localize("backup.delete_backup_confirm"),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
@ -292,44 +292,44 @@ export class HassioSnapshots extends LitElement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this._selectedSnapshots.map((slug) => removeSnapshot(this.hass, slug))
|
this._selectedBackups.map((slug) => removeBackup(this.hass, slug))
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this.supervisor.localize("snapshot.failed_to_delete"),
|
title: this.supervisor.localize("backup.failed_to_delete"),
|
||||||
text: extractApiErrorMessage(err),
|
text: extractApiErrorMessage(err),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await reloadHassioSnapshots(this.hass);
|
await reloadHassioBackups(this.hass);
|
||||||
this._snapshots = await fetchHassioSnapshots(this.hass);
|
this._backups = await fetchHassioBackups(this.hass);
|
||||||
this._dataTable.clearSelection();
|
this._dataTable.clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||||
const slug = ev.detail.id;
|
const slug = ev.detail.id;
|
||||||
showHassioSnapshotDialog(this, {
|
showHassioBackupDialog(this, {
|
||||||
slug,
|
slug,
|
||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
onDelete: () => this.fetchSnapshots(),
|
onDelete: () => this.fetchBackups(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createSnapshot() {
|
private _createBackup() {
|
||||||
if (this.supervisor!.info.state !== "running") {
|
if (this.supervisor!.info.state !== "running") {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this.supervisor!.localize("snapshot.could_not_create"),
|
title: this.supervisor!.localize("backup.could_not_create"),
|
||||||
text: this.supervisor!.localize(
|
text: this.supervisor!.localize(
|
||||||
"snapshot.create_blocked_not_running",
|
"backup.create_blocked_not_running",
|
||||||
"state",
|
"state",
|
||||||
this.supervisor!.info.state
|
this.supervisor!.info.state
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showHassioCreateSnapshotDialog(this, {
|
showHassioCreateBackupDialog(this, {
|
||||||
supervisor: this.supervisor!,
|
supervisor: this.supervisor!,
|
||||||
onCreate: () => this.fetchSnapshots(),
|
onCreate: () => this.fetchBackups(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,6 +378,6 @@ export class HassioSnapshots extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"hassio-snapshots": HassioSnapshots;
|
"hassio-backups": HassioBackups;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -41,16 +41,16 @@ class HassioAnsiToHtml extends LitElement {
|
|||||||
text-decoration: underline line-through;
|
text-decoration: underline line-through;
|
||||||
}
|
}
|
||||||
.fg-red {
|
.fg-red {
|
||||||
color: rgb(222, 56, 43);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
.fg-green {
|
.fg-green {
|
||||||
color: rgb(57, 181, 74);
|
color: var(--success-color);
|
||||||
}
|
}
|
||||||
.fg-yellow {
|
.fg-yellow {
|
||||||
color: rgb(255, 199, 6);
|
color: var(--warning-color);
|
||||||
}
|
}
|
||||||
.fg-blue {
|
.fg-blue {
|
||||||
color: rgb(0, 111, 184);
|
color: var(--info-color);
|
||||||
}
|
}
|
||||||
.fg-magenta {
|
.fg-magenta {
|
||||||
color: rgb(118, 38, 113);
|
color: rgb(118, 38, 113);
|
||||||
@ -65,16 +65,16 @@ class HassioAnsiToHtml extends LitElement {
|
|||||||
background-color: rgb(0, 0, 0);
|
background-color: rgb(0, 0, 0);
|
||||||
}
|
}
|
||||||
.bg-red {
|
.bg-red {
|
||||||
background-color: rgb(222, 56, 43);
|
background-color: var(--error-color);
|
||||||
}
|
}
|
||||||
.bg-green {
|
.bg-green {
|
||||||
background-color: rgb(57, 181, 74);
|
background-color: var(--success-color);
|
||||||
}
|
}
|
||||||
.bg-yellow {
|
.bg-yellow {
|
||||||
background-color: rgb(255, 199, 6);
|
background-color: var(--warning-color);
|
||||||
}
|
}
|
||||||
.bg-blue {
|
.bg-blue {
|
||||||
background-color: rgb(0, 111, 184);
|
background-color: var(--info-color);
|
||||||
}
|
}
|
||||||
.bg-magenta {
|
.bg-magenta {
|
||||||
background-color: rgb(118, 38, 113);
|
background-color: rgb(118, 38, 113);
|
||||||
|
@ -80,14 +80,14 @@ class HassioCardContent extends LitElement {
|
|||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
ha-svg-icon.update {
|
ha-svg-icon.update {
|
||||||
color: var(--paper-orange-400);
|
color: var(--warning-color);
|
||||||
}
|
}
|
||||||
ha-svg-icon.running,
|
ha-svg-icon.running,
|
||||||
ha-svg-icon.installed {
|
ha-svg-icon.installed {
|
||||||
color: var(--paper-green-400);
|
color: var(--success-color);
|
||||||
}
|
}
|
||||||
ha-svg-icon.hassupdate,
|
ha-svg-icon.hassupdate,
|
||||||
ha-svg-icon.snapshot {
|
ha-svg-icon.backup {
|
||||||
color: var(--paper-item-icon-color);
|
color: var(--paper-item-icon-color);
|
||||||
}
|
}
|
||||||
ha-svg-icon.not_available {
|
ha-svg-icon.not_available {
|
||||||
@ -122,7 +122,7 @@ class HassioCardContent extends LitElement {
|
|||||||
}
|
}
|
||||||
.dot {
|
.dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: var(--paper-orange-400);
|
background-color: var(--warning-color);
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
|
@ -8,23 +8,20 @@ import "../../../src/components/ha-circular-progress";
|
|||||||
import "../../../src/components/ha-file-upload";
|
import "../../../src/components/ha-file-upload";
|
||||||
import "../../../src/components/ha-svg-icon";
|
import "../../../src/components/ha-svg-icon";
|
||||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
import {
|
import { HassioBackup, uploadBackup } from "../../../src/data/hassio/backup";
|
||||||
HassioSnapshot,
|
|
||||||
uploadSnapshot,
|
|
||||||
} from "../../../src/data/hassio/snapshot";
|
|
||||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"snapshot-uploaded": { snapshot: HassioSnapshot };
|
"backup-uploaded": { backup: HassioBackup };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
|
const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
|
||||||
|
|
||||||
@customElement("hassio-upload-snapshot")
|
@customElement("hassio-upload-backup")
|
||||||
export class HassioUploadSnapshot extends LitElement {
|
export class HassioUploadBackup extends LitElement {
|
||||||
public hass!: HomeAssistant;
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() public value: string | null = null;
|
@state() public value: string | null = null;
|
||||||
@ -37,7 +34,7 @@ export class HassioUploadSnapshot extends LitElement {
|
|||||||
.uploading=${this._uploading}
|
.uploading=${this._uploading}
|
||||||
.icon=${mdiFolderUpload}
|
.icon=${mdiFolderUpload}
|
||||||
accept="application/x-tar"
|
accept="application/x-tar"
|
||||||
label="Upload snapshot"
|
label="Upload backup"
|
||||||
@file-picked=${this._uploadFile}
|
@file-picked=${this._uploadFile}
|
||||||
auto-open-file-dialog
|
auto-open-file-dialog
|
||||||
></ha-file-upload>
|
></ha-file-upload>
|
||||||
@ -49,10 +46,10 @@ export class HassioUploadSnapshot extends LitElement {
|
|||||||
|
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Snapshot file is too big",
|
title: "Backup file is too big",
|
||||||
text: html`The maximum allowed filesize is 1GB.<br />
|
text: html`The maximum allowed filesize is 1GB.<br />
|
||||||
<a
|
<a
|
||||||
href="https://www.home-assistant.io/hassio/haos_common_tasks/#restoring-a-snapshot-on-a-new-install"
|
href="https://www.home-assistant.io/hassio/haos_common_tasks/#restoring-a-backup-on-a-new-install"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>Have a look here on how to restore it.</a
|
>Have a look here on how to restore it.</a
|
||||||
>`,
|
>`,
|
||||||
@ -64,15 +61,15 @@ export class HassioUploadSnapshot extends LitElement {
|
|||||||
if (!["application/x-tar"].includes(file.type)) {
|
if (!["application/x-tar"].includes(file.type)) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Unsupported file format",
|
title: "Unsupported file format",
|
||||||
text: "Please choose a Home Assistant snapshot file (.tar)",
|
text: "Please choose a Home Assistant backup file (.tar)",
|
||||||
confirmText: "ok",
|
confirmText: "ok",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._uploading = true;
|
this._uploading = true;
|
||||||
try {
|
try {
|
||||||
const snapshot = await uploadSnapshot(this.hass, file);
|
const backup = await uploadBackup(this.hass, file);
|
||||||
fireEvent(this, "snapshot-uploaded", { snapshot: snapshot.data });
|
fireEvent(this, "backup-uploaded", { backup: backup.data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Upload failed",
|
title: "Upload failed",
|
||||||
@ -87,6 +84,6 @@ export class HassioUploadSnapshot extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"hassio-upload-snapshot": HassioUploadSnapshot;
|
"hassio-upload-backup": HassioUploadBackup;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,10 +11,10 @@ import "../../../src/components/ha-formfield";
|
|||||||
import "../../../src/components/ha-radio";
|
import "../../../src/components/ha-radio";
|
||||||
import type { HaRadio } from "../../../src/components/ha-radio";
|
import type { HaRadio } from "../../../src/components/ha-radio";
|
||||||
import {
|
import {
|
||||||
HassioFullSnapshotCreateParams,
|
HassioFullBackupCreateParams,
|
||||||
HassioPartialSnapshotCreateParams,
|
HassioPartialBackupCreateParams,
|
||||||
HassioSnapshotDetail,
|
HassioBackupDetail,
|
||||||
} from "../../../src/data/hassio/snapshot";
|
} from "../../../src/data/hassio/backup";
|
||||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
@ -64,17 +64,17 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
|
|||||||
}))
|
}))
|
||||||
.sort((a, b) => (a.name > b.name ? 1 : -1));
|
.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||||
|
|
||||||
@customElement("supervisor-snapshot-content")
|
@customElement("supervisor-backup-content")
|
||||||
export class SupervisorSnapshotContent extends LitElement {
|
export class SupervisorBackupContent extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public localize?: LocalizeFunc;
|
@property() public localize?: LocalizeFunc;
|
||||||
|
|
||||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||||
|
|
||||||
@property({ attribute: false }) public snapshot?: HassioSnapshotDetail;
|
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
||||||
|
|
||||||
@property() public snapshotType: HassioSnapshotDetail["type"] = "full";
|
@property() public backupType: HassioBackupDetail["type"] = "full";
|
||||||
|
|
||||||
@property({ attribute: false }) public folders?: CheckboxItem[];
|
@property({ attribute: false }) public folders?: CheckboxItem[];
|
||||||
|
|
||||||
@ -82,37 +82,35 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public homeAssistant = false;
|
@property({ type: Boolean }) public homeAssistant = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public snapshotHasPassword = false;
|
@property({ type: Boolean }) public backupHasPassword = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public onboarding = false;
|
@property({ type: Boolean }) public onboarding = false;
|
||||||
|
|
||||||
@property() public snapshotName = "";
|
@property() public backupName = "";
|
||||||
|
|
||||||
@property() public snapshotPassword = "";
|
@property() public backupPassword = "";
|
||||||
|
|
||||||
@property() public confirmSnapshotPassword = "";
|
@property() public confirmBackupPassword = "";
|
||||||
|
|
||||||
public willUpdate(changedProps) {
|
public willUpdate(changedProps) {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
this.folders = _computeFolders(
|
this.folders = _computeFolders(
|
||||||
this.snapshot
|
this.backup
|
||||||
? this.snapshot.folders
|
? this.backup.folders
|
||||||
: ["homeassistant", "ssl", "share", "media", "addons/local"]
|
: ["homeassistant", "ssl", "share", "media", "addons/local"]
|
||||||
);
|
);
|
||||||
this.addons = _computeAddons(
|
this.addons = _computeAddons(
|
||||||
this.snapshot
|
this.backup ? this.backup.addons : this.supervisor?.supervisor.addons
|
||||||
? this.snapshot.addons
|
|
||||||
: this.supervisor?.supervisor.addons
|
|
||||||
);
|
);
|
||||||
this.snapshotType = this.snapshot?.type || "full";
|
this.backupType = this.backup?.type || "full";
|
||||||
this.snapshotName = this.snapshot?.name || "";
|
this.backupName = this.backup?.name || "";
|
||||||
this.snapshotHasPassword = this.snapshot?.protected || false;
|
this.backupHasPassword = this.backup?.protected || false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _localize = (string: string) =>
|
private _localize = (string: string) =>
|
||||||
this.supervisor?.localize(`snapshot.${string}`) ||
|
this.supervisor?.localize(`backup.${string}`) ||
|
||||||
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
|
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
@ -120,64 +118,64 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
const foldersSection =
|
const foldersSection =
|
||||||
this.snapshotType === "partial" ? this._getSection("folders") : undefined;
|
this.backupType === "partial" ? this._getSection("folders") : undefined;
|
||||||
const addonsSection =
|
const addonsSection =
|
||||||
this.snapshotType === "partial" ? this._getSection("addons") : undefined;
|
this.backupType === "partial" ? this._getSection("addons") : undefined;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.snapshot
|
${this.backup
|
||||||
? html`<div class="details">
|
? html`<div class="details">
|
||||||
${this.snapshot.type === "full"
|
${this.backup.type === "full"
|
||||||
? this._localize("full_snapshot")
|
? this._localize("full_backup")
|
||||||
: this._localize("partial_snapshot")}
|
: this._localize("partial_backup")}
|
||||||
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
|
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||||
${this.hass
|
${this.hass
|
||||||
? formatDateTime(new Date(this.snapshot.date), this.hass.locale)
|
? formatDateTime(new Date(this.backup.date), this.hass.locale)
|
||||||
: this.snapshot.date}
|
: this.backup.date}
|
||||||
</div>`
|
</div>`
|
||||||
: html`<paper-input
|
: html`<paper-input
|
||||||
name="snapshotName"
|
name="backupName"
|
||||||
.label=${this.supervisor?.localize("snapshot.name") || "Name"}
|
.label=${this._localize("name")}
|
||||||
.value=${this.snapshotName}
|
.value=${this.backupName}
|
||||||
@value-changed=${this._handleTextValueChanged}
|
@value-changed=${this._handleTextValueChanged}
|
||||||
>
|
>
|
||||||
</paper-input>`}
|
</paper-input>`}
|
||||||
${!this.snapshot || this.snapshot.type === "full"
|
${!this.backup || this.backup.type === "full"
|
||||||
? html`<div class="sub-header">
|
? html`<div class="sub-header">
|
||||||
${!this.snapshot
|
${!this.backup
|
||||||
? this._localize("type")
|
? this._localize("type")
|
||||||
: this._localize("select_type")}
|
: this._localize("select_type")}
|
||||||
</div>
|
</div>
|
||||||
<div class="snapshot-types">
|
<div class="backup-types">
|
||||||
<ha-formfield .label=${this._localize("full_snapshot")}>
|
<ha-formfield .label=${this._localize("full_backup")}>
|
||||||
<ha-radio
|
<ha-radio
|
||||||
@change=${this._handleRadioValueChanged}
|
@change=${this._handleRadioValueChanged}
|
||||||
value="full"
|
value="full"
|
||||||
name="snapshotType"
|
name="backupType"
|
||||||
.checked=${this.snapshotType === "full"}
|
.checked=${this.backupType === "full"}
|
||||||
>
|
>
|
||||||
</ha-radio>
|
</ha-radio>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
<ha-formfield .label=${this._localize("partial_snapshot")}>
|
<ha-formfield .label=${this._localize("partial_backup")}>
|
||||||
<ha-radio
|
<ha-radio
|
||||||
@change=${this._handleRadioValueChanged}
|
@change=${this._handleRadioValueChanged}
|
||||||
value="partial"
|
value="partial"
|
||||||
name="snapshotType"
|
name="backupType"
|
||||||
.checked=${this.snapshotType === "partial"}
|
.checked=${this.backupType === "partial"}
|
||||||
>
|
>
|
||||||
</ha-radio>
|
</ha-radio>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
</div>`
|
</div>`
|
||||||
: ""}
|
: ""}
|
||||||
${this.snapshotType === "partial"
|
${this.backupType === "partial"
|
||||||
? html`<div class="partial-picker">
|
? html`<div class="partial-picker">
|
||||||
${this.snapshot && this.snapshot.homeassistant
|
${this.backup && this.backup.homeassistant
|
||||||
? html`
|
? html`
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
.label=${html`<supervisor-formfield-label
|
.label=${html`<supervisor-formfield-label
|
||||||
label="Home Assistant"
|
label="Home Assistant"
|
||||||
.iconPath=${mdiHomeAssistant}
|
.iconPath=${mdiHomeAssistant}
|
||||||
.version=${this.snapshot.homeassistant}
|
.version=${this.backup.homeassistant}
|
||||||
>
|
>
|
||||||
</supervisor-formfield-label>`}
|
</supervisor-formfield-label>`}
|
||||||
>
|
>
|
||||||
@ -233,38 +231,38 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
</div> `
|
</div> `
|
||||||
: ""}
|
: ""}
|
||||||
${this.snapshotType === "partial" &&
|
${this.backupType === "partial" &&
|
||||||
(!this.snapshot || this.snapshotHasPassword)
|
(!this.backup || this.backupHasPassword)
|
||||||
? html`<hr />`
|
? html`<hr />`
|
||||||
: ""}
|
: ""}
|
||||||
${!this.snapshot
|
${!this.backup
|
||||||
? html`<ha-formfield
|
? html`<ha-formfield
|
||||||
class="password"
|
class="password"
|
||||||
.label=${this._localize("password_protection")}
|
.label=${this._localize("password_protection")}
|
||||||
>
|
>
|
||||||
<ha-checkbox
|
<ha-checkbox
|
||||||
.checked=${this.snapshotHasPassword}
|
.checked=${this.backupHasPassword}
|
||||||
@change=${this._toggleHasPassword}
|
@change=${this._toggleHasPassword}
|
||||||
>
|
>
|
||||||
</ha-checkbox>
|
</ha-checkbox>
|
||||||
</ha-formfield>`
|
</ha-formfield>`
|
||||||
: ""}
|
: ""}
|
||||||
${this.snapshotHasPassword
|
${this.backupHasPassword
|
||||||
? html`
|
? html`
|
||||||
<paper-input
|
<paper-input
|
||||||
.label=${this._localize("password")}
|
.label=${this._localize("password")}
|
||||||
type="password"
|
type="password"
|
||||||
name="snapshotPassword"
|
name="backupPassword"
|
||||||
.value=${this.snapshotPassword}
|
.value=${this.backupPassword}
|
||||||
@value-changed=${this._handleTextValueChanged}
|
@value-changed=${this._handleTextValueChanged}
|
||||||
>
|
>
|
||||||
</paper-input>
|
</paper-input>
|
||||||
${!this.snapshot
|
${!this.backup
|
||||||
? html` <paper-input
|
? html` <paper-input
|
||||||
.label=${this.supervisor?.localize("confirm_password")}
|
.label=${this._localize("confirm_password")}
|
||||||
type="password"
|
type="password"
|
||||||
name="confirmSnapshotPassword"
|
name="confirmBackupPassword"
|
||||||
.value=${this.confirmSnapshotPassword}
|
.value=${this.confirmBackupPassword}
|
||||||
@value-changed=${this._handleTextValueChanged}
|
@value-changed=${this._handleTextValueChanged}
|
||||||
>
|
>
|
||||||
</paper-input>`
|
</paper-input>`
|
||||||
@ -307,7 +305,7 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
margin: 0 -14px -16px;
|
margin: 0 -14px -16px;
|
||||||
}
|
}
|
||||||
.snapshot-types {
|
.backup-types {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-left: -13px;
|
margin-left: -13px;
|
||||||
}
|
}
|
||||||
@ -317,23 +315,23 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public snapshotDetails():
|
public backupDetails():
|
||||||
| HassioPartialSnapshotCreateParams
|
| HassioPartialBackupCreateParams
|
||||||
| HassioFullSnapshotCreateParams {
|
| HassioFullBackupCreateParams {
|
||||||
const data: any = {};
|
const data: any = {};
|
||||||
|
|
||||||
if (!this.snapshot) {
|
if (!this.backup) {
|
||||||
data.name = this.snapshotName || formatDate(new Date(), this.hass.locale);
|
data.name = this.backupName || formatDate(new Date(), this.hass.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.snapshotHasPassword) {
|
if (this.backupHasPassword) {
|
||||||
data.password = this.snapshotPassword;
|
data.password = this.backupPassword;
|
||||||
if (!this.snapshot) {
|
if (!this.backup) {
|
||||||
data.confirm_password = this.confirmSnapshotPassword;
|
data.confirm_password = this.confirmBackupPassword;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.snapshotType === "full") {
|
if (this.backupType === "full") {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,7 +413,7 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _toggleHasPassword(): void {
|
private _toggleHasPassword(): void {
|
||||||
this.snapshotHasPassword = !this.snapshotHasPassword;
|
this.backupHasPassword = !this.backupHasPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleSection(ev): void {
|
private _toggleSection(ev): void {
|
||||||
@ -445,6 +443,6 @@ export class SupervisorSnapshotContent extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"supervisor-snapshot-content": SupervisorSnapshotContent;
|
"supervisor-backup-content": SupervisorBackupContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -162,7 +162,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
name: "Home Assistant Core",
|
name: "Home Assistant Core",
|
||||||
version: this.supervisor.core.version_latest,
|
version: this.supervisor.core.version_latest,
|
||||||
snapshotParams: {
|
backupParams: {
|
||||||
name: `core_${this.supervisor.core.version}`,
|
name: `core_${this.supervisor.core.version}`,
|
||||||
folders: ["homeassistant"],
|
folders: ["homeassistant"],
|
||||||
homeassistant: true,
|
homeassistant: true,
|
||||||
|
@ -6,20 +6,20 @@ import "../../../../src/components/ha-header-bar";
|
|||||||
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
import "../../components/hassio-upload-snapshot";
|
import "../../components/hassio-upload-backup";
|
||||||
import { HassioSnapshotUploadDialogParams } from "./show-dialog-snapshot-upload";
|
import { HassioBackupUploadDialogParams } from "./show-dialog-backup-upload";
|
||||||
|
|
||||||
@customElement("dialog-hassio-snapshot-upload")
|
@customElement("dialog-hassio-backup-upload")
|
||||||
export class DialogHassioSnapshotUpload
|
export class DialogHassioBackupUpload
|
||||||
extends LitElement
|
extends LitElement
|
||||||
implements HassDialog<HassioSnapshotUploadDialogParams>
|
implements HassDialog<HassioBackupUploadDialogParams>
|
||||||
{
|
{
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _params?: HassioSnapshotUploadDialogParams;
|
@state() private _params?: HassioBackupUploadDialogParams;
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
params: HassioSnapshotUploadDialogParams
|
params: HassioBackupUploadDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
@ -27,8 +27,8 @@ export class DialogHassioSnapshotUpload
|
|||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
if (this._params && !this._params.onboarding) {
|
if (this._params && !this._params.onboarding) {
|
||||||
if (this._params.reloadSnapshot) {
|
if (this._params.reloadBackup) {
|
||||||
this._params.reloadSnapshot();
|
this._params.reloadBackup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
@ -51,23 +51,23 @@ export class DialogHassioSnapshotUpload
|
|||||||
>
|
>
|
||||||
<div slot="heading">
|
<div slot="heading">
|
||||||
<ha-header-bar>
|
<ha-header-bar>
|
||||||
<span slot="title"> Upload snapshot </span>
|
<span slot="title"> Upload backup </span>
|
||||||
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
||||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
</ha-header-bar>
|
</ha-header-bar>
|
||||||
</div>
|
</div>
|
||||||
<hassio-upload-snapshot
|
<hassio-upload-backup
|
||||||
@snapshot-uploaded=${this._snapshotUploaded}
|
@backup-uploaded=${this._backupUploaded}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
></hassio-upload-snapshot>
|
></hassio-upload-backup>
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _snapshotUploaded(ev) {
|
private _backupUploaded(ev) {
|
||||||
const snapshot = ev.detail.snapshot;
|
const backup = ev.detail.backup;
|
||||||
this._params?.showSnapshot(snapshot.slug);
|
this._params?.showBackup(backup.slug);
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +94,6 @@ export class DialogHassioSnapshotUpload
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"dialog-hassio-snapshot-upload": DialogHassioSnapshotUpload;
|
"dialog-hassio-backup-upload": DialogHassioBackupUpload;
|
||||||
}
|
}
|
||||||
}
|
}
|
143
hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts → hassio/src/dialogs/backup/dialog-hassio-backup.ts
Executable file → Normal file
143
hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts → hassio/src/dialogs/backup/dialog-hassio-backup.ts
Executable file → Normal file
@ -6,15 +6,16 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import { slugify } from "../../../../src/common/string/slugify";
|
import { slugify } from "../../../../src/common/string/slugify";
|
||||||
import "../../../../src/components/buttons/ha-progress-button";
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-button-menu";
|
import "../../../../src/components/ha-button-menu";
|
||||||
import "../../../../src/components/ha-header-bar";
|
import "../../../../src/components/ha-header-bar";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-svg-icon";
|
||||||
import { getSignedPath } from "../../../../src/data/auth";
|
import { getSignedPath } from "../../../../src/data/auth";
|
||||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
fetchHassioSnapshotInfo,
|
fetchHassioBackupInfo,
|
||||||
HassioSnapshotDetail,
|
HassioBackupDetail,
|
||||||
} from "../../../../src/data/hassio/snapshot";
|
} from "../../../../src/data/hassio/backup";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
@ -23,44 +24,45 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
|||||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import { fileDownload } from "../../../../src/util/file_download";
|
import { fileDownload } from "../../../../src/util/file_download";
|
||||||
import "../../components/supervisor-snapshot-content";
|
import "../../components/supervisor-backup-content";
|
||||||
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
|
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||||
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
|
import { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
||||||
|
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||||
|
|
||||||
@customElement("dialog-hassio-snapshot")
|
@customElement("dialog-hassio-backup")
|
||||||
class HassioSnapshotDialog
|
class HassioBackupDialog
|
||||||
extends LitElement
|
extends LitElement
|
||||||
implements HassDialog<HassioSnapshotDialogParams>
|
implements HassDialog<HassioBackupDialogParams>
|
||||||
{
|
{
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _snapshot?: HassioSnapshotDetail;
|
@state() private _backup?: HassioBackupDetail;
|
||||||
|
|
||||||
@state() private _dialogParams?: HassioSnapshotDialogParams;
|
@state() private _dialogParams?: HassioBackupDialogParams;
|
||||||
|
|
||||||
@state() private _restoringSnapshot = false;
|
@state() private _restoringBackup = false;
|
||||||
|
|
||||||
@query("supervisor-snapshot-content")
|
@query("supervisor-backup-content")
|
||||||
private _snapshotContent!: SupervisorSnapshotContent;
|
private _backupContent!: SupervisorBackupContent;
|
||||||
|
|
||||||
public async showDialog(params: HassioSnapshotDialogParams) {
|
public async showDialog(params: HassioBackupDialogParams) {
|
||||||
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
|
this._backup = await fetchHassioBackupInfo(this.hass, params.slug);
|
||||||
this._dialogParams = params;
|
this._dialogParams = params;
|
||||||
this._restoringSnapshot = false;
|
this._restoringBackup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this._snapshot = undefined;
|
this._backup = undefined;
|
||||||
this._dialogParams = undefined;
|
this._dialogParams = undefined;
|
||||||
this._restoringSnapshot = false;
|
this._restoringBackup = false;
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this._dialogParams || !this._snapshot) {
|
if (!this._dialogParams || !this._backup) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
@ -72,26 +74,28 @@ class HassioSnapshotDialog
|
|||||||
>
|
>
|
||||||
<div slot="heading">
|
<div slot="heading">
|
||||||
<ha-header-bar>
|
<ha-header-bar>
|
||||||
<span slot="title">${this._snapshot.name}</span>
|
<span slot="title">${this._backup.name}</span>
|
||||||
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
||||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
</ha-header-bar>
|
</ha-header-bar>
|
||||||
</div>
|
</div>
|
||||||
${this._restoringSnapshot
|
${this._restoringBackup
|
||||||
? html` <ha-circular-progress active></ha-circular-progress>`
|
? html` <ha-circular-progress active></ha-circular-progress>`
|
||||||
: html`<supervisor-snapshot-content
|
: html`<supervisor-backup-content
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.supervisor=${this._dialogParams.supervisor}
|
.supervisor=${this._dialogParams.supervisor}
|
||||||
.snapshot=${this._snapshot}
|
.backup=${this._backup}
|
||||||
.onboarding=${this._dialogParams.onboarding || false}
|
.onboarding=${this._dialogParams.onboarding || false}
|
||||||
.localize=${this._dialogParams.localize}
|
.localize=${this._dialogParams.localize}
|
||||||
>
|
>
|
||||||
</supervisor-snapshot-content>`}
|
</supervisor-backup-content>`}
|
||||||
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
|
|
||||||
<mwc-button
|
<mwc-button
|
||||||
.disabled=${this._restoringSnapshot}
|
.disabled=${this._restoringBackup}
|
||||||
slot="secondaryAction"
|
slot="secondaryAction"
|
||||||
@click=${this._restoreClicked}
|
@click=${this._restoreClicked}
|
||||||
>
|
>
|
||||||
@ -108,8 +112,8 @@ class HassioSnapshotDialog
|
|||||||
<mwc-icon-button slot="trigger" alt="menu">
|
<mwc-icon-button slot="trigger" alt="menu">
|
||||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
<mwc-list-item>Download Snapshot</mwc-list-item>
|
<mwc-list-item>Download Backup</mwc-list-item>
|
||||||
<mwc-list-item class="error">Delete Snapshot</mwc-list-item>
|
<mwc-list-item class="error">Delete Backup</mwc-list-item>
|
||||||
</ha-button-menu>`
|
</ha-button-menu>`
|
||||||
: ""}
|
: ""}
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
@ -150,30 +154,30 @@ class HassioSnapshotDialog
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _restoreClicked() {
|
private async _restoreClicked() {
|
||||||
const snapshotDetails = this._snapshotContent.snapshotDetails();
|
const backupDetails = this._backupContent.backupDetails();
|
||||||
this._restoringSnapshot = true;
|
this._restoringBackup = true;
|
||||||
if (this._snapshotContent.snapshotType === "full") {
|
if (this._backupContent.backupType === "full") {
|
||||||
await this._fullRestoreClicked(snapshotDetails);
|
await this._fullRestoreClicked(backupDetails);
|
||||||
} else {
|
} else {
|
||||||
await this._partialRestoreClicked(snapshotDetails);
|
await this._partialRestoreClicked(backupDetails);
|
||||||
}
|
}
|
||||||
this._restoringSnapshot = false;
|
this._restoringBackup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _partialRestoreClicked(snapshotDetails) {
|
private async _partialRestoreClicked(backupDetails) {
|
||||||
if (
|
if (
|
||||||
this._dialogParams?.supervisor !== undefined &&
|
this._dialogParams?.supervisor !== undefined &&
|
||||||
this._dialogParams?.supervisor.info.state !== "running"
|
this._dialogParams?.supervisor.info.state !== "running"
|
||||||
) {
|
) {
|
||||||
await showAlertDialog(this, {
|
await showAlertDialog(this, {
|
||||||
title: "Could not restore snapshot",
|
title: "Could not restore backup",
|
||||||
text: `Restoring a snapshot is not possible right now because the system is in ${this._dialogParams?.supervisor.info.state} state.`,
|
text: `Restoring a backup is not possible right now because the system is in ${this._dialogParams?.supervisor.info.state} state.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!(await showConfirmationDialog(this, {
|
!(await showConfirmationDialog(this, {
|
||||||
title: "Are you sure you want partially to restore this snapshot?",
|
title: "Are you sure you want partially to restore this backup?",
|
||||||
confirmText: "restore",
|
confirmText: "restore",
|
||||||
dismissText: "cancel",
|
dismissText: "cancel",
|
||||||
}))
|
}))
|
||||||
@ -186,8 +190,12 @@ class HassioSnapshotDialog
|
|||||||
.callApi(
|
.callApi(
|
||||||
"POST",
|
"POST",
|
||||||
|
|
||||||
`hassio/snapshots/${this._snapshot!.slug}/restore/partial`,
|
`hassio/${
|
||||||
snapshotDetails
|
atLeastVersion(this.hass.config.version, 2021, 9)
|
||||||
|
? "backups"
|
||||||
|
: "snapshots"
|
||||||
|
}/${this._backup!.slug}/restore/partial`,
|
||||||
|
backupDetails
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
@ -199,29 +207,29 @@ class HassioSnapshotDialog
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
fireEvent(this, "restoring");
|
fireEvent(this, "restoring");
|
||||||
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/partial`, {
|
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(snapshotDetails),
|
body: JSON.stringify(backupDetails),
|
||||||
});
|
});
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fullRestoreClicked(snapshotDetails) {
|
private async _fullRestoreClicked(backupDetails) {
|
||||||
if (
|
if (
|
||||||
this._dialogParams?.supervisor !== undefined &&
|
this._dialogParams?.supervisor !== undefined &&
|
||||||
this._dialogParams?.supervisor.info.state !== "running"
|
this._dialogParams?.supervisor.info.state !== "running"
|
||||||
) {
|
) {
|
||||||
await showAlertDialog(this, {
|
await showAlertDialog(this, {
|
||||||
title: "Could not restore snapshot",
|
title: "Could not restore backup",
|
||||||
text: `Restoring a snapshot is not possible right now because the system is in ${this._dialogParams?.supervisor.info.state} state.`,
|
text: `Restoring a backup is not possible right now because the system is in ${this._dialogParams?.supervisor.info.state} state.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!(await showConfirmationDialog(this, {
|
!(await showConfirmationDialog(this, {
|
||||||
title:
|
title:
|
||||||
"Are you sure you want to wipe your system and restore this snapshot?",
|
"Are you sure you want to wipe your system and restore this backup?",
|
||||||
confirmText: "restore",
|
confirmText: "restore",
|
||||||
dismissText: "cancel",
|
dismissText: "cancel",
|
||||||
}))
|
}))
|
||||||
@ -233,8 +241,12 @@ class HassioSnapshotDialog
|
|||||||
this.hass
|
this.hass
|
||||||
.callApi(
|
.callApi(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/snapshots/${this._snapshot!.slug}/restore/full`,
|
`hassio/${
|
||||||
snapshotDetails
|
atLeastVersion(this.hass.config.version, 2021, 9)
|
||||||
|
? "backups"
|
||||||
|
: "snapshots"
|
||||||
|
}/${this._backup!.slug}/restore/full`,
|
||||||
|
backupDetails
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
@ -246,9 +258,9 @@ class HassioSnapshotDialog
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
fireEvent(this, "restoring");
|
fireEvent(this, "restoring");
|
||||||
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/full`, {
|
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(snapshotDetails),
|
body: JSON.stringify(backupDetails),
|
||||||
});
|
});
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
@ -257,7 +269,7 @@ class HassioSnapshotDialog
|
|||||||
private async _deleteClicked() {
|
private async _deleteClicked() {
|
||||||
if (
|
if (
|
||||||
!(await showConfirmationDialog(this, {
|
!(await showConfirmationDialog(this, {
|
||||||
title: "Are you sure you want to delete this snapshot?",
|
title: "Are you sure you want to delete this backup?",
|
||||||
confirmText: "delete",
|
confirmText: "delete",
|
||||||
dismissText: "cancel",
|
dismissText: "cancel",
|
||||||
}))
|
}))
|
||||||
@ -267,7 +279,14 @@ class HassioSnapshotDialog
|
|||||||
|
|
||||||
this.hass
|
this.hass
|
||||||
|
|
||||||
.callApi("POST", `hassio/snapshots/${this._snapshot!.slug}/remove`)
|
.callApi(
|
||||||
|
atLeastVersion(this.hass.config.version, 2021, 9) ? "DELETE" : "POST",
|
||||||
|
`hassio/${
|
||||||
|
atLeastVersion(this.hass.config.version, 2021, 9)
|
||||||
|
? `backups/${this._backup!.slug}`
|
||||||
|
: `snapshots/${this._backup!.slug}/remove`
|
||||||
|
}`
|
||||||
|
)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
if (this._dialogParams!.onDelete) {
|
if (this._dialogParams!.onDelete) {
|
||||||
@ -286,7 +305,11 @@ class HassioSnapshotDialog
|
|||||||
try {
|
try {
|
||||||
signedPath = await getSignedPath(
|
signedPath = await getSignedPath(
|
||||||
this.hass,
|
this.hass,
|
||||||
`/api/hassio/snapshots/${this._snapshot!.slug}/download`
|
`/api/hassio/${
|
||||||
|
atLeastVersion(this.hass.config.version, 2021, 9)
|
||||||
|
? "backups"
|
||||||
|
: "snapshots"
|
||||||
|
}/${this._backup!.slug}/download`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await showAlertDialog(this, {
|
await showAlertDialog(this, {
|
||||||
@ -298,7 +321,7 @@ class HassioSnapshotDialog
|
|||||||
if (window.location.href.includes("ui.nabu.casa")) {
|
if (window.location.href.includes("ui.nabu.casa")) {
|
||||||
const confirm = await showConfirmationDialog(this, {
|
const confirm = await showConfirmationDialog(this, {
|
||||||
title: "Potential slow download",
|
title: "Potential slow download",
|
||||||
text: "Downloading snapshots over the Nabu Casa URL will take some time, it is recomended to use your local URL instead, do you want to continue?",
|
text: "Downloading backups over the Nabu Casa URL will take some time, it is recomended to use your local URL instead, do you want to continue?",
|
||||||
confirmText: "continue",
|
confirmText: "continue",
|
||||||
dismissText: "cancel",
|
dismissText: "cancel",
|
||||||
});
|
});
|
||||||
@ -310,19 +333,19 @@ class HassioSnapshotDialog
|
|||||||
fileDownload(
|
fileDownload(
|
||||||
this,
|
this,
|
||||||
signedPath.path,
|
signedPath.path,
|
||||||
`home_assistant_snapshot_${slugify(this._computeName)}.tar`
|
`home_assistant_backup_${slugify(this._computeName)}.tar`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _computeName() {
|
private get _computeName() {
|
||||||
return this._snapshot
|
return this._backup
|
||||||
? this._snapshot.name || this._snapshot.slug
|
? this._backup.name || this._backup.slug
|
||||||
: "Unnamed snapshot";
|
: "Unnamed backup";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"dialog-hassio-snapshot": HassioSnapshotDialog;
|
"dialog-hassio-backup": HassioBackupDialog;
|
||||||
}
|
}
|
||||||
}
|
}
|
85
hassio/src/dialogs/snapshot/dialog-hassio-create-snapshot.ts → hassio/src/dialogs/backup/dialog-hassio-create-backup.ts
Executable file → Normal file
85
hassio/src/dialogs/snapshot/dialog-hassio-create-snapshot.ts → hassio/src/dialogs/backup/dialog-hassio-create-backup.ts
Executable file → Normal file
@ -2,41 +2,42 @@ import "@material/mwc-button";
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/buttons/ha-progress-button";
|
import "../../../../src/components/buttons/ha-progress-button";
|
||||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
createHassioFullSnapshot,
|
createHassioFullBackup,
|
||||||
createHassioPartialSnapshot,
|
createHassioPartialBackup,
|
||||||
} from "../../../../src/data/hassio/snapshot";
|
} from "../../../../src/data/hassio/backup";
|
||||||
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import "../../components/supervisor-snapshot-content";
|
import "../../components/supervisor-backup-content";
|
||||||
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
|
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||||
import { HassioCreateSnapshotDialogParams } from "./show-dialog-hassio-create-snapshot";
|
import { HassioCreateBackupDialogParams } from "./show-dialog-hassio-create-backup";
|
||||||
|
|
||||||
@customElement("dialog-hassio-create-snapshot")
|
@customElement("dialog-hassio-create-backup")
|
||||||
class HassioCreateSnapshotDialog extends LitElement {
|
class HassioCreateBackupDialog extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _dialogParams?: HassioCreateSnapshotDialogParams;
|
@state() private _dialogParams?: HassioCreateBackupDialogParams;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _creatingSnapshot = false;
|
@state() private _creatingBackup = false;
|
||||||
|
|
||||||
@query("supervisor-snapshot-content")
|
@query("supervisor-backup-content")
|
||||||
private _snapshotContent!: SupervisorSnapshotContent;
|
private _backupContent!: SupervisorBackupContent;
|
||||||
|
|
||||||
public showDialog(params: HassioCreateSnapshotDialogParams) {
|
public showDialog(params: HassioCreateBackupDialogParams) {
|
||||||
this._dialogParams = params;
|
this._dialogParams = params;
|
||||||
this._creatingSnapshot = false;
|
this._creatingBackup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this._dialogParams = undefined;
|
this._dialogParams = undefined;
|
||||||
this._creatingSnapshot = false;
|
this._creatingBackup = false;
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
@ -52,74 +53,76 @@ class HassioCreateSnapshotDialog extends LitElement {
|
|||||||
@closed=${this.closeDialog}
|
@closed=${this.closeDialog}
|
||||||
.heading=${createCloseHeading(
|
.heading=${createCloseHeading(
|
||||||
this.hass,
|
this.hass,
|
||||||
this._dialogParams.supervisor.localize("snapshot.create_snapshot")
|
this._dialogParams.supervisor.localize("backup.create_backup")
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
${this._creatingSnapshot
|
${this._creatingBackup
|
||||||
? html` <ha-circular-progress active></ha-circular-progress>`
|
? html` <ha-circular-progress active></ha-circular-progress>`
|
||||||
: html`<supervisor-snapshot-content
|
: html`<supervisor-backup-content
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.supervisor=${this._dialogParams.supervisor}
|
.supervisor=${this._dialogParams.supervisor}
|
||||||
>
|
>
|
||||||
</supervisor-snapshot-content>`}
|
</supervisor-backup-content>`}
|
||||||
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||||
${this._dialogParams.supervisor.localize("common.close")}
|
${this._dialogParams.supervisor.localize("common.close")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
<mwc-button
|
<mwc-button
|
||||||
.disabled=${this._creatingSnapshot}
|
.disabled=${this._creatingBackup}
|
||||||
slot="primaryAction"
|
slot="primaryAction"
|
||||||
@click=${this._createSnapshot}
|
@click=${this._createBackup}
|
||||||
>
|
>
|
||||||
${this._dialogParams.supervisor.localize("snapshot.create")}
|
${this._dialogParams.supervisor.localize("backup.create")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createSnapshot(): Promise<void> {
|
private async _createBackup(): Promise<void> {
|
||||||
if (this._dialogParams!.supervisor.info.state !== "running") {
|
if (this._dialogParams!.supervisor.info.state !== "running") {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this._dialogParams!.supervisor.localize(
|
title: this._dialogParams!.supervisor.localize(
|
||||||
"snapshot.could_not_create"
|
"backup.could_not_create"
|
||||||
),
|
),
|
||||||
text: this._dialogParams!.supervisor.localize(
|
text: this._dialogParams!.supervisor.localize(
|
||||||
"snapshot.create_blocked_not_running",
|
"backup.create_blocked_not_running",
|
||||||
"state",
|
"state",
|
||||||
this._dialogParams!.supervisor.info.state
|
this._dialogParams!.supervisor.info.state
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const snapshotDetails = this._snapshotContent.snapshotDetails();
|
const backupDetails = this._backupContent.backupDetails();
|
||||||
this._creatingSnapshot = true;
|
this._creatingBackup = true;
|
||||||
|
|
||||||
this._error = "";
|
this._error = "";
|
||||||
if (snapshotDetails.password && !snapshotDetails.password.length) {
|
if (backupDetails.password && !backupDetails.password.length) {
|
||||||
this._error = this._dialogParams!.supervisor.localize(
|
this._error = this._dialogParams!.supervisor.localize(
|
||||||
"snapshot.enter_password"
|
"backup.enter_password"
|
||||||
);
|
);
|
||||||
this._creatingSnapshot = false;
|
this._creatingBackup = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
snapshotDetails.password &&
|
backupDetails.password &&
|
||||||
snapshotDetails.password !== snapshotDetails.confirm_password
|
backupDetails.password !== backupDetails.confirm_password
|
||||||
) {
|
) {
|
||||||
this._error = this._dialogParams!.supervisor.localize(
|
this._error = this._dialogParams!.supervisor.localize(
|
||||||
"snapshot.passwords_not_matching"
|
"backup.passwords_not_matching"
|
||||||
);
|
);
|
||||||
this._creatingSnapshot = false;
|
this._creatingBackup = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete snapshotDetails.confirm_password;
|
delete backupDetails.confirm_password;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this._snapshotContent.snapshotType === "full") {
|
if (this._backupContent.backupType === "full") {
|
||||||
await createHassioFullSnapshot(this.hass, snapshotDetails);
|
await createHassioFullBackup(this.hass, backupDetails);
|
||||||
} else {
|
} else {
|
||||||
await createHassioPartialSnapshot(this.hass, snapshotDetails);
|
await createHassioPartialBackup(this.hass, backupDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._dialogParams!.onCreate();
|
this._dialogParams!.onCreate();
|
||||||
@ -127,7 +130,7 @@ class HassioCreateSnapshotDialog extends LitElement {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = extractApiErrorMessage(err);
|
this._error = extractApiErrorMessage(err);
|
||||||
}
|
}
|
||||||
this._creatingSnapshot = false;
|
this._creatingBackup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@ -146,6 +149,6 @@ class HassioCreateSnapshotDialog extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"dialog-hassio-create-snapshot": HassioCreateSnapshotDialog;
|
"dialog-hassio-create-backup": HassioCreateBackupDialog;
|
||||||
}
|
}
|
||||||
}
|
}
|
19
hassio/src/dialogs/backup/show-dialog-backup-upload.ts
Normal file
19
hassio/src/dialogs/backup/show-dialog-backup-upload.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import "./dialog-hassio-backup-upload";
|
||||||
|
|
||||||
|
export interface HassioBackupUploadDialogParams {
|
||||||
|
showBackup: (slug: string) => void;
|
||||||
|
reloadBackup?: () => Promise<void>;
|
||||||
|
onboarding?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showBackupUploadDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: HassioBackupUploadDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-hassio-backup-upload",
|
||||||
|
dialogImport: () => import("./dialog-hassio-backup-upload"),
|
||||||
|
dialogParams,
|
||||||
|
});
|
||||||
|
};
|
@ -2,7 +2,7 @@ import { fireEvent } from "../../../../src/common/dom/fire_event";
|
|||||||
import { LocalizeFunc } from "../../../../src/common/translations/localize";
|
import { LocalizeFunc } from "../../../../src/common/translations/localize";
|
||||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
|
|
||||||
export interface HassioSnapshotDialogParams {
|
export interface HassioBackupDialogParams {
|
||||||
slug: string;
|
slug: string;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onboarding?: boolean;
|
onboarding?: boolean;
|
||||||
@ -10,13 +10,13 @@ export interface HassioSnapshotDialogParams {
|
|||||||
localize?: LocalizeFunc;
|
localize?: LocalizeFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showHassioSnapshotDialog = (
|
export const showHassioBackupDialog = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
dialogParams: HassioSnapshotDialogParams
|
dialogParams: HassioBackupDialogParams
|
||||||
): void => {
|
): void => {
|
||||||
fireEvent(element, "show-dialog", {
|
fireEvent(element, "show-dialog", {
|
||||||
dialogTag: "dialog-hassio-snapshot",
|
dialogTag: "dialog-hassio-backup",
|
||||||
dialogImport: () => import("./dialog-hassio-snapshot"),
|
dialogImport: () => import("./dialog-hassio-backup"),
|
||||||
dialogParams,
|
dialogParams,
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,18 +1,18 @@
|
|||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
|
|
||||||
export interface HassioCreateSnapshotDialogParams {
|
export interface HassioCreateBackupDialogParams {
|
||||||
supervisor: Supervisor;
|
supervisor: Supervisor;
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showHassioCreateSnapshotDialog = (
|
export const showHassioCreateBackupDialog = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
dialogParams: HassioCreateSnapshotDialogParams
|
dialogParams: HassioCreateBackupDialogParams
|
||||||
): void => {
|
): void => {
|
||||||
fireEvent(element, "show-dialog", {
|
fireEvent(element, "show-dialog", {
|
||||||
dialogTag: "dialog-hassio-create-snapshot",
|
dialogTag: "dialog-hassio-create-backup",
|
||||||
dialogImport: () => import("./dialog-hassio-create-snapshot"),
|
dialogImport: () => import("./dialog-hassio-create-backup"),
|
||||||
dialogParams,
|
dialogParams,
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -10,6 +10,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { cache } from "lit/directives/cache";
|
import { cache } from "lit/directives/cache";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-circular-progress";
|
import "../../../../src/components/ha-circular-progress";
|
||||||
import "../../../../src/components/ha-dialog";
|
import "../../../../src/components/ha-dialog";
|
||||||
import "../../../../src/components/ha-expansion-panel";
|
import "../../../../src/components/ha-expansion-panel";
|
||||||
@ -251,9 +252,9 @@ export class DialogHassioNetwork
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${this._dirty
|
${this._dirty
|
||||||
? html`<div class="warning">
|
? html`<ha-alert alert-type="warning">
|
||||||
${this.supervisor.localize("dialog.network.warning")}
|
${this.supervisor.localize("dialog.network.warning")}
|
||||||
</div>`
|
</ha-alert>`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
@ -9,6 +9,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-circular-progress";
|
import "../../../../src/components/ha-circular-progress";
|
||||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-svg-icon";
|
||||||
@ -75,7 +76,9 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
this._dialogParams!.supervisor.localize("dialog.repositories.title")
|
this._dialogParams!.supervisor.localize("dialog.repositories.title")
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
<div class="form">
|
<div class="form">
|
||||||
${repositories.length
|
${repositories.length
|
||||||
? repositories.map(
|
? repositories.map(
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
|
||||||
import "./dialog-hassio-snapshot-upload";
|
|
||||||
|
|
||||||
export interface HassioSnapshotUploadDialogParams {
|
|
||||||
showSnapshot: (slug: string) => void;
|
|
||||||
reloadSnapshot?: () => Promise<void>;
|
|
||||||
onboarding?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const showSnapshotUploadDialog = (
|
|
||||||
element: HTMLElement,
|
|
||||||
dialogParams: HassioSnapshotUploadDialogParams
|
|
||||||
): void => {
|
|
||||||
fireEvent(element, "show-dialog", {
|
|
||||||
dialogTag: "dialog-hassio-snapshot-upload",
|
|
||||||
dialogImport: () => import("./dialog-hassio-snapshot-upload"),
|
|
||||||
dialogParams,
|
|
||||||
});
|
|
||||||
};
|
|
@ -2,6 +2,7 @@ import "@material/mwc-button/mwc-button";
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, state } from "lit/decorators";
|
import { customElement, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
|
import "../../../../src/components/ha-alert";
|
||||||
import "../../../../src/components/ha-circular-progress";
|
import "../../../../src/components/ha-circular-progress";
|
||||||
import "../../../../src/components/ha-dialog";
|
import "../../../../src/components/ha-dialog";
|
||||||
import "../../../../src/components/ha-settings-row";
|
import "../../../../src/components/ha-settings-row";
|
||||||
@ -11,7 +12,7 @@ import {
|
|||||||
extractApiErrorMessage,
|
extractApiErrorMessage,
|
||||||
ignoreSupervisorError,
|
ignoreSupervisorError,
|
||||||
} from "../../../../src/data/hassio/common";
|
} from "../../../../src/data/hassio/common";
|
||||||
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
|
import { createHassioPartialBackup } from "../../../../src/data/hassio/backup";
|
||||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
|
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
|
||||||
@ -22,9 +23,9 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _opened = false;
|
||||||
|
|
||||||
@state() private _createSnapshot = true;
|
@state() private _createBackup = true;
|
||||||
|
|
||||||
@state() private _action: "snapshot" | "update" | null = null;
|
@state() private _action: "backup" | "update" | null = null;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this._action = null;
|
this._action = null;
|
||||||
this._createSnapshot = true;
|
this._createBackup = true;
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._dialogParams = undefined;
|
this._dialogParams = undefined;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
@ -84,20 +85,20 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
<ha-settings-row>
|
<ha-settings-row>
|
||||||
<span slot="heading">
|
<span slot="heading">
|
||||||
${this._dialogParams.supervisor.localize(
|
${this._dialogParams.supervisor.localize(
|
||||||
"dialog.update.snapshot"
|
"dialog.update.backup"
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span slot="description">
|
<span slot="description">
|
||||||
${this._dialogParams.supervisor.localize(
|
${this._dialogParams.supervisor.localize(
|
||||||
"dialog.update.create_snapshot",
|
"dialog.update.create_backup",
|
||||||
"name",
|
"name",
|
||||||
this._dialogParams.name
|
this._dialogParams.name
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<ha-switch
|
<ha-switch
|
||||||
.checked=${this._createSnapshot}
|
.checked=${this._createBackup}
|
||||||
haptic
|
haptic
|
||||||
@click=${this._toggleSnapshot}
|
@click=${this._toggleBackup}
|
||||||
>
|
>
|
||||||
</ha-switch>
|
</ha-switch>
|
||||||
</ha-settings-row>
|
</ha-settings-row>
|
||||||
@ -123,27 +124,29 @@ class DialogSupervisorUpdate extends LitElement {
|
|||||||
this._dialogParams.version
|
this._dialogParams.version
|
||||||
)
|
)
|
||||||
: this._dialogParams.supervisor.localize(
|
: this._dialogParams.supervisor.localize(
|
||||||
"dialog.update.snapshotting",
|
"dialog.update.creating_backup",
|
||||||
"name",
|
"name",
|
||||||
this._dialogParams.name
|
this._dialogParams.name
|
||||||
)}
|
)}
|
||||||
</p>`}
|
</p>`}
|
||||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleSnapshot() {
|
private _toggleBackup() {
|
||||||
this._createSnapshot = !this._createSnapshot;
|
this._createBackup = !this._createBackup;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _update() {
|
private async _update() {
|
||||||
if (this._createSnapshot) {
|
if (this._createBackup) {
|
||||||
this._action = "snapshot";
|
this._action = "backup";
|
||||||
try {
|
try {
|
||||||
await createHassioPartialSnapshot(
|
await createHassioPartialBackup(
|
||||||
this.hass,
|
this.hass,
|
||||||
this._dialogParams!.snapshotParams
|
this._dialogParams!.backupParams
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = extractApiErrorMessage(err);
|
this._error = extractApiErrorMessage(err);
|
||||||
|
@ -5,7 +5,7 @@ export interface SupervisorDialogSupervisorUpdateParams {
|
|||||||
supervisor: Supervisor;
|
supervisor: Supervisor;
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
snapshotParams: any;
|
backupParams: any;
|
||||||
updateHandler: () => Promise<void>;
|
updateHandler: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,10 @@ const REDIRECTS: Redirects = {
|
|||||||
redirect: "/hassio/system",
|
redirect: "/hassio/system",
|
||||||
},
|
},
|
||||||
supervisor_snapshots: {
|
supervisor_snapshots: {
|
||||||
redirect: "/hassio/snapshots",
|
redirect: "/hassio/backups",
|
||||||
|
},
|
||||||
|
supervisor_backups: {
|
||||||
|
redirect: "/hassio/backups",
|
||||||
},
|
},
|
||||||
supervisor_store: {
|
supervisor_store: {
|
||||||
redirect: "/hassio/store",
|
redirect: "/hassio/store",
|
||||||
|
@ -9,7 +9,7 @@ import "./addon-store/hassio-addon-store";
|
|||||||
// Don't codesplit it, that way the dashboard always loads fast.
|
// Don't codesplit it, that way the dashboard always loads fast.
|
||||||
import "./dashboard/hassio-dashboard";
|
import "./dashboard/hassio-dashboard";
|
||||||
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
|
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
|
||||||
import "./snapshots/hassio-snapshots";
|
import "./backups/hassio-backups";
|
||||||
import "./system/hassio-system";
|
import "./system/hassio-system";
|
||||||
|
|
||||||
@customElement("hassio-panel-router")
|
@customElement("hassio-panel-router")
|
||||||
@ -23,6 +23,8 @@ class HassioPanelRouter extends HassRouterPage {
|
|||||||
@property({ type: Boolean }) public narrow!: boolean;
|
@property({ type: Boolean }) public narrow!: boolean;
|
||||||
|
|
||||||
protected routerOptions: RouterOptions = {
|
protected routerOptions: RouterOptions = {
|
||||||
|
beforeRender: (page: string) =>
|
||||||
|
page === "snapshots" ? "backups" : undefined,
|
||||||
routes: {
|
routes: {
|
||||||
dashboard: {
|
dashboard: {
|
||||||
tag: "hassio-dashboard",
|
tag: "hassio-dashboard",
|
||||||
@ -30,8 +32,8 @@ class HassioPanelRouter extends HassRouterPage {
|
|||||||
store: {
|
store: {
|
||||||
tag: "hassio-addon-store",
|
tag: "hassio-addon-store",
|
||||||
},
|
},
|
||||||
snapshots: {
|
backups: {
|
||||||
tag: "hassio-snapshots",
|
tag: "hassio-backups",
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
tag: "hassio-system",
|
tag: "hassio-system",
|
||||||
|
@ -23,6 +23,8 @@ class HassioRouter extends HassRouterPage {
|
|||||||
protected routerOptions: RouterOptions = {
|
protected routerOptions: RouterOptions = {
|
||||||
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
||||||
defaultPage: "dashboard",
|
defaultPage: "dashboard",
|
||||||
|
beforeRender: (page: string) =>
|
||||||
|
page === "snapshots" ? "backups" : undefined,
|
||||||
initialLoad: () => this._redirectIngress(),
|
initialLoad: () => this._redirectIngress(),
|
||||||
showLoading: true,
|
showLoading: true,
|
||||||
routes: {
|
routes: {
|
||||||
@ -30,7 +32,7 @@ class HassioRouter extends HassRouterPage {
|
|||||||
tag: "hassio-panel",
|
tag: "hassio-panel",
|
||||||
cache: true,
|
cache: true,
|
||||||
},
|
},
|
||||||
snapshots: "dashboard",
|
backups: "dashboard",
|
||||||
store: "dashboard",
|
store: "dashboard",
|
||||||
system: "dashboard",
|
system: "dashboard",
|
||||||
addon: {
|
addon: {
|
||||||
|
@ -13,8 +13,8 @@ export const supervisorTabs: PageNavigation[] = [
|
|||||||
iconPath: mdiStore,
|
iconPath: mdiStore,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
translationKey: "panel.snapshots",
|
translationKey: "panel.backups",
|
||||||
path: `/hassio/snapshots`,
|
path: `/hassio/backups`,
|
||||||
iconPath: mdiBackupRestore,
|
iconPath: mdiBackupRestore,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -165,7 +165,7 @@ class HassioCoreInfo extends LitElement {
|
|||||||
supervisor: this.supervisor,
|
supervisor: this.supervisor,
|
||||||
name: "Home Assistant Core",
|
name: "Home Assistant Core",
|
||||||
version: this.supervisor.core.version_latest,
|
version: this.supervisor.core.version_latest,
|
||||||
snapshotParams: {
|
backupParams: {
|
||||||
name: `core_${this.supervisor.core.version}`,
|
name: `core_${this.supervisor.core.version}`,
|
||||||
folders: ["homeassistant"],
|
folders: ["homeassistant"],
|
||||||
homeassistant: true,
|
homeassistant: true,
|
||||||
|
@ -3,6 +3,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { atLeastVersion } from "../../../src/common/config/version";
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
import "../../../src/components/buttons/ha-progress-button";
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
|
import "../../../src/components/ha-alert";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
import "../../../src/components/ha-settings-row";
|
import "../../../src/components/ha-settings-row";
|
||||||
import "../../../src/components/ha-switch";
|
import "../../../src/components/ha-switch";
|
||||||
@ -170,31 +171,25 @@ class HassioSupervisorInfo extends LitElement {
|
|||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-settings-row>`
|
</ha-settings-row>`
|
||||||
: ""
|
: ""
|
||||||
: html`<div class="error">
|
: html`<ha-alert
|
||||||
|
alert-type="warning"
|
||||||
|
.actionText=${this.supervisor.localize("common.learn_more")}
|
||||||
|
@alert-action-clicked=${this._unsupportedDialog}
|
||||||
|
>
|
||||||
${this.supervisor.localize(
|
${this.supervisor.localize(
|
||||||
"system.supervisor.unsupported_title"
|
"system.supervisor.unsupported_title"
|
||||||
)}
|
)}
|
||||||
<button
|
</ha-alert>`}
|
||||||
class="link"
|
|
||||||
.title=${this.supervisor.localize("common.learn_more")}
|
|
||||||
@click=${this._unsupportedDialog}
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</button>
|
|
||||||
</div>`}
|
|
||||||
${!this.supervisor.supervisor.healthy
|
${!this.supervisor.supervisor.healthy
|
||||||
? html`<div class="error">
|
? html`<ha-alert
|
||||||
|
alert-type="error"
|
||||||
|
.actionText=${this.supervisor.localize("common.learn_more")}
|
||||||
|
@alert-action-clicked=${this._unhealthyDialog}
|
||||||
|
>
|
||||||
${this.supervisor.localize(
|
${this.supervisor.localize(
|
||||||
"system.supervisor.unhealthy_title"
|
"system.supervisor.unhealthy_title"
|
||||||
)}
|
)}
|
||||||
<button
|
</ha-alert>`
|
||||||
class="link"
|
|
||||||
.title=${this.supervisor.localize("common.learn_more")}
|
|
||||||
@click=${this._unhealthyDialog}
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</button>
|
|
||||||
</div>`
|
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="metrics-block">
|
<div class="metrics-block">
|
||||||
|
@ -5,6 +5,7 @@ import "@polymer/paper-listbox/paper-listbox";
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import "../../../src/components/buttons/ha-progress-button";
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
|
import "../../../src/components/ha-alert";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
|
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
|
||||||
@ -67,7 +68,9 @@ class HassioSupervisorLog extends LitElement {
|
|||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
return html`
|
return html`
|
||||||
<ha-card>
|
<ha-card>
|
||||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
${this.hass.userData?.showAdvanced
|
${this.hass.userData?.showAdvanced
|
||||||
? html`
|
? html`
|
||||||
<paper-dropdown-menu
|
<paper-dropdown-menu
|
||||||
@ -154,10 +157,6 @@ class HassioSupervisorLog extends LitElement {
|
|||||||
padding: 0 2%;
|
padding: 0 2%;
|
||||||
width: 96%;
|
width: 96%;
|
||||||
}
|
}
|
||||||
.errors {
|
|
||||||
color: var(--error-color);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
10
package.json
10
package.json
@ -17,7 +17,7 @@
|
|||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
|
||||||
"format": "yarn run format:eslint && yarn run format:prettier",
|
"format": "yarn run format:eslint && yarn run format:prettier",
|
||||||
"mocha": "ts-mocha -p test-mocha/tsconfig.test.json \"test-mocha/**/*.ts\"",
|
"mocha": "ts-mocha -p test-mocha/tsconfig.test.json \"test-mocha/**/*.ts\"",
|
||||||
"test": "yarn run lint && yarn run mocha"
|
"test": "yarn run mocha"
|
||||||
},
|
},
|
||||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@ -113,7 +113,7 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"leaflet": "^1.7.1",
|
"leaflet": "^1.7.1",
|
||||||
"leaflet-draw": "^1.0.4",
|
"leaflet-draw": "^1.0.4",
|
||||||
"lit": "^2.0.0-rc.2",
|
"lit": "^2.0.0-rc.3",
|
||||||
"lit-vaadin-helpers": "^0.1.3",
|
"lit-vaadin-helpers": "^0.1.3",
|
||||||
"marked": "^2.0.5",
|
"marked": "^2.0.5",
|
||||||
"mdn-polyfills": "^5.16.0",
|
"mdn-polyfills": "^5.16.0",
|
||||||
@ -233,8 +233,10 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
|
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
|
||||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||||
"lit-html": "2.0.0-rc.3",
|
"lit": "^2.0.0-rc.3",
|
||||||
"lit-element": "3.0.0-rc.2"
|
"lit-html": "2.0.0-rc.4",
|
||||||
|
"lit-element": "3.0.0-rc.3",
|
||||||
|
"@lit/reactive-element": "1.0.0-rc.3"
|
||||||
},
|
},
|
||||||
"main": "src/home-assistant.js",
|
"main": "src/home-assistant.js",
|
||||||
"husky": {
|
"husky": {
|
||||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="home-assistant-frontend",
|
name="home-assistant-frontend",
|
||||||
version="20210809.0",
|
version="20210830.0",
|
||||||
description="The Home Assistant frontend",
|
description="The Home Assistant frontend",
|
||||||
url="https://github.com/home-assistant/frontend",
|
url="https://github.com/home-assistant/frontend",
|
||||||
author="The Home Assistant Authors",
|
author="The Home Assistant Authors",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export const COLORS = [
|
export const COLORS = [
|
||||||
"#377eb8",
|
"#44739e",
|
||||||
"#984ea3",
|
"#984ea3",
|
||||||
"#00d2d5",
|
"#00d2d5",
|
||||||
"#ff7f00",
|
"#ff7f00",
|
||||||
|
@ -56,19 +56,30 @@ export const FIXED_DOMAIN_ICONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const FIXED_DEVICE_CLASS_ICONS = {
|
export const FIXED_DEVICE_CLASS_ICONS = {
|
||||||
|
aqi: "hass:air-filter",
|
||||||
current: "hass:current-ac",
|
current: "hass:current-ac",
|
||||||
carbon_dioxide: "mdi:molecule-co2",
|
carbon_dioxide: "mdi:molecule-co2",
|
||||||
carbon_monoxide: "mdi:molecule-co",
|
carbon_monoxide: "mdi:molecule-co",
|
||||||
energy: "hass:lightning-bolt",
|
energy: "hass:lightning-bolt",
|
||||||
|
gas: "hass:gas-cylinder",
|
||||||
humidity: "hass:water-percent",
|
humidity: "hass:water-percent",
|
||||||
illuminance: "hass:brightness-5",
|
illuminance: "hass:brightness-5",
|
||||||
|
nitrogen_dioxide: "mdi:molecule",
|
||||||
|
nitrogen_monoxide: "mdi:molecule",
|
||||||
|
nitrous_oxide: "mdi:molecule",
|
||||||
|
ozone: "mdi:molecule",
|
||||||
temperature: "hass:thermometer",
|
temperature: "hass:thermometer",
|
||||||
monetary: "mdi:cash",
|
monetary: "mdi:cash",
|
||||||
|
pm25: "mdi:molecule",
|
||||||
|
pm1: "mdi:molecule",
|
||||||
|
pm10: "mdi:molecule",
|
||||||
pressure: "hass:gauge",
|
pressure: "hass:gauge",
|
||||||
power: "hass:flash",
|
power: "hass:flash",
|
||||||
power_factor: "hass:angle-acute",
|
power_factor: "hass:angle-acute",
|
||||||
signal_strength: "hass:wifi",
|
signal_strength: "hass:wifi",
|
||||||
|
sulphur_dioxide: "mdi:molecule",
|
||||||
timestamp: "hass:clock",
|
timestamp: "hass:clock",
|
||||||
|
volatile_organic_compounds: "mdi:molecule",
|
||||||
voltage: "hass:sine-wave",
|
voltage: "hass:sine-wave",
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,66 +168,3 @@ export const UNIT_F = "°F";
|
|||||||
|
|
||||||
/** Entity ID of the default view. */
|
/** Entity ID of the default view. */
|
||||||
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
||||||
|
|
||||||
/** HA Color Pallete. */
|
|
||||||
export const HA_COLOR_PALETTE = [
|
|
||||||
"ff0029",
|
|
||||||
"66a61e",
|
|
||||||
"377eb8",
|
|
||||||
"984ea3",
|
|
||||||
"00d2d5",
|
|
||||||
"ff7f00",
|
|
||||||
"af8d00",
|
|
||||||
"7f80cd",
|
|
||||||
"b3e900",
|
|
||||||
"c42e60",
|
|
||||||
"a65628",
|
|
||||||
"f781bf",
|
|
||||||
"8dd3c7",
|
|
||||||
"bebada",
|
|
||||||
"fb8072",
|
|
||||||
"80b1d3",
|
|
||||||
"fdb462",
|
|
||||||
"fccde5",
|
|
||||||
"bc80bd",
|
|
||||||
"ffed6f",
|
|
||||||
"c4eaff",
|
|
||||||
"cf8c00",
|
|
||||||
"1b9e77",
|
|
||||||
"d95f02",
|
|
||||||
"e7298a",
|
|
||||||
"e6ab02",
|
|
||||||
"a6761d",
|
|
||||||
"0097ff",
|
|
||||||
"00d067",
|
|
||||||
"f43600",
|
|
||||||
"4ba93b",
|
|
||||||
"5779bb",
|
|
||||||
"927acc",
|
|
||||||
"97ee3f",
|
|
||||||
"bf3947",
|
|
||||||
"9f5b00",
|
|
||||||
"f48758",
|
|
||||||
"8caed6",
|
|
||||||
"f2b94f",
|
|
||||||
"eff26e",
|
|
||||||
"e43872",
|
|
||||||
"d9b100",
|
|
||||||
"9d7a00",
|
|
||||||
"698cff",
|
|
||||||
"d9d9d9",
|
|
||||||
"00d27e",
|
|
||||||
"d06800",
|
|
||||||
"009f82",
|
|
||||||
"c49200",
|
|
||||||
"cbe8ff",
|
|
||||||
"fecddf",
|
|
||||||
"c27eb6",
|
|
||||||
"8cd2ce",
|
|
||||||
"c4b8d9",
|
|
||||||
"f883b0",
|
|
||||||
"a49100",
|
|
||||||
"f48800",
|
|
||||||
"27d0df",
|
|
||||||
"a04a9b",
|
|
||||||
];
|
|
||||||
|
31
src/common/datetime/create_duration_data.ts
Normal file
31
src/common/datetime/create_duration_data.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { HaDurationData } from "../../components/ha-duration-input";
|
||||||
|
import { ForDict } from "../../data/automation";
|
||||||
|
|
||||||
|
export const createDurationData = (
|
||||||
|
duration: string | number | ForDict | undefined
|
||||||
|
): HaDurationData => {
|
||||||
|
if (duration === undefined) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (typeof duration !== "object") {
|
||||||
|
if (typeof duration === "string" || isNaN(duration)) {
|
||||||
|
const parts = duration?.toString().split(":") || [];
|
||||||
|
return {
|
||||||
|
hours: Number(parts[0]) || 0,
|
||||||
|
minutes: Number(parts[1]) || 0,
|
||||||
|
seconds: Number(parts[2]) || 0,
|
||||||
|
milliseconds: Number(parts[3]) || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { seconds: duration };
|
||||||
|
}
|
||||||
|
const { days, minutes, seconds, milliseconds } = duration;
|
||||||
|
let hours = duration.hours || 0;
|
||||||
|
hours = (hours || 0) + (days || 0) * 24;
|
||||||
|
return {
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
seconds,
|
||||||
|
milliseconds,
|
||||||
|
};
|
||||||
|
};
|
@ -3,33 +3,11 @@ import memoizeOne from "memoize-one";
|
|||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
|
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
|
||||||
|
|
||||||
const formatDateMem = memoizeOne(
|
// Tuesday, August 10
|
||||||
(locale: FrontendLocaleData) =>
|
export const formatDateWeekday = toLocaleDateStringSupportsOptions
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const formatDate = toLocaleDateStringSupportsOptions
|
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
formatDateMem(locale).format(dateObj)
|
formatDateWeekdayMem(locale).format(dateObj)
|
||||||
: (dateObj: Date) => format(dateObj, "longDate");
|
: (dateObj: Date) => format(dateObj, "dddd, MMMM D");
|
||||||
|
|
||||||
const formatDateShortMem = memoizeOne(
|
|
||||||
(locale: FrontendLocaleData) =>
|
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
|
||||||
day: "numeric",
|
|
||||||
month: "short",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const formatDateShort = toLocaleDateStringSupportsOptions
|
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
formatDateShortMem(locale).format(dateObj)
|
|
||||||
: (dateObj: Date) => format(dateObj, "shortDate");
|
|
||||||
|
|
||||||
const formatDateWeekdayMem = memoizeOne(
|
const formatDateWeekdayMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
@ -39,7 +17,80 @@ const formatDateWeekdayMem = memoizeOne(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const formatDateWeekday = toLocaleDateStringSupportsOptions
|
// August 10, 2021
|
||||||
|
export const formatDate = toLocaleDateStringSupportsOptions
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
formatDateWeekdayMem(locale).format(dateObj)
|
formatDateMem(locale).format(dateObj)
|
||||||
: (dateObj: Date) => format(dateObj, "dddd, MMM D");
|
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY");
|
||||||
|
const formatDateMem = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData) =>
|
||||||
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 10/08/2021
|
||||||
|
export const formatDateNumeric = toLocaleDateStringSupportsOptions
|
||||||
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
|
formatDateNumericMem(locale).format(dateObj)
|
||||||
|
: (dateObj: Date) => format(dateObj, "M/D/YYYY");
|
||||||
|
const formatDateNumericMem = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData) =>
|
||||||
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Aug 10
|
||||||
|
export const formatDateShort = toLocaleDateStringSupportsOptions
|
||||||
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
|
formatDateShortMem(locale).format(dateObj)
|
||||||
|
: (dateObj: Date) => format(dateObj, "MMM D");
|
||||||
|
const formatDateShortMem = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData) =>
|
||||||
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// August 2021
|
||||||
|
export const formatDateMonthYear = toLocaleDateStringSupportsOptions
|
||||||
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
|
formatDateMonthYearMem(locale).format(dateObj)
|
||||||
|
: (dateObj: Date) => format(dateObj, "MMMM YYYY");
|
||||||
|
const formatDateMonthYearMem = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData) =>
|
||||||
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// August
|
||||||
|
export const formatDateMonth = toLocaleDateStringSupportsOptions
|
||||||
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
|
formatDateMonthMem(locale).format(dateObj)
|
||||||
|
: (dateObj: Date) => format(dateObj, "MMMM");
|
||||||
|
const formatDateMonthMem = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData) =>
|
||||||
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
|
month: "long",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2021
|
||||||
|
export const formatDateYear = toLocaleDateStringSupportsOptions
|
||||||
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
|
formatDateYearMem(locale).format(dateObj)
|
||||||
|
: (dateObj: Date) => format(dateObj, "YYYY");
|
||||||
|
const formatDateYearMem = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData) =>
|
||||||
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
@ -4,6 +4,12 @@ import { FrontendLocaleData } from "../../data/translation";
|
|||||||
import { toLocaleStringSupportsOptions } from "./check_options_support";
|
import { toLocaleStringSupportsOptions } from "./check_options_support";
|
||||||
import { useAmPm } from "./use_am_pm";
|
import { useAmPm } from "./use_am_pm";
|
||||||
|
|
||||||
|
// August 9, 2021, 8:23 AM
|
||||||
|
export const formatDateTime = toLocaleStringSupportsOptions
|
||||||
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
|
formatDateTimeMem(locale).format(dateObj)
|
||||||
|
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
|
format(dateObj, "MMMM D, YYYY, HH:mm" + useAmPm(locale) ? " A" : "");
|
||||||
const formatDateTimeMem = memoizeOne(
|
const formatDateTimeMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
@ -16,12 +22,12 @@ const formatDateTimeMem = memoizeOne(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const formatDateTime = toLocaleStringSupportsOptions
|
// August 9, 2021, 8:23:15 AM
|
||||||
|
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
formatDateTimeMem(locale).format(dateObj)
|
formatDateTimeWithSecondsMem(locale).format(dateObj)
|
||||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
format(dateObj, "MMMM D, YYYY, HH:mm" + useAmPm(locale) ? " A" : "");
|
format(dateObj, "MMMM D, YYYY, HH:mm:ss" + useAmPm(locale) ? " A" : "");
|
||||||
|
|
||||||
const formatDateTimeWithSecondsMem = memoizeOne(
|
const formatDateTimeWithSecondsMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
@ -35,8 +41,20 @@ const formatDateTimeWithSecondsMem = memoizeOne(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
|
// 9/8/2021, 8:23 AM
|
||||||
|
export const formatDateTimeNumeric = toLocaleStringSupportsOptions
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
formatDateTimeWithSecondsMem(locale).format(dateObj)
|
formatDateTimeNumericMem(locale).format(dateObj)
|
||||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
format(dateObj, "MMMM D, YYYY, HH:mm:ss" + useAmPm(locale) ? " A" : "");
|
format(dateObj, "M/D/YYYY, HH:mm" + useAmPm(locale) ? " A" : "");
|
||||||
|
const formatDateTimeNumericMem = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData) =>
|
||||||
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: useAmPm(locale),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
@ -4,6 +4,12 @@ import { FrontendLocaleData } from "../../data/translation";
|
|||||||
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
|
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
|
||||||
import { useAmPm } from "./use_am_pm";
|
import { useAmPm } from "./use_am_pm";
|
||||||
|
|
||||||
|
// 9:15 PM || 21:15
|
||||||
|
export const formatTime = toLocaleTimeStringSupportsOptions
|
||||||
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
|
formatTimeMem(locale).format(dateObj)
|
||||||
|
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
|
format(dateObj, "shortTime" + useAmPm(locale) ? " A" : "");
|
||||||
const formatTimeMem = memoizeOne(
|
const formatTimeMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
@ -13,12 +19,12 @@ const formatTimeMem = memoizeOne(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const formatTime = toLocaleTimeStringSupportsOptions
|
// 9:15:24 PM || 21:15:24
|
||||||
|
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
formatTimeMem(locale).format(dateObj)
|
formatTimeWithSecondsMem(locale).format(dateObj)
|
||||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
format(dateObj, "shortTime" + useAmPm(locale) ? " A" : "");
|
format(dateObj, "mediumTime" + useAmPm(locale) ? " A" : "");
|
||||||
|
|
||||||
const formatTimeWithSecondsMem = memoizeOne(
|
const formatTimeWithSecondsMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
@ -29,12 +35,12 @@ const formatTimeWithSecondsMem = memoizeOne(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
// Tuesday 7:00 PM || Tuesday 19:00
|
||||||
|
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
? (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
formatTimeWithSecondsMem(locale).format(dateObj)
|
formatTimeWeekdayMem(locale).format(dateObj)
|
||||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
: (dateObj: Date, locale: FrontendLocaleData) =>
|
||||||
format(dateObj, "mediumTime" + useAmPm(locale) ? " A" : "");
|
format(dateObj, "dddd, HH:mm" + useAmPm(locale) ? " A" : "");
|
||||||
|
|
||||||
const formatTimeWeekdayMem = memoizeOne(
|
const formatTimeWeekdayMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
@ -44,9 +50,3 @@ const formatTimeWeekdayMem = memoizeOne(
|
|||||||
hour12: useAmPm(locale),
|
hour12: useAmPm(locale),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
|
|
||||||
? (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
formatTimeWeekdayMem(locale).format(dateObj)
|
|
||||||
: (dateObj: Date, locale: FrontendLocaleData) =>
|
|
||||||
format(dateObj, "dddd, HH:mm" + useAmPm(locale) ? " A" : "");
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { FrontendLocaleData, TimeFormat } from "../../data/translation";
|
import { FrontendLocaleData, TimeFormat } from "../../data/translation";
|
||||||
|
|
||||||
export const useAmPm = (locale: FrontendLocaleData): boolean => {
|
export const useAmPm = memoizeOne((locale: FrontendLocaleData): boolean => {
|
||||||
if (
|
if (
|
||||||
locale.time_format === TimeFormat.language ||
|
locale.time_format === TimeFormat.language ||
|
||||||
locale.time_format === TimeFormat.system
|
locale.time_format === TimeFormat.system
|
||||||
@ -12,4 +13,4 @@ export const useAmPm = (locale: FrontendLocaleData): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return locale.time_format === TimeFormat.am_pm;
|
return locale.time_format === TimeFormat.am_pm;
|
||||||
};
|
});
|
||||||
|
@ -44,6 +44,8 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
|
|||||||
return is_off ? "hass:home-outline" : "hass:home";
|
return is_off ? "hass:home-outline" : "hass:home";
|
||||||
case "sound":
|
case "sound":
|
||||||
return is_off ? "hass:music-note-off" : "hass:music-note";
|
return is_off ? "hass:music-note-off" : "hass:music-note";
|
||||||
|
case "update":
|
||||||
|
return is_off ? "mdi:package" : "mdi:package-up";
|
||||||
case "vibration":
|
case "vibration":
|
||||||
return is_off ? "hass:crop-portrait" : "hass:vibrate";
|
return is_off ? "hass:crop-portrait" : "hass:vibrate";
|
||||||
case "window":
|
case "window":
|
||||||
|
15
src/common/util/group-by.ts
Normal file
15
src/common/util/group-by.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const groupBy = <T>(
|
||||||
|
list: T[],
|
||||||
|
keySelector: (item: T) => string
|
||||||
|
): { [key: string]: T[] } => {
|
||||||
|
const result = {};
|
||||||
|
for (const item of list) {
|
||||||
|
const key = keySelector(item);
|
||||||
|
if (key in result) {
|
||||||
|
result[key].push(item);
|
||||||
|
} else {
|
||||||
|
result[key] = [item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
@ -7,14 +7,10 @@ interface ResultCache<T> {
|
|||||||
export const timeCachePromiseFunc = async <T>(
|
export const timeCachePromiseFunc = async <T>(
|
||||||
cacheKey: string,
|
cacheKey: string,
|
||||||
cacheTime: number,
|
cacheTime: number,
|
||||||
func: (
|
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
|
||||||
hass: HomeAssistant,
|
|
||||||
entityId: string,
|
|
||||||
...args: unknown[]
|
|
||||||
) => Promise<T>,
|
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
...args: unknown[]
|
...args: any[]
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey];
|
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey];
|
||||||
|
|
||||||
|
@ -35,7 +35,14 @@ import {
|
|||||||
endOfQuarter,
|
endOfQuarter,
|
||||||
endOfYear,
|
endOfYear,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { formatDate, formatDateShort } from "../../common/datetime/format_date";
|
import {
|
||||||
|
formatDate,
|
||||||
|
formatDateMonth,
|
||||||
|
formatDateMonthYear,
|
||||||
|
formatDateShort,
|
||||||
|
formatDateWeekday,
|
||||||
|
formatDateYear,
|
||||||
|
} from "../../common/datetime/format_date";
|
||||||
import {
|
import {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatDateTimeWithSeconds,
|
formatDateTimeWithSeconds,
|
||||||
@ -53,8 +60,11 @@ const FORMATS = {
|
|||||||
minute: "minute",
|
minute: "minute",
|
||||||
hour: "hour",
|
hour: "hour",
|
||||||
day: "day",
|
day: "day",
|
||||||
|
date: "date",
|
||||||
|
weekday: "weekday",
|
||||||
week: "week",
|
week: "week",
|
||||||
month: "month",
|
month: "month",
|
||||||
|
monthyear: "monthyear",
|
||||||
quarter: "quarter",
|
quarter: "quarter",
|
||||||
year: "year",
|
year: "year",
|
||||||
};
|
};
|
||||||
@ -81,16 +91,22 @@ _adapters._date.override({
|
|||||||
return formatTime(new Date(time), this.options.locale);
|
return formatTime(new Date(time), this.options.locale);
|
||||||
case "hour":
|
case "hour":
|
||||||
return formatTime(new Date(time), this.options.locale);
|
return formatTime(new Date(time), this.options.locale);
|
||||||
|
case "weekday":
|
||||||
|
return formatDateWeekday(new Date(time), this.options.locale);
|
||||||
|
case "date":
|
||||||
|
return formatDate(new Date(time), this.options.locale);
|
||||||
case "day":
|
case "day":
|
||||||
return formatDateShort(new Date(time), this.options.locale);
|
return formatDateShort(new Date(time), this.options.locale);
|
||||||
case "week":
|
case "week":
|
||||||
return formatDate(new Date(time), this.options.locale);
|
return formatDate(new Date(time), this.options.locale);
|
||||||
case "month":
|
case "month":
|
||||||
return formatDate(new Date(time), this.options.locale);
|
return formatDateMonth(new Date(time), this.options.locale);
|
||||||
|
case "monthyear":
|
||||||
|
return formatDateMonthYear(new Date(time), this.options.locale);
|
||||||
case "quarter":
|
case "quarter":
|
||||||
return formatDate(new Date(time), this.options.locale);
|
return formatDate(new Date(time), this.options.locale);
|
||||||
case "year":
|
case "year":
|
||||||
return formatDate(new Date(time), this.options.locale);
|
return formatDateYear(new Date(time), this.options.locale);
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,11 @@ export default class HaChartBase extends LitElement {
|
|||||||
this.chart.config.type = this.chartType;
|
this.chart.config.type = this.chartType;
|
||||||
}
|
}
|
||||||
if (changedProps.has("data")) {
|
if (changedProps.has("data")) {
|
||||||
|
if (this._hiddenDatasets.size) {
|
||||||
|
this.data.datasets.forEach((dataset, index) => {
|
||||||
|
dataset.hidden = this._hiddenDatasets.has(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
this.chart.data = this.data;
|
this.chart.data = this.data;
|
||||||
}
|
}
|
||||||
if (changedProps.has("options")) {
|
if (changedProps.has("options")) {
|
||||||
@ -238,9 +243,19 @@ export default class HaChartBase extends LitElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateChart = (): void => {
|
public updateChart = (
|
||||||
|
mode:
|
||||||
|
| "resize"
|
||||||
|
| "reset"
|
||||||
|
| "none"
|
||||||
|
| "hide"
|
||||||
|
| "show"
|
||||||
|
| "normal"
|
||||||
|
| "active"
|
||||||
|
| undefined
|
||||||
|
): void => {
|
||||||
if (this.chart) {
|
if (this.chart) {
|
||||||
this.chart.update();
|
this.chart.update(mode);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ export const createCurrencyListEl = () => {
|
|||||||
"BSD",
|
"BSD",
|
||||||
"BTN",
|
"BTN",
|
||||||
"BWP",
|
"BWP",
|
||||||
|
"BYN",
|
||||||
"BYR",
|
"BYR",
|
||||||
"BZD",
|
"BZD",
|
||||||
"CAD",
|
"CAD",
|
||||||
|
@ -62,6 +62,7 @@ export interface DataTableSortColumnData {
|
|||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
filterable?: boolean;
|
filterable?: boolean;
|
||||||
filterKey?: string;
|
filterKey?: string;
|
||||||
|
valueColumn?: string;
|
||||||
direction?: SortingDirection;
|
direction?: SortingDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ export interface DataTableColumnData extends DataTableSortColumnData {
|
|||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
|
export type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
|
||||||
title?: TemplateResult | string;
|
title?: TemplateResult | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -455,7 +456,7 @@ export class HaDataTable extends LitElement {
|
|||||||
const prom = this._sortColumn
|
const prom = this._sortColumn
|
||||||
? sortData(
|
? sortData(
|
||||||
filteredData,
|
filteredData,
|
||||||
this._sortColumns,
|
this._sortColumns[this._sortColumn],
|
||||||
this._sortDirection,
|
this._sortDirection,
|
||||||
this._sortColumn
|
this._sortColumn
|
||||||
)
|
)
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
import { expose } from "comlink";
|
import { expose } from "comlink";
|
||||||
import "proxy-polyfill";
|
import "proxy-polyfill";
|
||||||
import type {
|
import type {
|
||||||
|
ClonedDataTableColumnData,
|
||||||
DataTableRowData,
|
DataTableRowData,
|
||||||
DataTableSortColumnData,
|
|
||||||
SortableColumnContainer,
|
SortableColumnContainer,
|
||||||
SortingDirection,
|
SortingDirection,
|
||||||
} from "./ha-data-table";
|
} from "./ha-data-table";
|
||||||
@ -19,7 +19,11 @@ const filterData = (
|
|||||||
const [key, column] = columnEntry;
|
const [key, column] = columnEntry;
|
||||||
if (column.filterable) {
|
if (column.filterable) {
|
||||||
if (
|
if (
|
||||||
String(column.filterKey ? row[key][column.filterKey] : row[key])
|
String(
|
||||||
|
column.filterKey
|
||||||
|
? row[column.valueColumn || key][column.filterKey]
|
||||||
|
: row[column.valueColumn || key]
|
||||||
|
)
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.includes(filter)
|
.includes(filter)
|
||||||
) {
|
) {
|
||||||
@ -33,7 +37,7 @@ const filterData = (
|
|||||||
|
|
||||||
const sortData = (
|
const sortData = (
|
||||||
data: DataTableRowData[],
|
data: DataTableRowData[],
|
||||||
column: DataTableSortColumnData,
|
column: ClonedDataTableColumnData,
|
||||||
direction: SortingDirection,
|
direction: SortingDirection,
|
||||||
sortColumn: string
|
sortColumn: string
|
||||||
) =>
|
) =>
|
||||||
@ -44,12 +48,12 @@ const sortData = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
let valA = column.filterKey
|
let valA = column.filterKey
|
||||||
? a[sortColumn][column.filterKey]
|
? a[column.valueColumn || sortColumn][column.filterKey]
|
||||||
: a[sortColumn];
|
: a[column.valueColumn || sortColumn];
|
||||||
|
|
||||||
let valB = column.filterKey
|
let valB = column.filterKey
|
||||||
? b[sortColumn][column.filterKey]
|
? b[column.valueColumn || sortColumn][column.filterKey]
|
||||||
: b[sortColumn];
|
: b[column.valueColumn || sortColumn];
|
||||||
|
|
||||||
if (typeof valA === "string") {
|
if (typeof valA === "string") {
|
||||||
valA = valA.toUpperCase();
|
valA = valA.toUpperCase();
|
||||||
|
@ -257,7 +257,7 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
--ha-label-badge-color: var(--label-badge-yellow, #fce588);
|
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
180
src/components/ha-alert.ts
Normal file
180
src/components/ha-alert.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
|
import {
|
||||||
|
mdiAlertCircleOutline,
|
||||||
|
mdiAlertOutline,
|
||||||
|
mdiCheckboxMarkedCircleOutline,
|
||||||
|
mdiClose,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
|
const ALERT_ICONS = {
|
||||||
|
info: mdiAlertCircleOutline,
|
||||||
|
warning: mdiAlertOutline,
|
||||||
|
error: mdiAlertCircleOutline,
|
||||||
|
success: mdiCheckboxMarkedCircleOutline,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"alert-dismissed-clicked": undefined;
|
||||||
|
"alert-action-clicked": undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-alert")
|
||||||
|
class HaAlert extends LitElement {
|
||||||
|
@property() public title = "";
|
||||||
|
|
||||||
|
@property({ attribute: "alert-type" }) public alertType:
|
||||||
|
| "info"
|
||||||
|
| "warning"
|
||||||
|
| "error"
|
||||||
|
| "success" = "info";
|
||||||
|
|
||||||
|
@property({ attribute: "action-text" }) public actionText = "";
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public dismissable = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public rtl = false;
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="issue-type ${classMap({
|
||||||
|
rtl: this.rtl,
|
||||||
|
[this.alertType]: true,
|
||||||
|
})}"
|
||||||
|
>
|
||||||
|
<div class="icon">
|
||||||
|
<ha-svg-icon .path=${ALERT_ICONS[this.alertType]}></ha-svg-icon>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div
|
||||||
|
class="main-content ${classMap({
|
||||||
|
"no-title": !this.title,
|
||||||
|
})}"
|
||||||
|
>
|
||||||
|
${this.title ? html`<div class="title">${this.title}</div>` : ""}
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
${this.actionText
|
||||||
|
? html`<mwc-button
|
||||||
|
@click=${this._action_clicked}
|
||||||
|
.label=${this.actionText}
|
||||||
|
></mwc-button>`
|
||||||
|
: this.dismissable
|
||||||
|
? html`<mwc-icon-button
|
||||||
|
@click=${this._dismiss_clicked}
|
||||||
|
aria-label="Dismiss alert"
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiClose}> </ha-svg-icon>
|
||||||
|
</mwc-icon-button> `
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dismiss_clicked() {
|
||||||
|
fireEvent(this, "alert-dismissed-clicked");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _action_clicked() {
|
||||||
|
fireEvent(this, "alert-action-clicked");
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.issue-type {
|
||||||
|
position: relative;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
.issue-type.rtl {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
.issue-type::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0.12;
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
margin: 4px 8px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
.main-content.no-title {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.issue-type.rtl > .content {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.main-content {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mwc-button {
|
||||||
|
--mdc-theme-primary: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-type.info > .icon {
|
||||||
|
color: var(--info-color);
|
||||||
|
}
|
||||||
|
.issue-type.info::before {
|
||||||
|
background-color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-type.warning > .icon {
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
|
.issue-type.warning::before {
|
||||||
|
background-color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-type.error > .icon {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
.issue-type.error::before {
|
||||||
|
background-color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-type.success > .icon {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
.issue-type.success::before {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-alert": HaAlert;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import type { Button } from "@material/mwc-button/mwc-button";
|
||||||
import "@material/mwc-icon-button/mwc-icon-button";
|
import "@material/mwc-icon-button/mwc-icon-button";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, queryAll } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import type { ToggleButton } from "../types";
|
import type { ToggleButton } from "../types";
|
||||||
@ -15,6 +16,10 @@ export class HaButtonToggleGroup extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public fullWidth = false;
|
@property({ type: Boolean }) public fullWidth = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public dense = false;
|
||||||
|
|
||||||
|
@queryAll("mwc-button") private _buttons?: Button[];
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
@ -34,6 +39,8 @@ export class HaButtonToggleGroup extends LitElement {
|
|||||||
? `${100 / this.buttons.length}%`
|
? `${100 / this.buttons.length}%`
|
||||||
: "initial",
|
: "initial",
|
||||||
})}
|
})}
|
||||||
|
outlined
|
||||||
|
.dense=${this.dense}
|
||||||
.value=${button.value}
|
.value=${button.value}
|
||||||
?active=${this.active === button.value}
|
?active=${this.active === button.value}
|
||||||
@click=${this._handleClick}
|
@click=${this._handleClick}
|
||||||
@ -44,6 +51,16 @@ export class HaButtonToggleGroup extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected updated() {
|
||||||
|
// Work around Safari default margin that is not reset in mwc-button as of aug 2021
|
||||||
|
this._buttons?.forEach(async (button) => {
|
||||||
|
await button.updateComplete;
|
||||||
|
(
|
||||||
|
button.shadowRoot!.querySelector("button") as HTMLButtonElement
|
||||||
|
).style.margin = "0";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private _handleClick(ev): void {
|
private _handleClick(ev): void {
|
||||||
this.active = ev.currentTarget.value;
|
this.active = ev.currentTarget.value;
|
||||||
fireEvent(this, "value-changed", { value: this.active });
|
fireEvent(this, "value-changed", { value: this.active });
|
||||||
@ -56,10 +73,16 @@ export class HaButtonToggleGroup extends LitElement {
|
|||||||
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
||||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||||
}
|
}
|
||||||
mwc-icon-button,
|
|
||||||
mwc-button {
|
mwc-button {
|
||||||
|
--mdc-shape-small: 0;
|
||||||
|
--mdc-button-outline-width: 1px 0 1px 1px;
|
||||||
|
}
|
||||||
|
mwc-icon-button {
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-right-width: 0px;
|
border-right-width: 0px;
|
||||||
|
}
|
||||||
|
mwc-icon-button,
|
||||||
|
mwc-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -82,16 +105,19 @@ export class HaButtonToggleGroup extends LitElement {
|
|||||||
}
|
}
|
||||||
mwc-icon-button:first-child,
|
mwc-icon-button:first-child,
|
||||||
mwc-button:first-child {
|
mwc-button:first-child {
|
||||||
|
--mdc-shape-small: 4px 0 0 4px;
|
||||||
border-radius: 4px 0 0 4px;
|
border-radius: 4px 0 0 4px;
|
||||||
}
|
}
|
||||||
mwc-icon-button:last-child,
|
mwc-icon-button:last-child,
|
||||||
mwc-button:last-child {
|
mwc-button:last-child {
|
||||||
border-radius: 0 4px 4px 0;
|
border-radius: 0 4px 4px 0;
|
||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
|
--mdc-shape-small: 0 4px 4px 0;
|
||||||
|
--mdc-button-outline-width: 1px;
|
||||||
}
|
}
|
||||||
mwc-icon-button:only-child,
|
mwc-icon-button:only-child,
|
||||||
mwc-button:only-child {
|
mwc-button:only-child {
|
||||||
border-radius: 4px;
|
--mdc-shape-small: 4px;
|
||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
import { supportsFeature } from "../common/entity/supports-feature";
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
import {
|
import {
|
||||||
@ -41,6 +40,32 @@ class HaCameraStream extends LitElement {
|
|||||||
|
|
||||||
@state() private _url?: string;
|
@state() private _url?: string;
|
||||||
|
|
||||||
|
@state() private _connected = false;
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues): void {
|
||||||
|
if (
|
||||||
|
changedProps.has("stateObj") &&
|
||||||
|
!this._shouldRenderMJPEG &&
|
||||||
|
this.stateObj &&
|
||||||
|
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
|
||||||
|
this.stateObj.entity_id
|
||||||
|
) {
|
||||||
|
this._forceMJPEG = undefined;
|
||||||
|
this._url = undefined;
|
||||||
|
this._getStreamUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._connected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._connected = false;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.stateObj) {
|
if (!this.stateObj) {
|
||||||
return html``;
|
return html``;
|
||||||
@ -50,10 +75,11 @@ class HaCameraStream extends LitElement {
|
|||||||
${__DEMO__ || this._shouldRenderMJPEG
|
${__DEMO__ || this._shouldRenderMJPEG
|
||||||
? html`
|
? html`
|
||||||
<img
|
<img
|
||||||
@load=${this._elementResized}
|
|
||||||
.src=${__DEMO__
|
.src=${__DEMO__
|
||||||
? this.stateObj!.attributes.entity_picture
|
? this.stateObj!.attributes.entity_picture!
|
||||||
: computeMJPEGStreamUrl(this.stateObj)}
|
: this._connected
|
||||||
|
? computeMJPEGStreamUrl(this.stateObj)
|
||||||
|
: ""}
|
||||||
.alt=${`Preview of the ${computeStateName(
|
.alt=${`Preview of the ${computeStateName(
|
||||||
this.stateObj
|
this.stateObj
|
||||||
)} camera.`}
|
)} camera.`}
|
||||||
@ -75,13 +101,6 @@ class HaCameraStream extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
|
||||||
if (changedProps.has("stateObj") && !this._shouldRenderMJPEG) {
|
|
||||||
this._forceMJPEG = undefined;
|
|
||||||
this._getStreamUrl();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _shouldRenderMJPEG() {
|
private get _shouldRenderMJPEG() {
|
||||||
return (
|
return (
|
||||||
this._forceMJPEG === this.stateObj!.entity_id ||
|
this._forceMJPEG === this.stateObj!.entity_id ||
|
||||||
@ -107,10 +126,6 @@ class HaCameraStream extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _elementResized() {
|
|
||||||
fireEvent(this, "iron-resize");
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host,
|
:host,
|
||||||
|
140
src/components/ha-duration-input.ts
Normal file
140
src/components/ha-duration-input.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import "./paper-time-input";
|
||||||
|
|
||||||
|
export interface HaDurationData {
|
||||||
|
hours?: number;
|
||||||
|
minutes?: number;
|
||||||
|
seconds?: number;
|
||||||
|
milliseconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-duration-input")
|
||||||
|
class HaDurationInput extends LitElement {
|
||||||
|
@property({ attribute: false }) public data!: HaDurationData;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public suffix?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required?: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public enableMillisecond?: boolean;
|
||||||
|
|
||||||
|
@query("paper-time-input", true) private _input?: HTMLElement;
|
||||||
|
|
||||||
|
public focus() {
|
||||||
|
if (this._input) {
|
||||||
|
this._input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<paper-time-input
|
||||||
|
.label=${this.label}
|
||||||
|
.required=${this.required}
|
||||||
|
.autoValidate=${this.required}
|
||||||
|
error-message="Required"
|
||||||
|
enable-second
|
||||||
|
.enableMillisecond=${this.enableMillisecond}
|
||||||
|
format="24"
|
||||||
|
.hour=${this._parseDuration(this._hours)}
|
||||||
|
.min=${this._parseDuration(this._minutes)}
|
||||||
|
.sec=${this._parseDuration(this._seconds)}
|
||||||
|
.millisec=${this._parseDurationMillisec(this._milliseconds)}
|
||||||
|
@hour-changed=${this._hourChanged}
|
||||||
|
@min-changed=${this._minChanged}
|
||||||
|
@sec-changed=${this._secChanged}
|
||||||
|
@millisec-changed=${this._millisecChanged}
|
||||||
|
float-input-labels
|
||||||
|
no-hours-limit
|
||||||
|
always-float-input-labels
|
||||||
|
hour-label="hh"
|
||||||
|
min-label="mm"
|
||||||
|
sec-label="ss"
|
||||||
|
millisec-label="ms"
|
||||||
|
></paper-time-input>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _hours() {
|
||||||
|
return this.data && this.data.hours ? Number(this.data.hours) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _minutes() {
|
||||||
|
return this.data && this.data.minutes ? Number(this.data.minutes) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _seconds() {
|
||||||
|
return this.data && this.data.seconds ? Number(this.data.seconds) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _milliseconds() {
|
||||||
|
return this.data && this.data.milliseconds
|
||||||
|
? Number(this.data.milliseconds)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parseDuration(value) {
|
||||||
|
return value.toString().padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parseDurationMillisec(value) {
|
||||||
|
return value.toString().padStart(3, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hourChanged(ev) {
|
||||||
|
this._durationChanged(ev, "hours");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _minChanged(ev) {
|
||||||
|
this._durationChanged(ev, "minutes");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _secChanged(ev) {
|
||||||
|
this._durationChanged(ev, "seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _millisecChanged(ev) {
|
||||||
|
this._durationChanged(ev, "milliseconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _durationChanged(ev, unit) {
|
||||||
|
let value = Number(ev.detail.value);
|
||||||
|
|
||||||
|
if (value === this[`_${unit}`]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hours = this._hours;
|
||||||
|
let minutes = this._minutes;
|
||||||
|
|
||||||
|
if (unit === "seconds" && value > 59) {
|
||||||
|
minutes += Math.floor(value / 60);
|
||||||
|
value %= 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === "minutes" && value > 59) {
|
||||||
|
hours += Math.floor(value / 60);
|
||||||
|
value %= 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
seconds: this._seconds,
|
||||||
|
milliseconds: this._milliseconds,
|
||||||
|
...{ [unit]: value },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-duration-input": HaDurationInput;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { html, LitElement, TemplateResult } from "lit";
|
import { html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import "../ha-time-input";
|
import "../ha-duration-input";
|
||||||
import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./ha-form";
|
import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./ha-form";
|
||||||
|
|
||||||
@customElement("ha-form-positive_time_period_dict")
|
@customElement("ha-form-positive_time_period_dict")
|
||||||
@ -23,11 +23,11 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
|
|||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ha-time-input
|
<ha-duration-input
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.required=${this.schema.required}
|
.required=${this.schema.required}
|
||||||
.data=${this.data}
|
.data=${this.data}
|
||||||
></ha-time-input>
|
></ha-duration-input>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
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 { HaTimeData } from "../ha-time-input";
|
import { HaDurationData } from "../ha-duration-input";
|
||||||
import "./ha-form-boolean";
|
import "./ha-form-boolean";
|
||||||
import "./ha-form-constant";
|
import "./ha-form-constant";
|
||||||
import "./ha-form-float";
|
import "./ha-form-float";
|
||||||
@ -88,7 +88,7 @@ export type HaFormFloatData = number;
|
|||||||
export type HaFormBooleanData = boolean;
|
export type HaFormBooleanData = boolean;
|
||||||
export type HaFormSelectData = string;
|
export type HaFormSelectData = string;
|
||||||
export type HaFormMultiSelectData = string[];
|
export type HaFormMultiSelectData = string[];
|
||||||
export type HaFormTimeData = HaTimeData;
|
export type HaFormTimeData = HaDurationData;
|
||||||
|
|
||||||
export interface HaFormElement extends LitElement {
|
export interface HaFormElement extends LitElement {
|
||||||
schema: HaFormSchema | HaFormSchema[];
|
schema: HaFormSchema | HaFormSchema[];
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { css, LitElement, PropertyValues, svg } from "lit";
|
import { css, LitElement, PropertyValues, svg, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
@ -75,9 +75,22 @@ export class Gauge extends LitElement {
|
|||||||
this.levels
|
this.levels
|
||||||
? this.levels
|
? this.levels
|
||||||
.sort((a, b) => a.level - b.level)
|
.sort((a, b) => a.level - b.level)
|
||||||
.map((level) => {
|
.map((level, idx) => {
|
||||||
|
let firstPath: TemplateResult | undefined;
|
||||||
|
if (idx === 0 && level.level !== this.min) {
|
||||||
|
const angle = getAngle(this.min, this.min, this.max);
|
||||||
|
firstPath = svg`<path
|
||||||
|
stroke="var(--info-color)"
|
||||||
|
class="level"
|
||||||
|
d="M
|
||||||
|
${50 - 40 * Math.cos((angle * Math.PI) / 180)}
|
||||||
|
${50 - 40 * Math.sin((angle * Math.PI) / 180)}
|
||||||
|
A 40 40 0 0 1 90 50
|
||||||
|
"
|
||||||
|
></path>`;
|
||||||
|
}
|
||||||
const angle = getAngle(level.level, this.min, this.max);
|
const angle = getAngle(level.level, this.min, this.max);
|
||||||
return svg`<path
|
return svg`${firstPath}<path
|
||||||
stroke="${level.stroke}"
|
stroke="${level.stroke}"
|
||||||
class="level"
|
class="level"
|
||||||
d="M
|
d="M
|
||||||
|
@ -98,6 +98,11 @@ class HaHLSPlayer extends LitElement {
|
|||||||
|
|
||||||
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min"))
|
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min"))
|
||||||
.default;
|
.default;
|
||||||
|
|
||||||
|
if (!this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let hlsSupported = Hls.isSupported();
|
let hlsSupported = Hls.isSupported();
|
||||||
|
|
||||||
if (!hlsSupported) {
|
if (!hlsSupported) {
|
||||||
@ -115,6 +120,10 @@ class HaHLSPlayer extends LitElement {
|
|||||||
const useExoPlayer = await useExoPlayerPromise;
|
const useExoPlayer = await useExoPlayerPromise;
|
||||||
const masterPlaylist = await (await masterPlaylistPromise).text();
|
const masterPlaylist = await (await masterPlaylistPromise).text();
|
||||||
|
|
||||||
|
if (!this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url
|
// Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url
|
||||||
// See https://tools.ietf.org/html/rfc8216 for HLS spec details
|
// See https://tools.ietf.org/html/rfc8216 for HLS spec details
|
||||||
const playlistRegexp =
|
const playlistRegexp =
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { useAmPm } from "../../common/datetime/use_am_pm";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
|
||||||
import { TimeSelector } from "../../data/selector";
|
import { TimeSelector } from "../../data/selector";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../paper-time-input";
|
import "../ha-time-input";
|
||||||
|
|
||||||
@customElement("ha-selector-time")
|
@customElement("ha-selector-time")
|
||||||
export class HaTimeSelector extends LitElement {
|
export class HaTimeSelector extends LitElement {
|
||||||
@ -20,51 +16,17 @@ export class HaTimeSelector extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
private _useAmPmMem = memoizeOne((locale: FrontendLocaleData): boolean =>
|
|
||||||
useAmPm(locale)
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const useAMPM = this._useAmPmMem(this.hass.locale);
|
|
||||||
|
|
||||||
const parts = this.value?.split(":") || [];
|
|
||||||
const hours = parts[0];
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<paper-time-input
|
<ha-time-input
|
||||||
.label=${this.label}
|
.value=${this.value}
|
||||||
.hour=${hours &&
|
.locale=${this.hass.locale}
|
||||||
(useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours)}
|
|
||||||
.min=${parts[1]}
|
|
||||||
.sec=${parts[2]}
|
|
||||||
.format=${useAMPM ? 12 : 24}
|
|
||||||
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
|
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@change=${this._timeChanged}
|
|
||||||
@am-pm-changed=${this._timeChanged}
|
|
||||||
hide-label
|
hide-label
|
||||||
enable-second
|
enable-second
|
||||||
></paper-time-input>
|
></ha-time-input>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _timeChanged(ev) {
|
|
||||||
let value = ev.target.value;
|
|
||||||
const useAMPM = this._useAmPmMem(this.hass.locale);
|
|
||||||
let hours = Number(ev.target.hour || 0);
|
|
||||||
if (value && useAMPM) {
|
|
||||||
if (ev.target.amPm === "PM") {
|
|
||||||
hours += 12;
|
|
||||||
}
|
|
||||||
value = `${hours}:${ev.target.min || "00"}:${ev.target.sec || "00"}`;
|
|
||||||
}
|
|
||||||
if (value === this.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fireEvent(this, "value-changed", {
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -19,14 +19,14 @@ class HaSlider extends PaperSliderClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pin > .slider-knob > .slider-knob-inner {
|
.pin > .slider-knob > .slider-knob-inner {
|
||||||
font-size: var(--ha-slider-pin-font-size, 10px);
|
font-size: var(--ha-slider-pin-font-size, 15px);
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled.ring > .slider-knob > .slider-knob-inner {
|
.disabled.ring > .slider-knob > .slider-knob-inner {
|
||||||
background-color: var(--paper-slider-disabled-knob-color, var(--paper-grey-400));
|
background-color: var(--paper-slider-disabled-knob-color, var(--disabled-text-color));
|
||||||
border: 2px solid var(--paper-slider-disabled-knob-color, var(--paper-grey-400));
|
border: 2px solid var(--paper-slider-disabled-knob-color, var(--disabled-text-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin > .slider-knob > .slider-knob-inner::before {
|
.pin > .slider-knob > .slider-knob-inner::before {
|
||||||
|
@ -1,134 +1,78 @@
|
|||||||
import { html, LitElement, TemplateResult } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import "./paper-time-input";
|
import "./paper-time-input";
|
||||||
|
import { FrontendLocaleData } from "../data/translation";
|
||||||
export interface HaTimeData {
|
|
||||||
hours?: number;
|
|
||||||
minutes?: number;
|
|
||||||
seconds?: number;
|
|
||||||
milliseconds?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-time-input")
|
@customElement("ha-time-input")
|
||||||
class HaTimeInput extends LitElement {
|
export class HaTimeInput extends LitElement {
|
||||||
@property() public data!: HaTimeData;
|
@property() public locale!: FrontendLocaleData;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public suffix?: string;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required?: boolean;
|
@property({ type: Boolean, attribute: "hide-label" }) public hideLabel =
|
||||||
|
false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public enableMillisecond?: boolean;
|
@property({ type: Boolean, attribute: "enable-second" })
|
||||||
|
public enableSecond = false;
|
||||||
|
|
||||||
@query("paper-time-input", true) private _input?: HTMLElement;
|
protected render() {
|
||||||
|
const useAMPM = useAmPm(this.locale);
|
||||||
|
|
||||||
public focus() {
|
const parts = this.value?.split(":") || [];
|
||||||
if (this._input) {
|
let hours = parts[0];
|
||||||
this._input.focus();
|
const numberHours = Number(parts[0]);
|
||||||
|
if (numberHours && useAMPM && numberHours > 12) {
|
||||||
|
hours = String(numberHours - 12).padStart(2, "0");
|
||||||
|
}
|
||||||
|
if (useAMPM && numberHours === 0) {
|
||||||
|
hours = "12";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
return html`
|
||||||
<paper-time-input
|
<paper-time-input
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.required=${this.required}
|
.hour=${hours}
|
||||||
.autoValidate=${this.required}
|
.min=${parts[1]}
|
||||||
error-message="Required"
|
.sec=${parts[2]}
|
||||||
enable-second
|
.format=${useAMPM ? 12 : 24}
|
||||||
.enableMillisecond=${this.enableMillisecond}
|
.amPm=${useAMPM && (numberHours >= 12 ? "PM" : "AM")}
|
||||||
format="24"
|
.disabled=${this.disabled}
|
||||||
.hour=${this._parseDuration(this._hours)}
|
@change=${this._timeChanged}
|
||||||
.min=${this._parseDuration(this._minutes)}
|
@am-pm-changed=${this._timeChanged}
|
||||||
.sec=${this._parseDuration(this._seconds)}
|
.hideLabel=${this.hideLabel}
|
||||||
.millisec=${this._parseDurationMillisec(this._milliseconds)}
|
.enableSecond=${this.enableSecond}
|
||||||
@hour-changed=${this._hourChanged}
|
|
||||||
@min-changed=${this._minChanged}
|
|
||||||
@sec-changed=${this._secChanged}
|
|
||||||
@millisec-changed=${this._millisecChanged}
|
|
||||||
float-input-labels
|
|
||||||
no-hours-limit
|
|
||||||
always-float-input-labels
|
|
||||||
hour-label="hh"
|
|
||||||
min-label="mm"
|
|
||||||
sec-label="ss"
|
|
||||||
millisec-label="ms"
|
|
||||||
></paper-time-input>
|
></paper-time-input>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _hours() {
|
private _timeChanged(ev) {
|
||||||
return this.data && this.data.hours ? Number(this.data.hours) : 0;
|
let value = ev.target.value;
|
||||||
}
|
const useAMPM = useAmPm(this.locale);
|
||||||
|
let hours = Number(ev.target.hour || 0);
|
||||||
private get _minutes() {
|
if (value && useAMPM) {
|
||||||
return this.data && this.data.minutes ? Number(this.data.minutes) : 0;
|
if (ev.target.amPm === "PM" && hours < 12) {
|
||||||
}
|
hours += 12;
|
||||||
|
}
|
||||||
private get _seconds() {
|
if (ev.target.amPm === "AM" && hours === 12) {
|
||||||
return this.data && this.data.seconds ? Number(this.data.seconds) : 0;
|
hours = 0;
|
||||||
}
|
}
|
||||||
|
value = `${hours.toString().padStart(2, "0")}:${ev.target.min || "00"}:${
|
||||||
private get _milliseconds() {
|
ev.target.sec || "00"
|
||||||
return this.data && this.data.milliseconds
|
}`;
|
||||||
? Number(this.data.milliseconds)
|
}
|
||||||
: 0;
|
if (value === this.value) {
|
||||||
}
|
|
||||||
|
|
||||||
private _parseDuration(value) {
|
|
||||||
return value.toString().padStart(2, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseDurationMillisec(value) {
|
|
||||||
return value.toString().padStart(3, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _hourChanged(ev) {
|
|
||||||
this._durationChanged(ev, "hours");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _minChanged(ev) {
|
|
||||||
this._durationChanged(ev, "minutes");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _secChanged(ev) {
|
|
||||||
this._durationChanged(ev, "seconds");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _millisecChanged(ev) {
|
|
||||||
this._durationChanged(ev, "milliseconds");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _durationChanged(ev, unit) {
|
|
||||||
let value = Number(ev.detail.value);
|
|
||||||
|
|
||||||
if (value === this[`_${unit}`]) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.value = value;
|
||||||
let hours = this._hours;
|
fireEvent(this, "change");
|
||||||
let minutes = this._minutes;
|
|
||||||
|
|
||||||
if (unit === "seconds" && value > 59) {
|
|
||||||
minutes += Math.floor(value / 60);
|
|
||||||
value %= 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unit === "minutes" && value > 59) {
|
|
||||||
hours += Math.floor(value / 60);
|
|
||||||
value %= 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: {
|
value,
|
||||||
hours,
|
|
||||||
minutes,
|
|
||||||
seconds: this._seconds,
|
|
||||||
milliseconds: this._milliseconds,
|
|
||||||
...{ [unit]: value },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import { classMap } from "lit/directives/class-map";
|
|||||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||||
import "../ha-code-editor";
|
import "../ha-code-editor";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import type { NodeInfo } from "./hat-graph";
|
|
||||||
import "./hat-logbook-note";
|
import "./hat-logbook-note";
|
||||||
import { LogbookEntry } from "../../data/logbook";
|
import { LogbookEntry } from "../../data/logbook";
|
||||||
import {
|
import {
|
||||||
@ -17,6 +16,7 @@ import {
|
|||||||
import "../../panels/logbook/ha-logbook";
|
import "../../panels/logbook/ha-logbook";
|
||||||
import { traceTabStyles } from "./trace-tab-styles";
|
import { traceTabStyles } from "./trace-tab-styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
import type { NodeInfo } from "./hat-script-graph";
|
||||||
|
|
||||||
@customElement("ha-trace-path-details")
|
@customElement("ha-trace-path-details")
|
||||||
export class HaTracePathDetails extends LitElement {
|
export class HaTracePathDetails extends LitElement {
|
||||||
@ -30,7 +30,7 @@ export class HaTracePathDetails extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public selected!: NodeInfo;
|
@property({ attribute: false }) public selected!: NodeInfo;
|
||||||
|
|
||||||
@property() renderedNodes: Record<string, any> = {};
|
@property() public renderedNodes: Record<string, any> = {};
|
||||||
|
|
||||||
@property() public trackedNodes!: Record<string, any>;
|
@property() public trackedNodes!: Record<string, any>;
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { NodeInfo } from "./hat-graph";
|
|
||||||
import "./hat-logbook-note";
|
import "./hat-logbook-note";
|
||||||
import "./hat-trace-timeline";
|
import "./hat-trace-timeline";
|
||||||
import type { LogbookEntry } from "../../data/logbook";
|
import type { LogbookEntry } from "../../data/logbook";
|
||||||
import type { TraceExtended } from "../../data/trace";
|
import type { TraceExtended } from "../../data/trace";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import type { NodeInfo } from "./hat-script-graph";
|
||||||
|
|
||||||
@customElement("ha-trace-timeline")
|
@customElement("ha-trace-timeline")
|
||||||
export class HaTraceTimeline extends LitElement {
|
export class HaTraceTimeline extends LitElement {
|
||||||
@ -15,7 +15,7 @@ export class HaTraceTimeline extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public logbookEntries!: LogbookEntry[];
|
@property({ attribute: false }) public logbookEntries!: LogbookEntry[];
|
||||||
|
|
||||||
@property() public selected!: NodeInfo;
|
@property({ attribute: false }) public selected!: NodeInfo;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
|
186
src/components/trace/hat-graph-branch.ts
Normal file
186
src/components/trace/hat-graph-branch.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import { css, html, LitElement, svg } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { BRANCH_HEIGHT, SPACING } from "./hat-graph-const";
|
||||||
|
|
||||||
|
interface BranchConfig {
|
||||||
|
x: number;
|
||||||
|
height: number;
|
||||||
|
start: boolean;
|
||||||
|
end: boolean;
|
||||||
|
track: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @attribute active
|
||||||
|
* @attribute track
|
||||||
|
*/
|
||||||
|
@customElement("hat-graph-branch")
|
||||||
|
export class HatGraphBranch extends LitElement {
|
||||||
|
@property({ reflect: true, type: Boolean }) disabled?: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) selected?: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) start = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) short = false;
|
||||||
|
|
||||||
|
@state() _branches: BranchConfig[] = [];
|
||||||
|
|
||||||
|
private _totalWidth = 0;
|
||||||
|
|
||||||
|
private _maxHeight = 0;
|
||||||
|
|
||||||
|
private _updateBranches(ev: Event) {
|
||||||
|
let total_width = 0;
|
||||||
|
const heights: number[] = [];
|
||||||
|
const branches: BranchConfig[] = [];
|
||||||
|
(ev.target as HTMLSlotElement).assignedElements().forEach((c) => {
|
||||||
|
const width = c.clientWidth;
|
||||||
|
const height = c.clientHeight;
|
||||||
|
branches.push({
|
||||||
|
x: width / 2 + total_width,
|
||||||
|
height,
|
||||||
|
start: c.hasAttribute("graphStart"),
|
||||||
|
end: c.hasAttribute("graphEnd"),
|
||||||
|
track: c.hasAttribute("track"),
|
||||||
|
});
|
||||||
|
total_width += width;
|
||||||
|
heights.push(height);
|
||||||
|
});
|
||||||
|
this._totalWidth = total_width;
|
||||||
|
this._maxHeight = Math.max(...heights);
|
||||||
|
this._branches = branches.sort((a, b) => {
|
||||||
|
if (a.track && !b.track) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a.track && b.track) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<slot name="head"></slot>
|
||||||
|
${!this.start
|
||||||
|
? svg`
|
||||||
|
<svg
|
||||||
|
id="top"
|
||||||
|
width="${this._totalWidth}"
|
||||||
|
>
|
||||||
|
${this._branches.map((branch) =>
|
||||||
|
branch.start
|
||||||
|
? ""
|
||||||
|
: svg`
|
||||||
|
<path
|
||||||
|
class=${classMap({
|
||||||
|
track: branch.track,
|
||||||
|
})}
|
||||||
|
d="
|
||||||
|
M ${this._totalWidth / 2} 0
|
||||||
|
L ${branch.x} ${BRANCH_HEIGHT}
|
||||||
|
"/>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div id="branches">
|
||||||
|
<svg id="lines" width="${this._totalWidth}" height="${this._maxHeight}">
|
||||||
|
${this._branches.map((branch) => {
|
||||||
|
if (branch.end) return "";
|
||||||
|
return svg`
|
||||||
|
<path
|
||||||
|
class=${classMap({
|
||||||
|
track: branch.track,
|
||||||
|
})}
|
||||||
|
d="
|
||||||
|
M ${branch.x} ${branch.height}
|
||||||
|
v ${this._maxHeight - branch.height}
|
||||||
|
"/>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
<slot @slotchange=${this._updateBranches}></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${!this.short
|
||||||
|
? svg`
|
||||||
|
<svg
|
||||||
|
id="bottom"
|
||||||
|
width="${this._totalWidth}"
|
||||||
|
>
|
||||||
|
${this._branches.map((branch) => {
|
||||||
|
if (branch.end) return "";
|
||||||
|
return svg`
|
||||||
|
<path
|
||||||
|
class=${classMap({
|
||||||
|
track: branch.track,
|
||||||
|
})}
|
||||||
|
d="
|
||||||
|
M ${branch.x} 0
|
||||||
|
V ${SPACING}
|
||||||
|
L ${this._totalWidth / 2} ${BRANCH_HEIGHT + SPACING}
|
||||||
|
"/>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
:host(:focus) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
#branches {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
::slotted(*) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
::slotted([slot="head"]) {
|
||||||
|
margin-bottom: calc(var(--hat-graph-branch-height) / -2);
|
||||||
|
}
|
||||||
|
#lines {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
#top {
|
||||||
|
height: var(--hat-graph-branch-height);
|
||||||
|
}
|
||||||
|
#bottom {
|
||||||
|
height: calc(var(--hat-graph-branch-height) + var(--hat-graph-spacing));
|
||||||
|
}
|
||||||
|
path {
|
||||||
|
stroke: var(--stroke-clr);
|
||||||
|
stroke-width: 2;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
path.track {
|
||||||
|
stroke: var(--track-clr);
|
||||||
|
}
|
||||||
|
:host([disabled]) path {
|
||||||
|
stroke: var(--disabled-clr);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hat-graph-branch": HatGraphBranch;
|
||||||
|
}
|
||||||
|
}
|
3
src/components/trace/hat-graph-const.ts
Normal file
3
src/components/trace/hat-graph-const.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const SPACING = 10;
|
||||||
|
export const NODE_SIZE = 30;
|
||||||
|
export const BRANCH_HEIGHT = 20;
|
@ -1,7 +1,18 @@
|
|||||||
import { css, LitElement, svg } from "lit";
|
import {
|
||||||
|
css,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
html,
|
||||||
|
TemplateResult,
|
||||||
|
svg,
|
||||||
|
} from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { NODE_SIZE, SPACING } from "./hat-graph";
|
import { NODE_SIZE, SPACING } from "./hat-graph-const";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @attribute active
|
||||||
|
* @attribute track
|
||||||
|
*/
|
||||||
@customElement("hat-graph-node")
|
@customElement("hat-graph-node")
|
||||||
export class HatGraphNode extends LitElement {
|
export class HatGraphNode extends LitElement {
|
||||||
@property() iconPath?: string;
|
@property() iconPath?: string;
|
||||||
@ -10,31 +21,30 @@ export class HatGraphNode extends LitElement {
|
|||||||
|
|
||||||
@property({ reflect: true, type: Boolean }) graphStart?: boolean;
|
@property({ reflect: true, type: Boolean }) graphStart?: boolean;
|
||||||
|
|
||||||
@property({ reflect: true, type: Boolean }) nofocus?: boolean;
|
@property({ type: Boolean, attribute: "nofocus" }) noFocus = false;
|
||||||
|
|
||||||
@property({ reflect: true, type: Number }) badge?: number;
|
@property({ reflect: true, type: Number }) badge?: number;
|
||||||
|
|
||||||
connectedCallback() {
|
protected updated(changedProps: PropertyValues) {
|
||||||
super.connectedCallback();
|
if (changedProps.has("noFocus")) {
|
||||||
if (!this.hasAttribute("tabindex") && !this.nofocus)
|
if (!this.hasAttribute("tabindex") && !this.noFocus) {
|
||||||
this.setAttribute("tabindex", "0");
|
this.setAttribute("tabindex", "0");
|
||||||
|
} else if (changedProps.get("noFocus") !== undefined && this.noFocus) {
|
||||||
|
this.removeAttribute("tabindex");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
protected render(): TemplateResult {
|
||||||
const height = NODE_SIZE + (this.graphStart ? 2 : SPACING + 1);
|
const height = NODE_SIZE + (this.graphStart ? 2 : SPACING + 1);
|
||||||
const width = SPACING + NODE_SIZE;
|
const width = SPACING + NODE_SIZE;
|
||||||
return svg`
|
return html`
|
||||||
<svg
|
<svg
|
||||||
width="${width}px"
|
viewBox="-${Math.ceil(width / 2)} -${this.graphStart
|
||||||
height="${height}px"
|
? Math.ceil(height / 2)
|
||||||
viewBox="-${Math.ceil(width / 2)} -${
|
: Math.ceil((NODE_SIZE + SPACING * 2) / 2)} ${width} ${height}"
|
||||||
this.graphStart
|
>
|
||||||
? Math.ceil(height / 2)
|
${this.graphStart
|
||||||
: Math.ceil((NODE_SIZE + SPACING * 2) / 2)
|
|
||||||
} ${width} ${height}"
|
|
||||||
>
|
|
||||||
${
|
|
||||||
this.graphStart
|
|
||||||
? ``
|
? ``
|
||||||
: svg`
|
: svg`
|
||||||
<path
|
<path
|
||||||
@ -45,41 +55,31 @@ export class HatGraphNode extends LitElement {
|
|||||||
"
|
"
|
||||||
line-caps="round"
|
line-caps="round"
|
||||||
/>
|
/>
|
||||||
`
|
`}
|
||||||
}
|
<g class="node">
|
||||||
<g class="node">
|
<circle cx="0" cy="0" r=${NODE_SIZE / 2} />
|
||||||
<circle
|
}
|
||||||
cx="0"
|
${this.badge
|
||||||
cy="0"
|
? svg`
|
||||||
r="${NODE_SIZE / 2}"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
${
|
|
||||||
this.badge
|
|
||||||
? svg`
|
|
||||||
<g class="number">
|
<g class="number">
|
||||||
<circle
|
<circle
|
||||||
cx="8"
|
cx="8"
|
||||||
cy="${-NODE_SIZE / 2}"
|
cy=${-NODE_SIZE / 2}
|
||||||
r="8"
|
r="8"
|
||||||
></circle>
|
></circle>
|
||||||
<text
|
<text
|
||||||
x="8"
|
x="8"
|
||||||
y="${-NODE_SIZE / 2}"
|
y=${-NODE_SIZE / 2}
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
alignment-baseline="middle"
|
alignment-baseline="middle"
|
||||||
>${this.badge > 9 ? "9+" : this.badge}</text>
|
>${this.badge > 9 ? "9+" : this.badge}</text>
|
||||||
</g>
|
</g>
|
||||||
`
|
`
|
||||||
: ""
|
: ""}
|
||||||
}
|
<g style="pointer-events: none" transform="translate(${-12} ${-12})">
|
||||||
<g
|
${this.iconPath ? svg`<path class="icon" d=${this.iconPath}/>` : ""}
|
||||||
style="pointer-events: none"
|
</g>
|
||||||
transform="translate(${-12} ${-12})"
|
</g>
|
||||||
>
|
|
||||||
${this.iconPath ? svg`<path class="icon" d="${this.iconPath}"/>` : ""}
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -89,12 +89,19 @@ export class HatGraphNode extends LitElement {
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
width: calc(var(--hat-graph-node-size) + var(--hat-graph-spacing));
|
||||||
|
height: calc(
|
||||||
|
var(--hat-graph-node-size) + var(--hat-graph-spacing) + 1px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
:host(.track) {
|
:host([graphStart]) {
|
||||||
|
height: calc(var(--hat-graph-node-size) + 2px);
|
||||||
|
}
|
||||||
|
:host([track]) {
|
||||||
--stroke-clr: var(--track-clr);
|
--stroke-clr: var(--track-clr);
|
||||||
--icon-clr: var(--default-icon-clr);
|
--icon-clr: var(--default-icon-clr);
|
||||||
}
|
}
|
||||||
:host(.active) circle {
|
:host([active]) circle {
|
||||||
--stroke-clr: var(--active-clr);
|
--stroke-clr: var(--active-clr);
|
||||||
--icon-clr: var(--default-icon-clr);
|
--icon-clr: var(--default-icon-clr);
|
||||||
}
|
}
|
||||||
@ -108,13 +115,9 @@ export class HatGraphNode extends LitElement {
|
|||||||
:host([disabled]) circle {
|
:host([disabled]) circle {
|
||||||
stroke: var(--disabled-clr);
|
stroke: var(--disabled-clr);
|
||||||
}
|
}
|
||||||
:host-context([disabled]) {
|
svg {
|
||||||
--stroke-clr: var(--disabled-clr);
|
width: 100%;
|
||||||
}
|
height: 100%;
|
||||||
:host([nofocus]):host-context(.active),
|
|
||||||
:host([nofocus]):host-context(:focus) {
|
|
||||||
--circle-clr: var(--active-clr);
|
|
||||||
--icon-clr: var(--default-icon-clr);
|
|
||||||
}
|
}
|
||||||
circle,
|
circle,
|
||||||
path.connector {
|
path.connector {
|
||||||
@ -137,24 +140,6 @@ export class HatGraphNode extends LitElement {
|
|||||||
path.icon {
|
path.icon {
|
||||||
fill: var(--icon-clr);
|
fill: var(--icon-clr);
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(.triggered) svg {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
:host(.triggered) circle {
|
|
||||||
animation: glow 10s;
|
|
||||||
}
|
|
||||||
@keyframes glow {
|
|
||||||
0% {
|
|
||||||
filter: drop-shadow(0px 0px 5px rgba(var(--rgb-trigger-color), 0));
|
|
||||||
}
|
|
||||||
10% {
|
|
||||||
filter: drop-shadow(0px 0px 10px rgba(var(--rgb-trigger-color), 1));
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
filter: drop-shadow(0px 0px 5px rgba(var(--rgb-trigger-color), 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,26 @@
|
|||||||
import { css, LitElement, svg } from "lit";
|
import { css, LitElement, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { NODE_SIZE, SPACING } from "./hat-graph";
|
import { SPACING, NODE_SIZE } from "./hat-graph-const";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @attribute active
|
||||||
|
* @attribute track
|
||||||
|
*/
|
||||||
@customElement("hat-graph-spacer")
|
@customElement("hat-graph-spacer")
|
||||||
export class HatGraphSpacer extends LitElement {
|
export class HatGraphSpacer extends LitElement {
|
||||||
@property({ reflect: true, type: Boolean }) disabled?: boolean;
|
@property({ reflect: true, type: Boolean }) disabled?: boolean;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return svg`
|
return html`
|
||||||
<svg
|
<svg viewBox="-${SPACING / 2} 0 10 ${SPACING + NODE_SIZE + 1}">
|
||||||
width="${SPACING}px"
|
<path
|
||||||
height="${SPACING + NODE_SIZE + 1}px"
|
d="
|
||||||
viewBox="-${SPACING / 2} 0 10 ${SPACING + NODE_SIZE + 1}"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
class="connector"
|
|
||||||
d="
|
|
||||||
M 0 ${SPACING + NODE_SIZE + 1}
|
M 0 ${SPACING + NODE_SIZE + 1}
|
||||||
L 0 0
|
V 0
|
||||||
"
|
"
|
||||||
line-caps="round"
|
line-caps="round"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -31,15 +30,21 @@ export class HatGraphSpacer extends LitElement {
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
:host(.track) {
|
svg {
|
||||||
|
width: var(--hat-graph-spacing);
|
||||||
|
height: calc(
|
||||||
|
var(--hat-graph-spacing) + var(--hat-graph-node-size) + 1px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
:host([track]) {
|
||||||
--stroke-clr: var(--track-clr);
|
--stroke-clr: var(--track-clr);
|
||||||
--icon-clr: var(--default-icon-clr);
|
|
||||||
}
|
}
|
||||||
:host-context([disabled]) {
|
:host-context([disabled]) {
|
||||||
--stroke-clr: var(--disabled-clr);
|
--stroke-clr: var(--disabled-clr);
|
||||||
}
|
}
|
||||||
path.connector {
|
path {
|
||||||
stroke: var(--stroke-clr);
|
stroke: var(--stroke-clr);
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
fill: none;
|
fill: none;
|
||||||
|
@ -1,219 +0,0 @@
|
|||||||
import { css, html, LitElement, svg } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { classMap } from "lit/directives/class-map";
|
|
||||||
|
|
||||||
export const BRANCH_HEIGHT = 20;
|
|
||||||
export const SPACING = 10;
|
|
||||||
export const NODE_SIZE = 30;
|
|
||||||
|
|
||||||
const track_converter = {
|
|
||||||
fromAttribute: (value) => value.split(",").map((v) => parseInt(v)),
|
|
||||||
toAttribute: (value) =>
|
|
||||||
value instanceof Array ? value.join(",") : `${value}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface NodeInfo {
|
|
||||||
path: string;
|
|
||||||
config: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BranchConfig {
|
|
||||||
x: number;
|
|
||||||
height: number;
|
|
||||||
start: boolean;
|
|
||||||
end: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("hat-graph")
|
|
||||||
export class HatGraph extends LitElement {
|
|
||||||
@property({ type: Number }) _num_items = 0;
|
|
||||||
|
|
||||||
@property({ reflect: true, type: Boolean }) branching?: boolean;
|
|
||||||
|
|
||||||
@property({ converter: track_converter })
|
|
||||||
track_start?: number[];
|
|
||||||
|
|
||||||
@property({ converter: track_converter }) track_end?: number[];
|
|
||||||
|
|
||||||
@property({ reflect: true, type: Boolean }) disabled?: boolean;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) selected?: boolean;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) short = false;
|
|
||||||
|
|
||||||
async updateChildren() {
|
|
||||||
this._num_items = this.children.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const branches: BranchConfig[] = [];
|
|
||||||
let total_width = 0;
|
|
||||||
let max_height = 0;
|
|
||||||
let min_height = Number.POSITIVE_INFINITY;
|
|
||||||
if (this.branching) {
|
|
||||||
for (const c of Array.from(this.children)) {
|
|
||||||
if (c.slot === "head") continue;
|
|
||||||
const rect = c.getBoundingClientRect();
|
|
||||||
branches.push({
|
|
||||||
x: rect.width / 2 + total_width,
|
|
||||||
height: rect.height,
|
|
||||||
start: c.getAttribute("graphStart") != null,
|
|
||||||
end: c.getAttribute("graphEnd") != null,
|
|
||||||
});
|
|
||||||
total_width += rect.width;
|
|
||||||
max_height = Math.max(max_height, rect.height);
|
|
||||||
min_height = Math.min(min_height, rect.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<slot name="head" @slotchange=${this.updateChildren}> </slot>
|
|
||||||
${this.branching && branches.some((branch) => !branch.start)
|
|
||||||
? svg`
|
|
||||||
<svg
|
|
||||||
id="top"
|
|
||||||
width="${total_width}"
|
|
||||||
height="${BRANCH_HEIGHT}"
|
|
||||||
>
|
|
||||||
${branches.map((branch, i) => {
|
|
||||||
if (branch.start) return "";
|
|
||||||
return svg`
|
|
||||||
<path
|
|
||||||
class="${classMap({
|
|
||||||
line: true,
|
|
||||||
track: this.track_start?.includes(i) ?? false,
|
|
||||||
})}"
|
|
||||||
id="${this.track_start?.includes(i) ? "track-start" : ""}"
|
|
||||||
index=${i}
|
|
||||||
d="
|
|
||||||
M ${total_width / 2} 0
|
|
||||||
L ${branch.x} ${BRANCH_HEIGHT}
|
|
||||||
"/>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
<use xlink:href="#track-start" />
|
|
||||||
</svg>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<div id="branches">
|
|
||||||
${this.branching
|
|
||||||
? svg`
|
|
||||||
<svg
|
|
||||||
id="lines"
|
|
||||||
width="${total_width}"
|
|
||||||
height="${max_height}"
|
|
||||||
>
|
|
||||||
${branches.map((branch, i) => {
|
|
||||||
if (branch.end) return "";
|
|
||||||
return svg`
|
|
||||||
<path
|
|
||||||
class="${classMap({
|
|
||||||
line: true,
|
|
||||||
track: this.track_end?.includes(i) ?? false,
|
|
||||||
})}"
|
|
||||||
index=${i}
|
|
||||||
d="
|
|
||||||
M ${branch.x} ${branch.height}
|
|
||||||
l 0 ${max_height - branch.height}
|
|
||||||
"/>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
</svg>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<slot @slotchange=${this.updateChildren}></slot>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.branching && !this.short
|
|
||||||
? svg`
|
|
||||||
<svg
|
|
||||||
id="bottom"
|
|
||||||
width="${total_width}"
|
|
||||||
height="${BRANCH_HEIGHT + SPACING}"
|
|
||||||
>
|
|
||||||
${branches.map((branch, i) => {
|
|
||||||
if (branch.end) return "";
|
|
||||||
return svg`
|
|
||||||
<path
|
|
||||||
class="${classMap({
|
|
||||||
line: true,
|
|
||||||
track: this.track_end?.includes(i) ?? false,
|
|
||||||
})}"
|
|
||||||
id="${this.track_end?.includes(i) ? "track-end" : ""}"
|
|
||||||
index=${i}
|
|
||||||
d="
|
|
||||||
M ${branch.x} 0
|
|
||||||
L ${branch.x} ${SPACING}
|
|
||||||
L ${total_width / 2} ${BRANCH_HEIGHT + SPACING}
|
|
||||||
"/>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
<use xlink:href="#track-end" />
|
|
||||||
</svg>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
:host {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
--stroke-clr: var(--stroke-color, var(--secondary-text-color));
|
|
||||||
--active-clr: var(--active-color, var(--primary-color));
|
|
||||||
--track-clr: var(--track-color, var(--accent-color));
|
|
||||||
--hover-clr: var(--hover-color, var(--primary-color));
|
|
||||||
--disabled-clr: var(--disabled-color, var(--disabled-text-color));
|
|
||||||
--default-trigger-color: 3, 169, 244;
|
|
||||||
--rgb-trigger-color: var(--trigger-color, var(--default-trigger-color));
|
|
||||||
--background-clr: var(--background-color, white);
|
|
||||||
--default-icon-clr: var(--icon-color, black);
|
|
||||||
--icon-clr: var(--stroke-clr);
|
|
||||||
}
|
|
||||||
:host(:focus) {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
#branches {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
:host([branching]) #branches {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
:host([branching]) ::slotted(*) {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
:host([branching]) ::slotted([slot="head"]) {
|
|
||||||
margin-bottom: ${-BRANCH_HEIGHT / 2}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#lines {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
path.line {
|
|
||||||
stroke: var(--stroke-clr);
|
|
||||||
stroke-width: 2;
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
path.line.track {
|
|
||||||
stroke: var(--track-clr);
|
|
||||||
}
|
|
||||||
:host([disabled]) path.line {
|
|
||||||
stroke: var(--disabled-clr);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"hat-graph": HatGraph;
|
|
||||||
}
|
|
||||||
}
|
|
@ -19,7 +19,6 @@ import {
|
|||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { css, html, LitElement, PropertyValues } from "lit";
|
import { css, html, LitElement, PropertyValues } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { ensureArray } from "../../common/ensure-array";
|
import { ensureArray } from "../../common/ensure-array";
|
||||||
import { Condition, Trigger } from "../../data/automation";
|
import { Condition, Trigger } from "../../data/automation";
|
||||||
@ -41,9 +40,15 @@ import {
|
|||||||
TraceExtended,
|
TraceExtended,
|
||||||
} from "../../data/trace";
|
} from "../../data/trace";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import { NodeInfo, NODE_SIZE, SPACING } from "./hat-graph";
|
|
||||||
import "./hat-graph-node";
|
import "./hat-graph-node";
|
||||||
import "./hat-graph-spacer";
|
import "./hat-graph-spacer";
|
||||||
|
import "./hat-graph-branch";
|
||||||
|
import { NODE_SIZE, SPACING, BRANCH_HEIGHT } from "./hat-graph-const";
|
||||||
|
|
||||||
|
export interface NodeInfo {
|
||||||
|
path: string;
|
||||||
|
config: any;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@ -52,14 +57,14 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hat-script-graph")
|
@customElement("hat-script-graph")
|
||||||
class HatScriptGraph extends LitElement {
|
export class HatScriptGraph extends LitElement {
|
||||||
@property({ attribute: false }) public trace!: TraceExtended;
|
@property({ attribute: false }) public trace!: TraceExtended;
|
||||||
|
|
||||||
@property({ attribute: false }) public selected;
|
@property({ attribute: false }) public selected?: string;
|
||||||
|
|
||||||
@property() renderedNodes: Record<string, any> = {};
|
public renderedNodes: Record<string, NodeInfo> = {};
|
||||||
|
|
||||||
@property() trackedNodes: Record<string, any> = {};
|
public trackedNodes: Record<string, NodeInfo> = {};
|
||||||
|
|
||||||
private selectNode(config, path) {
|
private selectNode(config, path) {
|
||||||
return () => {
|
return () => {
|
||||||
@ -69,72 +74,54 @@ class HatScriptGraph extends LitElement {
|
|||||||
|
|
||||||
private render_trigger(config: Trigger, i: number) {
|
private render_trigger(config: Trigger, i: number) {
|
||||||
const path = `trigger/${i}`;
|
const path = `trigger/${i}`;
|
||||||
const tracked = this.trace && path in this.trace.trace;
|
const track = this.trace && path in this.trace.trace;
|
||||||
this.renderedNodes[path] = { config, path };
|
this.renderedNodes[path] = { config, path };
|
||||||
if (tracked) {
|
if (track) {
|
||||||
this.trackedNodes[path] = this.renderedNodes[path];
|
this.trackedNodes[path] = this.renderedNodes[path];
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<hat-graph-node
|
<hat-graph-node
|
||||||
graphStart
|
graphStart
|
||||||
|
?track=${track}
|
||||||
@focus=${this.selectNode(config, path)}
|
@focus=${this.selectNode(config, path)}
|
||||||
class=${classMap({
|
?active=${this.selected === path}
|
||||||
track: tracked,
|
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
.iconPath=${mdiAsterisk}
|
.iconPath=${mdiAsterisk}
|
||||||
tabindex=${tracked ? "0" : "-1"}
|
tabindex=${track ? "0" : "-1"}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private render_condition(config: Condition, i: number) {
|
private render_condition(config: Condition, i: number) {
|
||||||
const path = `condition/${i}`;
|
const path = `condition/${i}`;
|
||||||
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
|
|
||||||
const track_path =
|
|
||||||
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
|
|
||||||
this.renderedNodes[path] = { config, path };
|
this.renderedNodes[path] = { config, path };
|
||||||
if (trace) {
|
if (this.trace && path in this.trace.trace) {
|
||||||
this.trackedNodes[path] = this.renderedNodes[path];
|
this.trackedNodes[path] = this.renderedNodes[path];
|
||||||
}
|
}
|
||||||
return html`
|
return this.render_condition_node(config, path);
|
||||||
<hat-graph
|
}
|
||||||
branching
|
|
||||||
@focus=${this.selectNode(config, path)}
|
private typeRenderers = {
|
||||||
class=${classMap({
|
condition: this.render_condition_node,
|
||||||
track: track_path,
|
delay: this.render_delay_node,
|
||||||
active: this.selected === path,
|
event: this.render_event_node,
|
||||||
})}
|
scene: this.render_scene_node,
|
||||||
.track_start=${[track_path]}
|
service: this.render_service_node,
|
||||||
.track_end=${[track_path]}
|
wait_template: this.render_wait_node,
|
||||||
tabindex=${trace ? "-1" : "0"}
|
wait_for_trigger: this.render_wait_node,
|
||||||
short
|
repeat: this.render_repeat_node,
|
||||||
>
|
choose: this.render_choose_node,
|
||||||
<hat-graph-node
|
device_id: this.render_device_node,
|
||||||
slot="head"
|
other: this.render_other_node,
|
||||||
class=${classMap({
|
};
|
||||||
track: trace !== undefined,
|
|
||||||
})}
|
private render_action_node(node: Action, path: string, graphStart = false) {
|
||||||
.iconPath=${mdiAbTesting}
|
const type =
|
||||||
nofocus
|
Object.keys(this.typeRenderers).find((key) => key in node) || "other";
|
||||||
graphEnd
|
this.renderedNodes[path] = { config: node, path };
|
||||||
></hat-graph-node>
|
if (this.trace && path in this.trace.trace) {
|
||||||
<div
|
this.trackedNodes[path] = this.renderedNodes[path];
|
||||||
style=${`width: ${NODE_SIZE + SPACING}px;`}
|
}
|
||||||
graphStart
|
return this.typeRenderers[type].bind(this)(node, path, graphStart);
|
||||||
graphEnd
|
|
||||||
></div>
|
|
||||||
<div></div>
|
|
||||||
<hat-graph-node
|
|
||||||
.iconPath=${mdiClose}
|
|
||||||
graphEnd
|
|
||||||
nofocus
|
|
||||||
class=${classMap({
|
|
||||||
track: track_path === 2,
|
|
||||||
})}
|
|
||||||
></hat-graph-node>
|
|
||||||
</hat-graph>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private render_choose_node(
|
private render_choose_node(
|
||||||
@ -143,30 +130,26 @@ class HatScriptGraph extends LitElement {
|
|||||||
graphStart = false
|
graphStart = false
|
||||||
) {
|
) {
|
||||||
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
|
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
|
||||||
const trace_path =
|
const trace_path = trace
|
||||||
trace !== undefined
|
? trace.map((trc) =>
|
||||||
? trace[0].result === undefined || trace[0].result.choice === "default"
|
trc.result === undefined || trc.result.choice === "default"
|
||||||
? [Array.isArray(config.choose) ? config.choose.length : 0]
|
? "default"
|
||||||
: [trace[0].result.choice]
|
: trc.result.choice
|
||||||
: [];
|
)
|
||||||
|
: [];
|
||||||
|
const track_default = trace_path.includes("default");
|
||||||
return html`
|
return html`
|
||||||
<hat-graph
|
<hat-graph-branch
|
||||||
tabindex=${trace === undefined ? "-1" : "0"}
|
tabindex=${trace === undefined ? "-1" : "0"}
|
||||||
branching
|
|
||||||
.track_start=${trace_path}
|
|
||||||
.track_end=${trace_path}
|
|
||||||
@focus=${this.selectNode(config, path)}
|
@focus=${this.selectNode(config, path)}
|
||||||
class=${classMap({
|
?track=${trace !== undefined}
|
||||||
track: trace !== undefined,
|
?active=${this.selected === path}
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<hat-graph-node
|
<hat-graph-node
|
||||||
.graphStart=${graphStart}
|
.graphStart=${graphStart}
|
||||||
.iconPath=${mdiCallSplit}
|
.iconPath=${mdiCallSplit}
|
||||||
class=${classMap({
|
?track=${trace !== undefined}
|
||||||
track: trace !== undefined,
|
?active=${this.selected === path}
|
||||||
})}
|
|
||||||
slot="head"
|
slot="head"
|
||||||
nofocus
|
nofocus
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
@ -174,46 +157,39 @@ class HatScriptGraph extends LitElement {
|
|||||||
${config.choose
|
${config.choose
|
||||||
? ensureArray(config.choose)?.map((branch, i) => {
|
? ensureArray(config.choose)?.map((branch, i) => {
|
||||||
const branch_path = `${path}/choose/${i}`;
|
const branch_path = `${path}/choose/${i}`;
|
||||||
const track_this =
|
const track_this = trace_path.includes(i);
|
||||||
trace !== undefined && trace[0].result?.choice === i;
|
|
||||||
this.renderedNodes[branch_path] = { config, path: branch_path };
|
this.renderedNodes[branch_path] = { config, path: branch_path };
|
||||||
if (track_this) {
|
if (track_this) {
|
||||||
this.trackedNodes[branch_path] =
|
this.trackedNodes[branch_path] =
|
||||||
this.renderedNodes[branch_path];
|
this.renderedNodes[branch_path];
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<hat-graph>
|
<div class="graph-container" ?track=${track_this}>
|
||||||
<hat-graph-node
|
<hat-graph-node
|
||||||
.iconPath=${!trace || track_this
|
.iconPath=${!trace || track_this
|
||||||
? mdiCheckBoxOutline
|
? mdiCheckBoxOutline
|
||||||
: mdiCheckboxBlankOutline}
|
: mdiCheckboxBlankOutline}
|
||||||
@focus=${this.selectNode(config, branch_path)}
|
@focus=${this.selectNode(config, branch_path)}
|
||||||
class=${classMap({
|
?track=${track_this}
|
||||||
active: this.selected === branch_path,
|
?active=${this.selected === branch_path}
|
||||||
track: track_this,
|
|
||||||
})}
|
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
${ensureArray(branch.sequence).map((action, j) =>
|
${ensureArray(branch.sequence).map((action, j) =>
|
||||||
this.render_node(action, `${branch_path}/sequence/${j}`)
|
this.render_action_node(
|
||||||
|
action,
|
||||||
|
`${branch_path}/sequence/${j}`
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</hat-graph>
|
</div>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
: ""}
|
: ""}
|
||||||
<hat-graph>
|
<div ?track=${track_default}>
|
||||||
<hat-graph-spacer
|
<hat-graph-spacer ?track=${track_default}></hat-graph-spacer>
|
||||||
class=${classMap({
|
|
||||||
track:
|
|
||||||
trace !== undefined &&
|
|
||||||
(trace[0].result === undefined ||
|
|
||||||
trace[0].result.choice === "default"),
|
|
||||||
})}
|
|
||||||
></hat-graph-spacer>
|
|
||||||
${ensureArray(config.default)?.map((action, i) =>
|
${ensureArray(config.default)?.map((action, i) =>
|
||||||
this.render_node(action, `${path}/default/${i}`)
|
this.render_action_node(action, `${path}/default/${i}`)
|
||||||
)}
|
)}
|
||||||
</hat-graph>
|
</div>
|
||||||
</hat-graph>
|
</hat-graph-branch>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,41 +198,54 @@ class HatScriptGraph extends LitElement {
|
|||||||
path: string,
|
path: string,
|
||||||
graphStart = false
|
graphStart = false
|
||||||
) {
|
) {
|
||||||
const trace = (this.trace.trace[path] as ConditionTraceStep[]) || undefined;
|
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
|
||||||
const track_path =
|
let track = false;
|
||||||
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
|
let trackPass = false;
|
||||||
|
let trackFailed = false;
|
||||||
|
if (trace) {
|
||||||
|
for (const trc of trace) {
|
||||||
|
if (trc.result) {
|
||||||
|
track = true;
|
||||||
|
if (trc.result.result) {
|
||||||
|
trackPass = true;
|
||||||
|
} else {
|
||||||
|
trackFailed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (trackPass && trackFailed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<hat-graph
|
<hat-graph-branch
|
||||||
branching
|
|
||||||
@focus=${this.selectNode(node, path)}
|
@focus=${this.selectNode(node, path)}
|
||||||
class=${classMap({
|
?track=${track}
|
||||||
track: track_path,
|
?active=${this.selected === path}
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
.track_start=${[track_path]}
|
|
||||||
.track_end=${[track_path]}
|
|
||||||
tabindex=${trace === undefined ? "-1" : "0"}
|
tabindex=${trace === undefined ? "-1" : "0"}
|
||||||
short
|
short
|
||||||
>
|
>
|
||||||
<hat-graph-node
|
<hat-graph-node
|
||||||
.graphStart=${graphStart}
|
.graphStart=${graphStart}
|
||||||
slot="head"
|
slot="head"
|
||||||
class=${classMap({
|
?track=${track}
|
||||||
track: Boolean(trace),
|
?active=${this.selected === path}
|
||||||
})}
|
|
||||||
.iconPath=${mdiAbTesting}
|
.iconPath=${mdiAbTesting}
|
||||||
nofocus
|
nofocus
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
<div style=${`width: ${NODE_SIZE + SPACING}px;`}></div>
|
<div
|
||||||
<div></div>
|
style=${`width: ${NODE_SIZE + SPACING}px;`}
|
||||||
|
graphStart
|
||||||
|
graphEnd
|
||||||
|
></div>
|
||||||
|
<div ?track=${trackPass}></div>
|
||||||
<hat-graph-node
|
<hat-graph-node
|
||||||
.iconPath=${mdiClose}
|
.iconPath=${mdiClose}
|
||||||
nofocus
|
nofocus
|
||||||
class=${classMap({
|
?track=${trackFailed}
|
||||||
track: track_path === 2,
|
?active=${this.selected === path}
|
||||||
})}
|
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
</hat-graph>
|
</hat-graph-branch>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,10 +259,8 @@ class HatScriptGraph extends LitElement {
|
|||||||
.graphStart=${graphStart}
|
.graphStart=${graphStart}
|
||||||
.iconPath=${mdiTimerOutline}
|
.iconPath=${mdiTimerOutline}
|
||||||
@focus=${this.selectNode(node, path)}
|
@focus=${this.selectNode(node, path)}
|
||||||
class=${classMap({
|
?track=${path in this.trace.trace}
|
||||||
track: path in this.trace.trace,
|
?active=${this.selected === path}
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
@ -289,10 +276,8 @@ class HatScriptGraph extends LitElement {
|
|||||||
.graphStart=${graphStart}
|
.graphStart=${graphStart}
|
||||||
.iconPath=${mdiDevices}
|
.iconPath=${mdiDevices}
|
||||||
@focus=${this.selectNode(node, path)}
|
@focus=${this.selectNode(node, path)}
|
||||||
class=${classMap({
|
?track=${path in this.trace.trace}
|
||||||
track: path in this.trace.trace,
|
?active=${this.selected === path}
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
@ -308,10 +293,8 @@ class HatScriptGraph extends LitElement {
|
|||||||
.graphStart=${graphStart}
|
.graphStart=${graphStart}
|
||||||
.iconPath=${mdiExclamation}
|
.iconPath=${mdiExclamation}
|
||||||
@focus=${this.selectNode(node, path)}
|
@focus=${this.selectNode(node, path)}
|
||||||
class=${classMap({
|
?track=${path in this.trace.trace}
|
||||||
track: path in this.trace.trace,
|
?active=${this.selected === path}
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
@ -323,43 +306,35 @@ class HatScriptGraph extends LitElement {
|
|||||||
graphStart = false
|
graphStart = false
|
||||||
) {
|
) {
|
||||||
const trace: any = this.trace.trace[path];
|
const trace: any = this.trace.trace[path];
|
||||||
const track_path = trace ? [0, 1] : [];
|
|
||||||
const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length;
|
const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length;
|
||||||
return html`
|
return html`
|
||||||
<hat-graph
|
<hat-graph-branch
|
||||||
.track_start=${track_path}
|
|
||||||
.track_end=${track_path}
|
|
||||||
tabindex=${trace === undefined ? "-1" : "0"}
|
tabindex=${trace === undefined ? "-1" : "0"}
|
||||||
branching
|
|
||||||
@focus=${this.selectNode(node, path)}
|
@focus=${this.selectNode(node, path)}
|
||||||
class=${classMap({
|
?track=${path in this.trace.trace}
|
||||||
track: path in this.trace.trace,
|
?active=${this.selected === path}
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<hat-graph-node
|
<hat-graph-node
|
||||||
.graphStart=${graphStart}
|
.graphStart=${graphStart}
|
||||||
.iconPath=${mdiRefresh}
|
.iconPath=${mdiRefresh}
|
||||||
class=${classMap({
|
?track=${path in this.trace.trace}
|
||||||
track: trace,
|
?active=${this.selected === path}
|
||||||
})}
|
|
||||||
slot="head"
|
slot="head"
|
||||||
nofocus
|
nofocus
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
<hat-graph-node
|
<hat-graph-node
|
||||||
.iconPath=${mdiArrowUp}
|
.iconPath=${mdiArrowUp}
|
||||||
|
?track=${repeats > 1}
|
||||||
|
?active=${this.selected === path}
|
||||||
nofocus
|
nofocus
|
||||||
class=${classMap({
|
.badge=${repeats > 1 ? repeats : undefined}
|
||||||
track: track_path.includes(1),
|
|
||||||
})}
|
|
||||||
.badge=${repeats}
|
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
<hat-graph>
|
<div ?track=${trace}>
|
||||||
${ensureArray(node.repeat.sequence).map((action, i) =>
|
${ensureArray(node.repeat.sequence).map((action, i) =>
|
||||||
this.render_node(action, `${path}/repeat/sequence/${i}`)
|
this.render_action_node(action, `${path}/repeat/sequence/${i}`)
|
||||||
)}
|
)}
|
||||||
</hat-graph>
|
</div>
|
||||||
</hat-graph>
|
</hat-graph-branch>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,10 +348,8 @@ class HatScriptGraph extends LitElement {
|
|||||||
.graphStart=${graphStart}
|
.graphStart=${graphStart}
|
||||||
.iconPath=${mdiExclamation}
|
.iconPath=${mdiExclamation}
|
||||||
@focus=${this.selectNode(node, path)}
|
@focus=${this.selectNode(node, path)}
|
||||||
class=${classMap({
|
?track=${path in this.trace.trace}
|
||||||
track: path in this.trace.trace,
|
?active=${this.selected === path}
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
@ -392,10 +365,8 @@ class HatScriptGraph extends LitElement {
|
|||||||
.graphStart=${graphStart}
|
.graphStart=${graphStart}
|
||||||
.iconPath=${mdiChevronRight}
|
.iconPath=${mdiChevronRight}
|
||||||
@focus=${this.selectNode(node, path)}
|
@focus=${this.selectNode(node, path)}
|
||||||
class=${classMap({
|
?track=${path in this.trace.trace}
|
||||||
track: path in this.trace.trace,
|
?active=${this.selected === path}
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
@ -411,10 +382,8 @@ class HatScriptGraph extends LitElement {
|
|||||||
.graphStart=${graphStart}
|
.graphStart=${graphStart}
|
||||||
.iconPath=${mdiTrafficLight}
|
.iconPath=${mdiTrafficLight}
|
||||||
@focus=${this.selectNode(node, path)}
|
@focus=${this.selectNode(node, path)}
|
||||||
class=${classMap({
|
?track=${path in this.trace.trace}
|
||||||
track: path in this.trace.trace,
|
?active=${this.selected === path}
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
@ -426,95 +395,55 @@ class HatScriptGraph extends LitElement {
|
|||||||
.graphStart=${graphStart}
|
.graphStart=${graphStart}
|
||||||
.iconPath=${mdiCodeBrackets}
|
.iconPath=${mdiCodeBrackets}
|
||||||
@focus=${this.selectNode(node, path)}
|
@focus=${this.selectNode(node, path)}
|
||||||
class=${classMap({
|
?track=${path in this.trace.trace}
|
||||||
track: path in this.trace.trace,
|
?active=${this.selected === path}
|
||||||
active: this.selected === path,
|
|
||||||
})}
|
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private render_node(node: Action, path: string, graphStart = false) {
|
|
||||||
const NODE_TYPES = {
|
|
||||||
choose: this.render_choose_node,
|
|
||||||
condition: this.render_condition_node,
|
|
||||||
delay: this.render_delay_node,
|
|
||||||
device_id: this.render_device_node,
|
|
||||||
event: this.render_event_node,
|
|
||||||
repeat: this.render_repeat_node,
|
|
||||||
scene: this.render_scene_node,
|
|
||||||
service: this.render_service_node,
|
|
||||||
wait_template: this.render_wait_node,
|
|
||||||
wait_for_trigger: this.render_wait_node,
|
|
||||||
other: this.render_other_node,
|
|
||||||
};
|
|
||||||
|
|
||||||
const type = Object.keys(NODE_TYPES).find((key) => key in node) || "other";
|
|
||||||
const nodeEl = NODE_TYPES[type].bind(this)(node, path, graphStart);
|
|
||||||
this.renderedNodes[path] = { config: node, path };
|
|
||||||
if (this.trace && path in this.trace.trace) {
|
|
||||||
this.trackedNodes[path] = this.renderedNodes[path];
|
|
||||||
}
|
|
||||||
return nodeEl;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const paths = Object.keys(this.trackedNodes);
|
const paths = Object.keys(this.trackedNodes);
|
||||||
const manual_triggered = this.trace && "trigger" in this.trace.trace;
|
|
||||||
let track_path = manual_triggered ? undefined : [0];
|
|
||||||
const trigger_nodes =
|
const trigger_nodes =
|
||||||
"trigger" in this.trace.config
|
"trigger" in this.trace.config
|
||||||
? ensureArray(this.trace.config.trigger).map((trigger, i) => {
|
? ensureArray(this.trace.config.trigger).map((trigger, i) =>
|
||||||
if (this.trace && `trigger/${i}` in this.trace.trace) {
|
this.render_trigger(trigger, i)
|
||||||
track_path = [i];
|
)
|
||||||
}
|
|
||||||
return this.render_trigger(trigger, i);
|
|
||||||
})
|
|
||||||
: undefined;
|
: undefined;
|
||||||
try {
|
try {
|
||||||
return html`
|
return html`
|
||||||
<hat-graph class="parent">
|
<div class="parent graph-container">
|
||||||
<div></div>
|
|
||||||
${trigger_nodes
|
${trigger_nodes
|
||||||
? html`<hat-graph
|
? html`<hat-graph-branch start .short=${trigger_nodes.length < 2}>
|
||||||
branching
|
|
||||||
id="trigger"
|
|
||||||
.short=${trigger_nodes.length < 2}
|
|
||||||
.track_start=${track_path}
|
|
||||||
.track_end=${track_path}
|
|
||||||
>
|
|
||||||
${trigger_nodes}
|
${trigger_nodes}
|
||||||
</hat-graph>`
|
</hat-graph-branch>`
|
||||||
: ""}
|
: ""}
|
||||||
${"condition" in this.trace.config
|
${"condition" in this.trace.config
|
||||||
? html`<hat-graph id="condition">
|
? html`${ensureArray(this.trace.config.condition)?.map(
|
||||||
${ensureArray(this.trace.config.condition)?.map(
|
(condition, i) => this.render_condition(condition, i)
|
||||||
(condition, i) => this.render_condition(condition!, i)
|
)}`
|
||||||
)}
|
|
||||||
</hat-graph>`
|
|
||||||
: ""}
|
: ""}
|
||||||
${"action" in this.trace.config
|
${"action" in this.trace.config
|
||||||
? html`${ensureArray(this.trace.config.action).map((action, i) =>
|
? html`${ensureArray(this.trace.config.action).map((action, i) =>
|
||||||
this.render_node(action, `action/${i}`)
|
this.render_action_node(action, `action/${i}`)
|
||||||
)}`
|
)}`
|
||||||
: ""}
|
: ""}
|
||||||
${"sequence" in this.trace.config
|
${"sequence" in this.trace.config
|
||||||
? html`${ensureArray(this.trace.config.sequence).map((action, i) =>
|
? html`${ensureArray(this.trace.config.sequence).map((action, i) =>
|
||||||
this.render_node(action, `sequence/${i}`, i === 0)
|
this.render_action_node(action, `sequence/${i}`, i === 0)
|
||||||
)}`
|
)}`
|
||||||
: ""}
|
: ""}
|
||||||
</hat-graph>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<mwc-icon-button
|
<mwc-icon-button
|
||||||
.disabled=${paths.length === 0 || paths[0] === this.selected}
|
.disabled=${paths.length === 0 || paths[0] === this.selected}
|
||||||
@click=${this.previousTrackedNode}
|
@click=${this._previousTrackedNode}
|
||||||
>
|
>
|
||||||
<ha-svg-icon .path=${mdiChevronUp}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiChevronUp}></ha-svg-icon>
|
||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
<mwc-icon-button
|
<mwc-icon-button
|
||||||
.disabled=${paths.length === 0 ||
|
.disabled=${paths.length === 0 ||
|
||||||
paths[paths.length - 1] === this.selected}
|
paths[paths.length - 1] === this.selected}
|
||||||
@click=${this.nextTrackedNode}
|
@click=${this._nextTrackedNode}
|
||||||
>
|
>
|
||||||
<ha-svg-icon .path=${mdiChevronDown}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiChevronDown}></ha-svg-icon>
|
||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
@ -545,44 +474,39 @@ class HatScriptGraph extends LitElement {
|
|||||||
protected updated(changedProps: PropertyValues<this>) {
|
protected updated(changedProps: PropertyValues<this>) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
// Select first node if new trace loaded but no selection given.
|
if (!changedProps.has("trace")) {
|
||||||
if (changedProps.has("trace")) {
|
return;
|
||||||
const tracked = this.trackedNodes;
|
}
|
||||||
const paths = Object.keys(tracked);
|
|
||||||
|
|
||||||
// If trace changed and we have no or an invalid selection, select first option.
|
// If trace changed and we have no or an invalid selection, select first option.
|
||||||
if (this.selected === "" || !(this.selected in paths)) {
|
if (!this.selected || !(this.selected in this.trackedNodes)) {
|
||||||
// Find first tracked node with node info
|
const firstNode = this.trackedNodes[Object.keys(this.trackedNodes)[0]];
|
||||||
for (const path of paths) {
|
if (firstNode) {
|
||||||
if (tracked[path]) {
|
fireEvent(this, "graph-node-selected", firstNode);
|
||||||
fireEvent(this, "graph-node-selected", tracked[path]);
|
}
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
|
if (this.trace) {
|
||||||
|
const sortKeys = Object.keys(this.trace.trace);
|
||||||
|
const keys = Object.keys(this.renderedNodes).sort(
|
||||||
|
(a, b) => sortKeys.indexOf(a) - sortKeys.indexOf(b)
|
||||||
|
);
|
||||||
|
const sortedTrackedNodes = {};
|
||||||
|
const sortedRenderedNodes = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
sortedRenderedNodes[key] = this.renderedNodes[key];
|
||||||
|
if (key in this.trackedNodes) {
|
||||||
|
sortedTrackedNodes[key] = this.trackedNodes[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.renderedNodes = sortedRenderedNodes;
|
||||||
if (this.trace) {
|
this.trackedNodes = sortedTrackedNodes;
|
||||||
const sortKeys = Object.keys(this.trace.trace);
|
|
||||||
const keys = Object.keys(this.renderedNodes).sort(
|
|
||||||
(a, b) => sortKeys.indexOf(a) - sortKeys.indexOf(b)
|
|
||||||
);
|
|
||||||
const sortedTrackedNodes = {};
|
|
||||||
const sortedRenderedNodes = {};
|
|
||||||
for (const key of keys) {
|
|
||||||
sortedRenderedNodes[key] = this.renderedNodes[key];
|
|
||||||
if (key in this.trackedNodes) {
|
|
||||||
sortedTrackedNodes[key] = this.trackedNodes[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.renderedNodes = sortedRenderedNodes;
|
|
||||||
this.trackedNodes = sortedTrackedNodes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public previousTrackedNode() {
|
private _previousTrackedNode() {
|
||||||
const nodes = Object.keys(this.trackedNodes);
|
const nodes = Object.keys(this.trackedNodes);
|
||||||
const prevIndex = nodes.indexOf(this.selected) - 1;
|
const prevIndex = nodes.indexOf(this.selected!) - 1;
|
||||||
if (prevIndex >= 0) {
|
if (prevIndex >= 0) {
|
||||||
fireEvent(
|
fireEvent(
|
||||||
this,
|
this,
|
||||||
@ -592,9 +516,9 @@ class HatScriptGraph extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public nextTrackedNode() {
|
private _nextTrackedNode() {
|
||||||
const nodes = Object.keys(this.trackedNodes);
|
const nodes = Object.keys(this.trackedNodes);
|
||||||
const nextIndex = nodes.indexOf(this.selected) + 1;
|
const nextIndex = nodes.indexOf(this.selected!) + 1;
|
||||||
if (nextIndex < nodes.length) {
|
if (nextIndex < nodes.length) {
|
||||||
fireEvent(
|
fireEvent(
|
||||||
this,
|
this,
|
||||||
@ -608,6 +532,25 @@ class HatScriptGraph extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
--stroke-clr: var(--stroke-color, var(--secondary-text-color));
|
||||||
|
--active-clr: var(--active-color, var(--primary-color));
|
||||||
|
--track-clr: var(--track-color, var(--accent-color));
|
||||||
|
--hover-clr: var(--hover-color, var(--primary-color));
|
||||||
|
--disabled-clr: var(--disabled-color, var(--disabled-text-color));
|
||||||
|
--default-trigger-color: 3, 169, 244;
|
||||||
|
--rgb-trigger-color: var(--trigger-color, var(--default-trigger-color));
|
||||||
|
--background-clr: var(--background-color, white);
|
||||||
|
--default-icon-clr: var(--icon-color, black);
|
||||||
|
--icon-clr: var(--stroke-clr);
|
||||||
|
|
||||||
|
--hat-graph-spacing: ${SPACING}px;
|
||||||
|
--hat-graph-node-size: ${NODE_SIZE}px;
|
||||||
|
--hat-graph-branch-height: ${BRANCH_HEIGHT}px;
|
||||||
|
}
|
||||||
|
.graph-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -46,9 +46,11 @@ export interface BlueprintAutomationConfig extends ManualAutomationConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ForDict {
|
export interface ForDict {
|
||||||
hours?: number | string;
|
days?: number;
|
||||||
minutes?: number | string;
|
hours?: number;
|
||||||
seconds?: number | string;
|
minutes?: number;
|
||||||
|
seconds?: number;
|
||||||
|
milliseconds?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContextConstraint {
|
export interface ContextConstraint {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { HA_COLOR_PALETTE } from "../common/const";
|
import { getColorByIndex } from "../common/color/colors";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
import type { CalendarEvent, HomeAssistant } from "../types";
|
import type { CalendarEvent, HomeAssistant } from "../types";
|
||||||
@ -81,5 +81,5 @@ export const getCalendars = (hass: HomeAssistant): Calendar[] =>
|
|||||||
.map((eid, idx) => ({
|
.map((eid, idx) => ({
|
||||||
entity_id: eid,
|
entity_id: eid,
|
||||||
name: computeStateName(hass.states[eid]),
|
name: computeStateName(hass.states[eid]),
|
||||||
backgroundColor: `#${HA_COLOR_PALETTE[idx % HA_COLOR_PALETTE.length]}`,
|
backgroundColor: getColorByIndex(idx),
|
||||||
}));
|
}));
|
||||||
|
@ -36,17 +36,21 @@ export interface Stream {
|
|||||||
export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
|
export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
|
||||||
`/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`;
|
`/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`;
|
||||||
|
|
||||||
export const fetchThumbnailUrlWithCache = (
|
export const fetchThumbnailUrlWithCache = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string
|
entityId: string,
|
||||||
) =>
|
width: number,
|
||||||
timeCachePromiseFunc(
|
height: number
|
||||||
|
) => {
|
||||||
|
const base_url = await timeCachePromiseFunc(
|
||||||
"_cameraTmbUrl",
|
"_cameraTmbUrl",
|
||||||
9000,
|
9000,
|
||||||
fetchThumbnailUrl,
|
fetchThumbnailUrl,
|
||||||
hass,
|
hass,
|
||||||
entityId
|
entityId
|
||||||
);
|
);
|
||||||
|
return `${base_url}&width=${width}&height=${height}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchThumbnailUrl = async (
|
export const fetchThumbnailUrl = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -6,6 +6,7 @@ import { DataEntryFlowProgress, DataEntryFlowStep } from "./data_entry_flow";
|
|||||||
import { domainToName } from "./integration";
|
import { domainToName } from "./integration";
|
||||||
|
|
||||||
export const DISCOVERY_SOURCES = [
|
export const DISCOVERY_SOURCES = [
|
||||||
|
"usb",
|
||||||
"unignore",
|
"unignore",
|
||||||
"dhcp",
|
"dhcp",
|
||||||
"homekit",
|
"homekit",
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
startOfYesterday,
|
startOfYesterday,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { Collection, getCollection } from "home-assistant-js-websocket";
|
import { Collection, getCollection } from "home-assistant-js-websocket";
|
||||||
|
import { groupBy } from "../common/util/group-by";
|
||||||
import { subscribeOne } from "../common/util/subscribe-one";
|
import { subscribeOne } from "../common/util/subscribe-one";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { ConfigEntry, getConfigEntries } from "./config_entries";
|
import { ConfigEntry, getConfigEntries } from "./config_entries";
|
||||||
@ -47,6 +48,28 @@ export const emptySolarEnergyPreference =
|
|||||||
config_entry_solar_forecast: null,
|
config_entry_solar_forecast: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const emptyBatteryEnergyPreference =
|
||||||
|
(): BatterySourceTypeEnergyPreference => ({
|
||||||
|
type: "battery",
|
||||||
|
stat_energy_from: "",
|
||||||
|
stat_energy_to: "",
|
||||||
|
});
|
||||||
|
export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({
|
||||||
|
type: "gas",
|
||||||
|
stat_energy_from: "",
|
||||||
|
stat_cost: null,
|
||||||
|
entity_energy_from: null,
|
||||||
|
entity_energy_price: null,
|
||||||
|
number_energy_price: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface EnergySolarForecast {
|
||||||
|
wh_hours: Record<string, number>;
|
||||||
|
}
|
||||||
|
export type EnergySolarForecasts = {
|
||||||
|
[config_entry_id: string]: EnergySolarForecast;
|
||||||
|
};
|
||||||
|
|
||||||
export interface DeviceConsumptionEnergyPreference {
|
export interface DeviceConsumptionEnergyPreference {
|
||||||
// This is an ever increasing value
|
// This is an ever increasing value
|
||||||
stat_consumption: string;
|
stat_consumption: string;
|
||||||
@ -94,9 +117,31 @@ export interface SolarSourceTypeEnergyPreference {
|
|||||||
config_entry_solar_forecast: string[] | null;
|
config_entry_solar_forecast: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BatterySourceTypeEnergyPreference {
|
||||||
|
type: "battery";
|
||||||
|
stat_energy_from: string;
|
||||||
|
stat_energy_to: string;
|
||||||
|
}
|
||||||
|
export interface GasSourceTypeEnergyPreference {
|
||||||
|
type: "gas";
|
||||||
|
|
||||||
|
// kWh meter
|
||||||
|
stat_energy_from: string;
|
||||||
|
|
||||||
|
// $ meter
|
||||||
|
stat_cost: string | null;
|
||||||
|
|
||||||
|
// Can be used to generate costs if stat_cost omitted
|
||||||
|
entity_energy_from: string | null;
|
||||||
|
entity_energy_price: string | null;
|
||||||
|
number_energy_price: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
type EnergySource =
|
type EnergySource =
|
||||||
| SolarSourceTypeEnergyPreference
|
| SolarSourceTypeEnergyPreference
|
||||||
| GridSourceTypeEnergyPreference;
|
| GridSourceTypeEnergyPreference
|
||||||
|
| BatterySourceTypeEnergyPreference
|
||||||
|
| GasSourceTypeEnergyPreference;
|
||||||
|
|
||||||
export interface EnergyPreferences {
|
export interface EnergyPreferences {
|
||||||
energy_sources: EnergySource[];
|
energy_sources: EnergySource[];
|
||||||
@ -105,6 +150,18 @@ export interface EnergyPreferences {
|
|||||||
|
|
||||||
export interface EnergyInfo {
|
export interface EnergyInfo {
|
||||||
cost_sensors: Record<string, string>;
|
cost_sensors: Record<string, string>;
|
||||||
|
solar_forecast_domains: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnergyValidationIssue {
|
||||||
|
type: string;
|
||||||
|
identifier: string;
|
||||||
|
value?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnergyPreferencesValidation {
|
||||||
|
energy_sources: EnergyValidationIssue[][];
|
||||||
|
device_consumption: EnergyValidationIssue[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEnergyInfo = (hass: HomeAssistant) =>
|
export const getEnergyInfo = (hass: HomeAssistant) =>
|
||||||
@ -112,6 +169,11 @@ export const getEnergyInfo = (hass: HomeAssistant) =>
|
|||||||
type: "energy/info",
|
type: "energy/info",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getEnergyPreferenceValidation = (hass: HomeAssistant) =>
|
||||||
|
hass.callWS<EnergyPreferencesValidation>({
|
||||||
|
type: "energy/validate",
|
||||||
|
});
|
||||||
|
|
||||||
export const getEnergyPreferences = (hass: HomeAssistant) =>
|
export const getEnergyPreferences = (hass: HomeAssistant) =>
|
||||||
hass.callWS<EnergyPreferences>({
|
hass.callWS<EnergyPreferences>({
|
||||||
type: "energy/get_prefs",
|
type: "energy/get_prefs",
|
||||||
@ -132,19 +194,12 @@ export const saveEnergyPreferences = async (
|
|||||||
interface EnergySourceByType {
|
interface EnergySourceByType {
|
||||||
grid?: GridSourceTypeEnergyPreference[];
|
grid?: GridSourceTypeEnergyPreference[];
|
||||||
solar?: SolarSourceTypeEnergyPreference[];
|
solar?: SolarSourceTypeEnergyPreference[];
|
||||||
|
battery?: BatterySourceTypeEnergyPreference[];
|
||||||
|
gas?: GasSourceTypeEnergyPreference[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const energySourcesByType = (prefs: EnergyPreferences) => {
|
export const energySourcesByType = (prefs: EnergyPreferences) =>
|
||||||
const types: EnergySourceByType = {};
|
groupBy(prefs.energy_sources, (item) => item.type) as EnergySourceByType;
|
||||||
for (const source of prefs.energy_sources) {
|
|
||||||
if (source.type in types) {
|
|
||||||
types[source.type]!.push(source as any);
|
|
||||||
} else {
|
|
||||||
types[source.type] = [source as any];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return types;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface EnergyData {
|
export interface EnergyData {
|
||||||
start: Date;
|
start: Date;
|
||||||
@ -203,6 +258,24 @@ const getEnergyData = async (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (source.type === "gas") {
|
||||||
|
statIDs.push(source.stat_energy_from);
|
||||||
|
if (source.stat_cost) {
|
||||||
|
statIDs.push(source.stat_cost);
|
||||||
|
}
|
||||||
|
const costStatId = info.cost_sensors[source.stat_energy_from];
|
||||||
|
if (costStatId) {
|
||||||
|
statIDs.push(costStatId);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.type === "battery") {
|
||||||
|
statIDs.push(source.stat_energy_from);
|
||||||
|
statIDs.push(source.stat_energy_to);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// grid source
|
// grid source
|
||||||
for (const flowFrom of source.flow_from) {
|
for (const flowFrom of source.flow_from) {
|
||||||
statIDs.push(flowFrom.stat_energy_from);
|
statIDs.push(flowFrom.stat_energy_from);
|
||||||
@ -375,3 +448,8 @@ export const getEnergyDataCollection = (
|
|||||||
};
|
};
|
||||||
return collection;
|
return collection;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getEnergySolarForecasts = (hass: HomeAssistant) =>
|
||||||
|
hass.callWS<EnergySolarForecasts>({
|
||||||
|
type: "energy/solar_forecast",
|
||||||
|
});
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { HomeAssistant } from "../types";
|
|
||||||
|
|
||||||
export interface ForecastSolarForecast {
|
|
||||||
wh_hours: Record<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getForecastSolarForecasts = (hass: HomeAssistant) =>
|
|
||||||
hass.callWS<Record<string, ForecastSolarForecast>>({
|
|
||||||
type: "forecast_solar/forecasts",
|
|
||||||
});
|
|
233
src/data/hassio/backup.ts
Normal file
233
src/data/hassio/backup.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||||
|
|
||||||
|
export const friendlyFolderName = {
|
||||||
|
ssl: "SSL",
|
||||||
|
homeassistant: "Configuration",
|
||||||
|
"addons/local": "Local add-ons",
|
||||||
|
media: "Media",
|
||||||
|
share: "Share",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BackupContent {
|
||||||
|
homeassistant: boolean;
|
||||||
|
folders: string[];
|
||||||
|
addons: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HassioBackup {
|
||||||
|
slug: string;
|
||||||
|
date: string;
|
||||||
|
name: string;
|
||||||
|
type: "full" | "partial";
|
||||||
|
protected: boolean;
|
||||||
|
content: BackupContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HassioBackupDetail extends HassioBackup {
|
||||||
|
size: number;
|
||||||
|
homeassistant: string;
|
||||||
|
addons: Array<{
|
||||||
|
slug: "ADDON_SLUG";
|
||||||
|
name: "NAME";
|
||||||
|
version: "INSTALLED_VERSION";
|
||||||
|
size: "SIZE_IN_MB";
|
||||||
|
}>;
|
||||||
|
repositories: string[];
|
||||||
|
folders: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HassioFullBackupCreateParams {
|
||||||
|
name: string;
|
||||||
|
password?: string;
|
||||||
|
confirm_password?: string;
|
||||||
|
}
|
||||||
|
export interface HassioPartialBackupCreateParams
|
||||||
|
extends HassioFullBackupCreateParams {
|
||||||
|
folders?: string[];
|
||||||
|
addons?: string[];
|
||||||
|
homeassistant?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchHassioBackups = async (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<HassioBackup[]> => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
const data: {
|
||||||
|
[key: string]: HassioBackup[];
|
||||||
|
} = await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return data[
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return hassioApiResultExtractor(
|
||||||
|
await hass.callApi<HassioResponse<{ snapshots: HassioBackup[] }>>(
|
||||||
|
"GET",
|
||||||
|
`hassio/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
).snapshots;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchHassioBackupInfo = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
backup: string
|
||||||
|
): Promise<HassioBackupDetail> => {
|
||||||
|
if (hass) {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
return hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/${backup}/info`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return hassioApiResultExtractor(
|
||||||
|
await hass.callApi<HassioResponse<HassioBackupDetail>>(
|
||||||
|
"GET",
|
||||||
|
`hassio/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/${backup}/info`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// When called from onboarding we don't have hass
|
||||||
|
const resp = await fetch(`/api/hassio/backups/${backup}/info`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const data = (await resp.json()).data;
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reloadHassioBackups = async (hass: HomeAssistant) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/reload`,
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.callApi<HassioResponse<void>>(
|
||||||
|
"POST",
|
||||||
|
`hassio/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/reload`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createHassioFullBackup = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
data: HassioFullBackupCreateParams
|
||||||
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/new/full`,
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await hass.callApi<HassioResponse<void>>(
|
||||||
|
"POST",
|
||||||
|
`hassio/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/new/full`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeBackup = async (hass: HomeAssistant, slug: string) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/${slug}/remove`,
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await hass.callApi<HassioResponse<void>>(
|
||||||
|
"POST",
|
||||||
|
`hassio/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/${slug}/remove`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createHassioPartialBackup = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
data: HassioPartialBackupCreateParams
|
||||||
|
) => {
|
||||||
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
|
await hass.callWS({
|
||||||
|
type: "supervisor/api",
|
||||||
|
endpoint: `/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/new/partial`,
|
||||||
|
method: "post",
|
||||||
|
timeout: null,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.callApi<HassioResponse<void>>(
|
||||||
|
"POST",
|
||||||
|
`hassio/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/new/partial`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadBackup = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
file: File
|
||||||
|
): Promise<HassioResponse<HassioBackup>> => {
|
||||||
|
const fd = new FormData();
|
||||||
|
let resp;
|
||||||
|
fd.append("file", file);
|
||||||
|
if (hass) {
|
||||||
|
resp = await hass.fetchWithAuth(
|
||||||
|
`/api/hassio/${
|
||||||
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/new/upload`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// When called from onboarding we don't have hass
|
||||||
|
resp = await fetch("/api/hassio/backups/new/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.status === 413) {
|
||||||
|
throw new Error("Uploaded backup is too large");
|
||||||
|
} else if (resp.status !== 200) {
|
||||||
|
throw new Error(`${resp.status} ${resp.statusText}`);
|
||||||
|
}
|
||||||
|
return resp.json();
|
||||||
|
};
|
@ -1,197 +0,0 @@
|
|||||||
import { atLeastVersion } from "../../common/config/version";
|
|
||||||
import { HomeAssistant } from "../../types";
|
|
||||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
|
||||||
|
|
||||||
export const friendlyFolderName = {
|
|
||||||
ssl: "SSL",
|
|
||||||
homeassistant: "Configuration",
|
|
||||||
"addons/local": "Local add-ons",
|
|
||||||
media: "Media",
|
|
||||||
share: "Share",
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SnapshotContent {
|
|
||||||
homeassistant: boolean;
|
|
||||||
folders: string[];
|
|
||||||
addons: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HassioSnapshot {
|
|
||||||
slug: string;
|
|
||||||
date: string;
|
|
||||||
name: string;
|
|
||||||
type: "full" | "partial";
|
|
||||||
protected: boolean;
|
|
||||||
content: SnapshotContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HassioSnapshotDetail extends HassioSnapshot {
|
|
||||||
size: number;
|
|
||||||
homeassistant: string;
|
|
||||||
addons: Array<{
|
|
||||||
slug: "ADDON_SLUG";
|
|
||||||
name: "NAME";
|
|
||||||
version: "INSTALLED_VERSION";
|
|
||||||
size: "SIZE_IN_MB";
|
|
||||||
}>;
|
|
||||||
repositories: string[];
|
|
||||||
folders: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HassioFullSnapshotCreateParams {
|
|
||||||
name: string;
|
|
||||||
password?: string;
|
|
||||||
confirm_password?: string;
|
|
||||||
}
|
|
||||||
export interface HassioPartialSnapshotCreateParams
|
|
||||||
extends HassioFullSnapshotCreateParams {
|
|
||||||
folders?: string[];
|
|
||||||
addons?: string[];
|
|
||||||
homeassistant?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchHassioSnapshots = async (
|
|
||||||
hass: HomeAssistant
|
|
||||||
): Promise<HassioSnapshot[]> => {
|
|
||||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
|
||||||
const data: { snapshots: HassioSnapshot[] } = await hass.callWS({
|
|
||||||
type: "supervisor/api",
|
|
||||||
endpoint: `/snapshots`,
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
return data.snapshots;
|
|
||||||
}
|
|
||||||
|
|
||||||
return hassioApiResultExtractor(
|
|
||||||
await hass.callApi<HassioResponse<{ snapshots: HassioSnapshot[] }>>(
|
|
||||||
"GET",
|
|
||||||
"hassio/snapshots"
|
|
||||||
)
|
|
||||||
).snapshots;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchHassioSnapshotInfo = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
snapshot: string
|
|
||||||
): Promise<HassioSnapshotDetail> => {
|
|
||||||
if (hass) {
|
|
||||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
|
||||||
return hass.callWS({
|
|
||||||
type: "supervisor/api",
|
|
||||||
endpoint: `/snapshots/${snapshot}/info`,
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return hassioApiResultExtractor(
|
|
||||||
await hass.callApi<HassioResponse<HassioSnapshotDetail>>(
|
|
||||||
"GET",
|
|
||||||
`hassio/snapshots/${snapshot}/info`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// When called from onboarding we don't have hass
|
|
||||||
const resp = await fetch(`/api/hassio/snapshots/${snapshot}/info`, {
|
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
const data = (await resp.json()).data;
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const reloadHassioSnapshots = async (hass: HomeAssistant) => {
|
|
||||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
|
||||||
await hass.callWS({
|
|
||||||
type: "supervisor/api",
|
|
||||||
endpoint: "/snapshots/reload",
|
|
||||||
method: "post",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/snapshots/reload`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createHassioFullSnapshot = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
data: HassioFullSnapshotCreateParams
|
|
||||||
) => {
|
|
||||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
|
||||||
await hass.callWS({
|
|
||||||
type: "supervisor/api",
|
|
||||||
endpoint: "/snapshots/new/full",
|
|
||||||
method: "post",
|
|
||||||
timeout: null,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await hass.callApi<HassioResponse<void>>(
|
|
||||||
"POST",
|
|
||||||
`hassio/snapshots/new/full`,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeSnapshot = async (hass: HomeAssistant, slug: string) => {
|
|
||||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
|
||||||
await hass.callWS({
|
|
||||||
type: "supervisor/api",
|
|
||||||
endpoint: `/snapshots/${slug}/remove`,
|
|
||||||
method: "post",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await hass.callApi<HassioResponse<void>>(
|
|
||||||
"POST",
|
|
||||||
`hassio/snapshots/${slug}/remove`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createHassioPartialSnapshot = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
data: HassioPartialSnapshotCreateParams
|
|
||||||
) => {
|
|
||||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
|
||||||
await hass.callWS({
|
|
||||||
type: "supervisor/api",
|
|
||||||
endpoint: "/snapshots/new/partial",
|
|
||||||
method: "post",
|
|
||||||
timeout: null,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await hass.callApi<HassioResponse<void>>(
|
|
||||||
"POST",
|
|
||||||
`hassio/snapshots/new/partial`,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const uploadSnapshot = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
file: File
|
|
||||||
): Promise<HassioResponse<HassioSnapshot>> => {
|
|
||||||
const fd = new FormData();
|
|
||||||
let resp;
|
|
||||||
fd.append("file", file);
|
|
||||||
if (hass) {
|
|
||||||
resp = await hass.fetchWithAuth("/api/hassio/snapshots/new/upload", {
|
|
||||||
method: "POST",
|
|
||||||
body: fd,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// When called from onboarding we don't have hass
|
|
||||||
resp = await fetch("/api/hassio/snapshots/new/upload", {
|
|
||||||
method: "POST",
|
|
||||||
body: fd,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resp.status === 413) {
|
|
||||||
throw new Error("Uploaded snapshot is too large");
|
|
||||||
} else if (resp.status !== 200) {
|
|
||||||
throw new Error(`${resp.status} ${resp.statusText}`);
|
|
||||||
}
|
|
||||||
return resp.json();
|
|
||||||
};
|
|
@ -1,3 +1,4 @@
|
|||||||
|
import { addDays, addMonths, startOfDay, startOfMonth } from "date-fns";
|
||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
@ -238,14 +239,17 @@ export const computeHistory = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateWithUnit = stateInfo.find(
|
const stateWithUnitorStateClass = stateInfo.find(
|
||||||
(state) => state.attributes && "unit_of_measurement" in state.attributes
|
(state) =>
|
||||||
|
state.attributes &&
|
||||||
|
("unit_of_measurement" in state.attributes ||
|
||||||
|
"state_class" in state.attributes)
|
||||||
);
|
);
|
||||||
|
|
||||||
let unit: string | undefined;
|
let unit: string | undefined;
|
||||||
|
|
||||||
if (stateWithUnit) {
|
if (stateWithUnitorStateClass) {
|
||||||
unit = stateWithUnit.attributes.unit_of_measurement;
|
unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " ";
|
||||||
} else {
|
} else {
|
||||||
unit = {
|
unit = {
|
||||||
climate: hass.config.unit_system.temperature,
|
climate: hass.config.unit_system.temperature,
|
||||||
@ -406,3 +410,90 @@ export const calculateStatisticsSumGrowthWithPercentage = (
|
|||||||
|
|
||||||
return sum;
|
return sum;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const reduceSumStatisticsByDay = (
|
||||||
|
values: StatisticValue[]
|
||||||
|
): StatisticValue[] => {
|
||||||
|
if (!values?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result: StatisticValue[] = [];
|
||||||
|
if (
|
||||||
|
values.length > 1 &&
|
||||||
|
new Date(values[0].start).getDate() === new Date(values[1].start).getDate()
|
||||||
|
) {
|
||||||
|
// add init value if the first value isn't end of previous period
|
||||||
|
result.push({
|
||||||
|
...values[0]!,
|
||||||
|
start: startOfMonth(addDays(new Date(values[0].start), -1)).toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let lastValue: StatisticValue;
|
||||||
|
let prevDate: number | undefined;
|
||||||
|
for (const value of values) {
|
||||||
|
const date = new Date(value.start).getDate();
|
||||||
|
if (prevDate === undefined) {
|
||||||
|
prevDate = date;
|
||||||
|
}
|
||||||
|
if (prevDate !== date) {
|
||||||
|
// Last value of the day
|
||||||
|
result.push({
|
||||||
|
...lastValue!,
|
||||||
|
start: startOfDay(new Date(lastValue!.start)).toISOString(),
|
||||||
|
});
|
||||||
|
prevDate = date;
|
||||||
|
}
|
||||||
|
lastValue = value;
|
||||||
|
}
|
||||||
|
// Add final value
|
||||||
|
result.push({
|
||||||
|
...lastValue!,
|
||||||
|
start: startOfDay(new Date(lastValue!.start)).toISOString(),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reduceSumStatisticsByMonth = (
|
||||||
|
values: StatisticValue[]
|
||||||
|
): StatisticValue[] => {
|
||||||
|
if (!values?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result: StatisticValue[] = [];
|
||||||
|
if (
|
||||||
|
values.length > 1 &&
|
||||||
|
new Date(values[0].start).getMonth() ===
|
||||||
|
new Date(values[1].start).getMonth()
|
||||||
|
) {
|
||||||
|
// add init value if the first value isn't end of previous period
|
||||||
|
result.push({
|
||||||
|
...values[0]!,
|
||||||
|
start: startOfMonth(
|
||||||
|
addMonths(new Date(values[0].start), -1)
|
||||||
|
).toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let lastValue: StatisticValue;
|
||||||
|
let prevMonth: number | undefined;
|
||||||
|
for (const value of values) {
|
||||||
|
const month = new Date(value.start).getMonth();
|
||||||
|
if (prevMonth === undefined) {
|
||||||
|
prevMonth = month;
|
||||||
|
}
|
||||||
|
if (prevMonth !== month) {
|
||||||
|
// Last value of the day
|
||||||
|
result.push({
|
||||||
|
...lastValue!,
|
||||||
|
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
|
||||||
|
});
|
||||||
|
prevMonth = month;
|
||||||
|
}
|
||||||
|
lastValue = value;
|
||||||
|
}
|
||||||
|
// Add final value
|
||||||
|
result.push({
|
||||||
|
...lastValue!,
|
||||||
|
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
4
src/data/usb.ts
Normal file
4
src/data/usb.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export const scanUSBDevices = (hass: HomeAssistant) =>
|
||||||
|
hass.callWS({ type: "usb/scan" });
|
@ -24,12 +24,22 @@ export interface ZWaveJSController {
|
|||||||
is_heal_network_active: boolean;
|
is_heal_network_active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ZWaveJSNode {
|
export interface ZWaveJSNodeStatus {
|
||||||
node_id: number;
|
node_id: number;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ZwaveJSNodeMetadata {
|
||||||
|
node_id: number;
|
||||||
|
exclusion: string;
|
||||||
|
inclusion: string;
|
||||||
|
manual: string;
|
||||||
|
wakeup: string;
|
||||||
|
reset: string;
|
||||||
|
device_database_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ZWaveJSNodeConfigParams {
|
export interface ZWaveJSNodeConfigParams {
|
||||||
[key: string]: ZWaveJSNodeConfigParam;
|
[key: string]: ZWaveJSNodeConfigParam;
|
||||||
}
|
}
|
||||||
@ -132,13 +142,24 @@ export const fetchNodeStatus = (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string,
|
entry_id: string,
|
||||||
node_id: number
|
node_id: number
|
||||||
): Promise<ZWaveJSNode> =>
|
): Promise<ZWaveJSNodeStatus> =>
|
||||||
hass.callWS({
|
hass.callWS({
|
||||||
type: "zwave_js/node_status",
|
type: "zwave_js/node_status",
|
||||||
entry_id,
|
entry_id,
|
||||||
node_id,
|
node_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fetchNodeMetadata = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry_id: string,
|
||||||
|
node_id: number
|
||||||
|
): Promise<ZwaveJSNodeMetadata> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "zwave_js/node_metadata",
|
||||||
|
entry_id,
|
||||||
|
node_id,
|
||||||
|
});
|
||||||
|
|
||||||
export const fetchNodeConfigParameters = (
|
export const fetchNodeConfigParameters = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry_id: string,
|
entry_id: string,
|
||||||
|
@ -41,7 +41,12 @@ class StepFlowPickFlow extends LitElement {
|
|||||||
<img
|
<img
|
||||||
slot="item-icon"
|
slot="item-icon"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src=${brandsUrl(flow.handler, "icon", true)}
|
src=${brandsUrl({
|
||||||
|
domain: flow.handler,
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.selectedTheme?.dark,
|
||||||
|
})}
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -96,7 +96,12 @@ class StepFlowPickHandler extends LitElement {
|
|||||||
<img
|
<img
|
||||||
slot="item-icon"
|
slot="item-icon"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src=${brandsUrl(handler.slug, "icon", true)}
|
src=${brandsUrl({
|
||||||
|
domain: handler.slug,
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.selectedTheme?.dark,
|
||||||
|
})}
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -149,14 +149,12 @@ class MoreInfoClimate extends LitElement {
|
|||||||
${stateObj.attributes.humidity} %
|
${stateObj.attributes.humidity} %
|
||||||
</div>
|
</div>
|
||||||
<ha-slider
|
<ha-slider
|
||||||
class="humidity"
|
|
||||||
step="1"
|
step="1"
|
||||||
pin
|
pin
|
||||||
ignore-bar-touch
|
ignore-bar-touch
|
||||||
dir=${rtlDirection}
|
dir=${rtlDirection}
|
||||||
.min=${stateObj.attributes.min_humidity}
|
.min=${stateObj.attributes.min_humidity}
|
||||||
.max=${stateObj.attributes.max_humidity}
|
.max=${stateObj.attributes.max_humidity}
|
||||||
.secondaryProgress=${stateObj.attributes.max_humidity}
|
|
||||||
.value=${stateObj.attributes.humidity}
|
.value=${stateObj.attributes.humidity}
|
||||||
@change=${this._targetHumiditySliderChanged}
|
@change=${this._targetHumiditySliderChanged}
|
||||||
>
|
>
|
||||||
@ -479,11 +477,6 @@ class MoreInfoClimate extends LitElement {
|
|||||||
margin-left: 4%;
|
margin-left: 4%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.humidity {
|
|
||||||
--paper-slider-active-color: var(--paper-blue-400);
|
|
||||||
--paper-slider-secondary-color: var(--paper-blue-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-row {
|
.single-row {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
@ -53,14 +53,12 @@ class MoreInfoHumidifier extends LitElement {
|
|||||||
<div class="single-row">
|
<div class="single-row">
|
||||||
<div class="target-humidity">${stateObj.attributes.humidity} %</div>
|
<div class="target-humidity">${stateObj.attributes.humidity} %</div>
|
||||||
<ha-slider
|
<ha-slider
|
||||||
class="humidity"
|
|
||||||
step="1"
|
step="1"
|
||||||
pin
|
pin
|
||||||
ignore-bar-touch
|
ignore-bar-touch
|
||||||
dir=${rtlDirection}
|
dir=${rtlDirection}
|
||||||
.min=${stateObj.attributes.min_humidity}
|
.min=${stateObj.attributes.min_humidity}
|
||||||
.max=${stateObj.attributes.max_humidity}
|
.max=${stateObj.attributes.max_humidity}
|
||||||
.secondaryProgress=${stateObj.attributes.max_humidity}
|
|
||||||
.value=${stateObj.attributes.humidity}
|
.value=${stateObj.attributes.humidity}
|
||||||
@change=${this._targetHumiditySliderChanged}
|
@change=${this._targetHumiditySliderChanged}
|
||||||
>
|
>
|
||||||
@ -201,11 +199,6 @@ class MoreInfoHumidifier extends LitElement {
|
|||||||
direction: ltr;
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.humidity {
|
|
||||||
--paper-slider-active-color: var(--paper-blue-400);
|
|
||||||
--paper-slider-secondary-color: var(--paper-blue-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-row {
|
.single-row {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
@ -1,191 +0,0 @@
|
|||||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
|
||||||
import "@polymer/paper-input/paper-input";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
/* eslint-plugin-disable lit */
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
|
|
||||||
import "../../../components/ha-date-input";
|
|
||||||
import "../../../components/ha-relative-time";
|
|
||||||
import "../../../components/paper-time-input";
|
|
||||||
|
|
||||||
class DatetimeInput extends PolymerElement {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<div class$="[[computeClassNames(stateObj)]]">
|
|
||||||
<template is="dom-if" if="[[doesHaveDate(stateObj)]]" restamp="">
|
|
||||||
<div>
|
|
||||||
<ha-date-input
|
|
||||||
id="dateInput"
|
|
||||||
label="Date"
|
|
||||||
value="{{selectedDate}}"
|
|
||||||
></ha-date-input>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[doesHaveTime(stateObj)]]" restamp="">
|
|
||||||
<div>
|
|
||||||
<paper-time-input
|
|
||||||
hour="{{selectedHour}}"
|
|
||||||
min="{{selectedMinute}}"
|
|
||||||
format="24"
|
|
||||||
></paper-time-input>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.is_ready = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
stateObj: {
|
|
||||||
type: Object,
|
|
||||||
observer: "stateObjChanged",
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedDate: {
|
|
||||||
type: String,
|
|
||||||
observer: "dateTimeChanged",
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedHour: {
|
|
||||||
type: Number,
|
|
||||||
observer: "dateTimeChanged",
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedMinute: {
|
|
||||||
type: Number,
|
|
||||||
observer: "dateTimeChanged",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
this.is_ready = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Convert the date in the stateObj into a string useable by vaadin-date-picker */
|
|
||||||
getDateString(stateObj) {
|
|
||||||
if (stateObj.state === "unknown") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
let monthFiller;
|
|
||||||
if (stateObj.attributes.month < 10) {
|
|
||||||
monthFiller = "0";
|
|
||||||
} else {
|
|
||||||
monthFiller = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
let dayFiller;
|
|
||||||
if (stateObj.attributes.day < 10) {
|
|
||||||
dayFiller = "0";
|
|
||||||
} else {
|
|
||||||
dayFiller = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
stateObj.attributes.year +
|
|
||||||
"-" +
|
|
||||||
monthFiller +
|
|
||||||
stateObj.attributes.month +
|
|
||||||
"-" +
|
|
||||||
dayFiller +
|
|
||||||
stateObj.attributes.day
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Should fire when any value was changed *by the user*, not b/c of setting
|
|
||||||
* initial values. */
|
|
||||||
dateTimeChanged() {
|
|
||||||
// Check if the change is really coming from the user
|
|
||||||
if (!this.is_ready) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let changed = false;
|
|
||||||
let minuteFiller;
|
|
||||||
|
|
||||||
const serviceData = {
|
|
||||||
entity_id: this.stateObj.entity_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.stateObj.attributes.has_time) {
|
|
||||||
changed =
|
|
||||||
changed ||
|
|
||||||
parseInt(this.selectedMinute) !== this.stateObj.attributes.minute;
|
|
||||||
changed =
|
|
||||||
changed ||
|
|
||||||
parseInt(this.selectedHour) !== this.stateObj.attributes.hour;
|
|
||||||
if (this.selectedMinute < 10) {
|
|
||||||
minuteFiller = "0";
|
|
||||||
} else {
|
|
||||||
minuteFiller = "";
|
|
||||||
}
|
|
||||||
const timeStr =
|
|
||||||
this.selectedHour + ":" + minuteFiller + this.selectedMinute;
|
|
||||||
serviceData.time = timeStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.stateObj.attributes.has_date) {
|
|
||||||
if (this.selectedDate.length === 0) {
|
|
||||||
return; // Date was not set
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateValInput = new Date(this.selectedDate);
|
|
||||||
const dateValState = new Date(
|
|
||||||
this.stateObj.attributes.year,
|
|
||||||
this.stateObj.attributes.month - 1,
|
|
||||||
this.stateObj.attributes.day
|
|
||||||
);
|
|
||||||
|
|
||||||
changed = changed || dateValState !== dateValInput;
|
|
||||||
|
|
||||||
serviceData.date = this.selectedDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
this.hass.callService("input_datetime", "set_datetime", serviceData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stateObjChanged(newVal) {
|
|
||||||
// Set to non-ready s.t. dateTimeChanged does not fire
|
|
||||||
this.is_ready = false;
|
|
||||||
|
|
||||||
if (newVal.attributes.has_time) {
|
|
||||||
this.selectedHour = newVal.attributes.hour;
|
|
||||||
this.selectedMinute = newVal.attributes.minute;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newVal.attributes.has_date) {
|
|
||||||
this.selectedDate = this.getDateString(newVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.is_ready = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
doesHaveDate(stateObj) {
|
|
||||||
return stateObj.attributes.has_date;
|
|
||||||
}
|
|
||||||
|
|
||||||
doesHaveTime(stateObj) {
|
|
||||||
return stateObj.attributes.has_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeClassNames(stateObj) {
|
|
||||||
return (
|
|
||||||
"more-info-input_datetime " +
|
|
||||||
attributeClassNames(stateObj, ["has_time", "has_date"])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("more-info-input_datetime", DatetimeInput);
|
|
103
src/dialogs/more-info/controls/more-info-input_datetime.ts
Normal file
103
src/dialogs/more-info/controls/more-info-input_datetime.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "../../../components/ha-date-input";
|
||||||
|
import "../../../components/ha-time-input";
|
||||||
|
import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
|
||||||
|
import { setInputDateTimeValue } from "../../../data/input_datetime";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
|
@customElement("more-info-input_datetime")
|
||||||
|
class MoreInfoInputDatetime extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.stateObj) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${
|
||||||
|
this.stateObj.attributes.has_date
|
||||||
|
? html`
|
||||||
|
<ha-date-input
|
||||||
|
.value=${`${this.stateObj.attributes.year}-${this.stateObj.attributes.month}-${this.stateObj.attributes.day}`}
|
||||||
|
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
|
||||||
|
@value-changed=${this._dateChanged}
|
||||||
|
>
|
||||||
|
</ha-date-input>
|
||||||
|
`
|
||||||
|
: ``
|
||||||
|
}
|
||||||
|
${
|
||||||
|
this.stateObj.attributes.has_time
|
||||||
|
? html`
|
||||||
|
<ha-time-input
|
||||||
|
.value=${this.stateObj.state === UNKNOWN
|
||||||
|
? ""
|
||||||
|
: this.stateObj.attributes.has_date
|
||||||
|
? this.stateObj.state.split(" ")[1]
|
||||||
|
: this.stateObj.state}
|
||||||
|
.locale=${this.hass.locale}
|
||||||
|
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
|
||||||
|
hide-label
|
||||||
|
@value-changed=${this._timeChanged}
|
||||||
|
@click=${this._stopEventPropagation}
|
||||||
|
></ha-time-input>
|
||||||
|
`
|
||||||
|
: ``
|
||||||
|
}
|
||||||
|
</hui-generic-entity-row>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopEventPropagation(ev: Event): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _timeChanged(ev): void {
|
||||||
|
setInputDateTimeValue(
|
||||||
|
this.hass!,
|
||||||
|
this.stateObj!.entity_id,
|
||||||
|
ev.detail.value,
|
||||||
|
this.stateObj!.attributes.has_date
|
||||||
|
? this.stateObj!.state.split(" ")[0]
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
ev.target.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dateChanged(ev): void {
|
||||||
|
setInputDateTimeValue(
|
||||||
|
this.hass!,
|
||||||
|
this.stateObj!.entity_id,
|
||||||
|
this.stateObj!.attributes.has_time
|
||||||
|
? this.stateObj!.state.split(" ")[1]
|
||||||
|
: undefined,
|
||||||
|
ev.detail.value
|
||||||
|
);
|
||||||
|
|
||||||
|
ev.target.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
ha-date-input + ha-time-input {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"more-info-input_datetime": MoreInfoInputDatetime;
|
||||||
|
}
|
||||||
|
}
|
@ -21,5 +21,5 @@
|
|||||||
"content" in document.createElement("template"))) {
|
"content" in document.createElement("template"))) {
|
||||||
document.write("<script src='/static/polyfills/webcomponents-bundle.js'><"+"/script>");
|
document.write("<script src='/static/polyfills/webcomponents-bundle.js'><"+"/script>");
|
||||||
}
|
}
|
||||||
var isS11_12 = /.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent);
|
var isS11_12 = /(?:.*(?:iPhone|iPad).*OS (?:11|12)_\d)|(?:.*Version\/(?:11|12)(?:\.\d+)*.*Safari\/)/.test(navigator.userAgent);
|
||||||
</script>
|
</script>
|
||||||
|
@ -211,6 +211,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
|||||||
if (!this.hass!.connection.connected) {
|
if (!this.hass!.connection.connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
window.stop();
|
||||||
this.hass!.connection.suspend();
|
this.hass!.connection.suspend();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,13 +89,13 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
</onboarding-create-user>`
|
</onboarding-create-user>`
|
||||||
: ""}
|
: ""}
|
||||||
${this._supervisor
|
${this._supervisor
|
||||||
? html`<onboarding-restore-snapshot
|
? html`<onboarding-restore-backup
|
||||||
.localize=${this.localize}
|
.localize=${this.localize}
|
||||||
.restoring=${this._restoring}
|
.restoring=${this._restoring}
|
||||||
.discoveryInformation=${this._discoveryInformation}
|
.discoveryInformation=${this._discoveryInformation}
|
||||||
@restoring=${this._restoringSnapshot}
|
@restoring=${this._restoringBackup}
|
||||||
>
|
>
|
||||||
</onboarding-restore-snapshot>`
|
</onboarding-restore-backup>`
|
||||||
: ""}
|
: ""}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -170,7 +170,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
return this._steps ? this._steps.find((stp) => !stp.done) : undefined;
|
return this._steps ? this._steps.find((stp) => !stp.done) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _restoringSnapshot() {
|
private _restoringBackup() {
|
||||||
this._restoring = true;
|
this._restoring = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,12 +183,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
].includes(response.installation_type);
|
].includes(response.installation_type);
|
||||||
if (this._supervisor) {
|
if (this._supervisor) {
|
||||||
// Only load if we have supervisor
|
// Only load if we have supervisor
|
||||||
import("./onboarding-restore-snapshot");
|
import("./onboarding-restore-backup");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(
|
console.error(
|
||||||
"Something went wrong loading onboarding-restore-snapshot",
|
"Something went wrong loading onboarding-restore-backup",
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,19 @@ class IntegrationBadge extends LitElement {
|
|||||||
|
|
||||||
@property() public badgeIcon?: string;
|
@property() public badgeIcon?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public darkOptimizedIcon?: boolean;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public clickable = false;
|
@property({ type: Boolean, reflect: true }) public clickable = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<img
|
<img
|
||||||
src=${brandsUrl(this.domain, "icon")}
|
src=${brandsUrl({
|
||||||
|
domain: this.domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.darkOptimizedIcon,
|
||||||
|
})}
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
${this.badgeIcon
|
${this.badgeIcon
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user