mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 00:36:34 +00:00
20221130.0 (#14492)
Co-authored-by: Bram Kragten <mail@bramkragten.nl> Co-authored-by: Paul Bottein <paul.bottein@gmail.com> Co-authored-by: Steve Repsher <steverep@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev> Co-authored-by: Franck Nijhof <git@frenck.dev> Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Bagira <bagdi.istvan@gmail.com> Co-authored-by: Ben Randall <veleek@gmail.com> Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Chris <31055115+darthsebulba04@users.noreply.github.com> Co-authored-by: Aaron Carson <aaron@aaroncarson.co.uk> Co-authored-by: David F. Mulcahey <david.mulcahey@me.com> Co-authored-by: Yosi Levy <37745463+yosilevy@users.noreply.github.com> Co-authored-by: Alex van den Hoogen <alex3305@gmail.com> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: Jani Lahti <jani.lahti@iki.fi> Co-authored-by: RoboMagus <68224306+RoboMagus@users.noreply.github.com> Co-authored-by: Brynley McDonald <brynley+github@zephire.nz> Co-authored-by: Denis Shulyaka <Shulyaka@gmail.com> Co-authored-by: KablammoNick <nick@kablammo.net> Co-authored-by: Allen Porter <allen@thebends.org> Co-authored-by: Philip Allgaier <mail@spacegaier.de>
This commit is contained in:
parent
c92e6423e8
commit
eccc6a8cdb
1
.github/workflows/ci.yaml
vendored
1
.github/workflows/ci.yaml
vendored
@ -13,6 +13,7 @@ on:
|
||||
env:
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
2
.github/workflows/demo.yaml
vendored
2
.github/workflows/demo.yaml
vendored
@ -26,6 +26,8 @@ jobs:
|
||||
CI: true
|
||||
- name: Build Demo
|
||||
run: ./node_modules/.bin/gulp build-demo
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Deploy to Netlify
|
||||
run: npx netlify-cli deploy --dir=demo/dist --prod
|
||||
env:
|
||||
|
3
.github/workflows/nightly.yaml
vendored
3
.github/workflows/nightly.yaml
vendored
@ -49,9 +49,8 @@ jobs:
|
||||
run: |
|
||||
pip install build
|
||||
yarn install
|
||||
|
||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||
script/build_frontend
|
||||
|
||||
rm -rf dist home_assistant_frontend.egg-info
|
||||
python3 -m build
|
||||
|
||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@ -52,11 +52,11 @@ jobs:
|
||||
python3 -m pip install twine build
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
|
||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v0.1.14
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@
|
||||
build
|
||||
hass_frontend/*
|
||||
dist
|
||||
translations
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
|
8
.vscode/tasks.json
vendored
8
.vscode/tasks.json
vendored
@ -191,7 +191,13 @@
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Setup and fetch nightly translations",
|
||||
"type": "gulp",
|
||||
"task": "setup-and-fetch-nightly-translations",
|
||||
"problemMatcher": []
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"no-console": 0
|
||||
}
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
{
|
||||
"extends": "../.eslintrc.json",
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"global-require": 0
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"prefer-arrow-callback": "off"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const path = require("path");
|
||||
|
||||
// Currently only supports CommonJS modules, as require is synchronous. `import` would need babel running asynchronous.
|
||||
@ -29,7 +28,6 @@ module.exports = function inlineConstants(babel, options, cwd) {
|
||||
const absolute = module.startsWith(".")
|
||||
? require.resolve(module, { paths: [cwd] })
|
||||
: module;
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
return [absolute, require(absolute)];
|
||||
})
|
||||
);
|
||||
|
@ -1,9 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const path = require("path");
|
||||
const env = require("./env.js");
|
||||
const paths = require("./paths.js");
|
||||
|
||||
// Files from NPM Packages that should not be imported
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
module.exports.ignorePackages = ({ latestBuild }) => [
|
||||
// Part of yaml.js and only used for !!js functions that we don't use
|
||||
require.resolve("esprima"),
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const paths = require("./paths.js");
|
||||
|
@ -1,6 +1,4 @@
|
||||
// Tasks to generate entry HTML
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable global-require */
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
@ -91,7 +89,9 @@ gulp.task("gen-pages-prod", (done) => {
|
||||
});
|
||||
|
||||
gulp.task("gen-index-app-dev", (done) => {
|
||||
let latestAppJS, latestCoreJS, latestCustomPanelJS;
|
||||
let latestAppJS;
|
||||
let latestCoreJS;
|
||||
let latestCustomPanelJS;
|
||||
|
||||
if (env.useWDS()) {
|
||||
latestAppJS = "http://localhost:8000/src/entrypoints/app.ts";
|
||||
|
170
build-scripts/gulp/fetch-nightly_translations.js
Normal file
170
build-scripts/gulp/fetch-nightly_translations.js
Normal file
@ -0,0 +1,170 @@
|
||||
// Task to download the latest Lokalise translations from the nightly workflow artifacts
|
||||
|
||||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
const process = require("process");
|
||||
const del = require("del");
|
||||
const gulp = require("gulp");
|
||||
const jszip = require("jszip");
|
||||
const tar = require("tar");
|
||||
const { Octokit } = require("@octokit/rest");
|
||||
const { createOAuthDeviceAuth } = require("@octokit/auth-oauth-device");
|
||||
|
||||
const MAX_AGE = 24; // hours
|
||||
const OWNER = "home-assistant";
|
||||
const REPO = "frontend";
|
||||
const WORKFLOW_NAME = "nightly.yaml";
|
||||
const ARTIFACT_NAME = "translations";
|
||||
const CLIENT_ID = "Iv1.3914e28cb27834d1";
|
||||
const EXTRACT_DIR = "translations";
|
||||
const TOKEN_FILE = path.join(EXTRACT_DIR, "token.json");
|
||||
const ARTIFACT_FILE = path.join(EXTRACT_DIR, "artifact.json");
|
||||
|
||||
let allowTokenSetup = false;
|
||||
gulp.task("allow-setup-fetch-nightly-translations", (done) => {
|
||||
allowTokenSetup = true;
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("fetch-nightly-translations", async function () {
|
||||
// Skip all when environment flag is set (assumes translations are already in place)
|
||||
if (process.env?.SKIP_FETCH_NIGHTLY_TRANSLATIONS) {
|
||||
console.log("Skipping fetch due to environment signal");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read current translations artifact info if it exists,
|
||||
// and stop if they are not old enough
|
||||
let currentArtifact;
|
||||
try {
|
||||
currentArtifact = JSON.parse(await fs.readFile(ARTIFACT_FILE, "utf-8"));
|
||||
const currentAge =
|
||||
(Date.now() - Date.parse(currentArtifact.created_at)) / 3600000;
|
||||
if (currentAge < MAX_AGE) {
|
||||
console.log(
|
||||
"Keeping current translations (only %s hours old)",
|
||||
currentAge.toFixed(1)
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
currentArtifact = null;
|
||||
}
|
||||
|
||||
// To store file writing promises
|
||||
const createExtractDir = fs.mkdir(EXTRACT_DIR, { recursive: true });
|
||||
const writings = [];
|
||||
|
||||
// Authenticate to GitHub using GitHub action token if it exists,
|
||||
// otherwise look for a saved user token or generate a new one if none
|
||||
let tokenAuth;
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
tokenAuth = { token: process.env.GITHUB_TOKEN };
|
||||
} else {
|
||||
try {
|
||||
tokenAuth = JSON.parse(await fs.readFile(TOKEN_FILE, "utf-8"));
|
||||
} catch {
|
||||
if (!allowTokenSetup) {
|
||||
console.log("No token found so build wil continue with English only");
|
||||
return;
|
||||
}
|
||||
const auth = createOAuthDeviceAuth({
|
||||
clientType: "github-app",
|
||||
clientId: CLIENT_ID,
|
||||
onVerification: (verification) => {
|
||||
console.log(
|
||||
"Task needs to authenticate to GitHub to fetch the translations from nightly workflow\n" +
|
||||
"Please go to %s to authorize this task\n" +
|
||||
"\nEnter user code: %s\n\n" +
|
||||
"This code will expire in %s minutes\n" +
|
||||
"Task will automatically continue after authorization and token will be saved for future use",
|
||||
verification.verification_uri,
|
||||
verification.user_code,
|
||||
(verification.expires_in / 60).toFixed(0)
|
||||
);
|
||||
},
|
||||
});
|
||||
tokenAuth = await auth({ type: "oauth" });
|
||||
writings.push(
|
||||
createExtractDir.then(
|
||||
fs.writeFile(TOKEN_FILE, JSON.stringify(tokenAuth, null, 2))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate with token and request workflow runs from GitHub
|
||||
console.log("Fetching new translations...");
|
||||
const octokit = new Octokit({
|
||||
userAgent: "Fetch Nightly Translations",
|
||||
auth: tokenAuth.token,
|
||||
});
|
||||
|
||||
const workflowRunsResponse = await octokit.rest.actions.listWorkflowRuns({
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
workflow_id: WORKFLOW_NAME,
|
||||
status: "success",
|
||||
event: "schedule",
|
||||
per_page: 1,
|
||||
exclude_pull_requests: true,
|
||||
});
|
||||
if (workflowRunsResponse.data.total_count === 0) {
|
||||
throw Error("No successful nightly workflow runs found");
|
||||
}
|
||||
const latestNightlyRun = workflowRunsResponse.data.workflow_runs[0];
|
||||
|
||||
// Stop if current is already the latest, otherwise Find the translations artifact
|
||||
if (currentArtifact?.workflow_run.id === latestNightlyRun.id) {
|
||||
console.log("Stopping because current translations are still the latest");
|
||||
return;
|
||||
}
|
||||
const latestArtifact = (
|
||||
await octokit.actions.listWorkflowRunArtifacts({
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
run_id: latestNightlyRun.id,
|
||||
})
|
||||
).data.artifacts.find((artifact) => artifact.name === ARTIFACT_NAME);
|
||||
if (!latestArtifact) {
|
||||
throw Error("Latest nightly workflow run has no translations artifact");
|
||||
}
|
||||
writings.push(
|
||||
createExtractDir.then(
|
||||
fs.writeFile(ARTIFACT_FILE, JSON.stringify(latestArtifact, null, 2))
|
||||
)
|
||||
);
|
||||
|
||||
// Remove the current translations
|
||||
const deleteCurrent = Promise.all(writings).then(
|
||||
del([`${EXTRACT_DIR}/*`, `!${ARTIFACT_FILE}`, `!${TOKEN_FILE}`])
|
||||
);
|
||||
|
||||
// Get the download URL and follow the redirect to download (stored as ArrayBuffer)
|
||||
const downloadResponse = await octokit.actions.downloadArtifact({
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
artifact_id: latestArtifact.id,
|
||||
archive_format: "zip",
|
||||
});
|
||||
if (downloadResponse.status !== 200) {
|
||||
throw Error("Failure downloading translations artifact");
|
||||
}
|
||||
|
||||
// Artifact is a tarball, but GitHub adds it to a zip file
|
||||
console.log("Unpacking downloaded translations...");
|
||||
const zip = await jszip.loadAsync(downloadResponse.data);
|
||||
await deleteCurrent;
|
||||
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(tar.extract());
|
||||
await new Promise((resolve, reject) => {
|
||||
extractStream.on("close", resolve).on("error", reject);
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"setup-and-fetch-nightly-translations",
|
||||
gulp.series(
|
||||
"allow-setup-fetch-nightly-translations",
|
||||
"fetch-nightly-translations"
|
||||
)
|
||||
);
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable */
|
||||
// Run demo develop mode
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
@ -41,7 +40,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
}
|
||||
processed.add(pageId);
|
||||
|
||||
const [category, name] = pageId.split("/", 2);
|
||||
const [category] = pageId.split("/", 2);
|
||||
|
||||
const demoFile = path.resolve(pageDir, `${pageId}.ts`);
|
||||
const descriptionFile = path.resolve(pageDir, `${pageId}.markdown`);
|
||||
|
@ -1,5 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
const gulp = require("gulp");
|
||||
|
@ -5,9 +5,9 @@ const rollup = require("rollup");
|
||||
const handler = require("serve-handler");
|
||||
const http = require("http");
|
||||
const log = require("fancy-log");
|
||||
const open = require("open");
|
||||
const rollupConfig = require("../rollup");
|
||||
const paths = require("../paths");
|
||||
const open = require("open");
|
||||
|
||||
const bothBuilds = (createConfigFunc, params) =>
|
||||
gulp.series(
|
||||
@ -30,11 +30,11 @@ const bothBuilds = (createConfigFunc, params) =>
|
||||
);
|
||||
|
||||
function createServer(serveOptions) {
|
||||
const server = http.createServer((request, response) => {
|
||||
return handler(request, response, {
|
||||
const server = http.createServer((request, response) =>
|
||||
handler(request, response, {
|
||||
public: serveOptions.root,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
server.listen(
|
||||
serveOptions.port,
|
||||
|
@ -1,7 +1,5 @@
|
||||
// Generate service worker.
|
||||
// Based on manifest, create a file with the content as service_worker.js
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable global-require */
|
||||
const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
|
@ -1,5 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
const crypto = require("crypto");
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
@ -15,6 +13,8 @@ const { mapFiles } = require("../util");
|
||||
const env = require("../env");
|
||||
const paths = require("../paths");
|
||||
|
||||
require("./fetch-nightly_translations");
|
||||
|
||||
const inFrontendDir = "translations/frontend";
|
||||
const inBackendDir = "translations/backend";
|
||||
const workDir = "build/translations";
|
||||
@ -23,10 +23,13 @@ const coreDir = workDir + "/core";
|
||||
const outDir = workDir + "/output";
|
||||
let mergeBackend = false;
|
||||
|
||||
gulp.task("translations-enable-merge-backend", (done) => {
|
||||
mergeBackend = true;
|
||||
done();
|
||||
});
|
||||
gulp.task(
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel((done) => {
|
||||
mergeBackend = true;
|
||||
done();
|
||||
}, "allow-setup-fetch-nightly-translations")
|
||||
);
|
||||
|
||||
// Panel translations which should be split from the core translations.
|
||||
const TRANSLATION_FRAGMENTS = Object.keys(
|
||||
@ -170,17 +173,24 @@ gulp.task("build-master-translation", () => {
|
||||
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: "translationMaster.json",
|
||||
fileName: "en.json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(workDir));
|
||||
.pipe(gulp.dest(fullDir));
|
||||
});
|
||||
|
||||
gulp.task("build-merged-translations", () =>
|
||||
gulp
|
||||
.src([inFrontendDir + "/*.json", workDir + "/test.json"], {
|
||||
allowEmpty: true,
|
||||
})
|
||||
.src(
|
||||
[
|
||||
inFrontendDir + "/*.json",
|
||||
"!" + inFrontendDir + "/en.json",
|
||||
workDir + "/test.json",
|
||||
],
|
||||
{
|
||||
allowEmpty: true,
|
||||
}
|
||||
)
|
||||
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||
.pipe(
|
||||
flatmap((stream, file) => {
|
||||
@ -193,7 +203,7 @@ gulp.task("build-merged-translations", () =>
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [workDir + "/translationMaster.json"];
|
||||
const src = [fullDir + "/en.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
@ -378,7 +388,6 @@ gulp.task("build-translation-write-metadata", () =>
|
||||
if (value.nativeName) {
|
||||
newData[key] = value;
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Skipping language ${key}. Native name was not translated.`
|
||||
);
|
||||
@ -411,8 +420,10 @@ gulp.task(
|
||||
gulp.task(
|
||||
"build-translations",
|
||||
gulp.series(
|
||||
"clean-translations",
|
||||
"ensure-translations-build-dir",
|
||||
gulp.parallel(
|
||||
"fetch-nightly-translations",
|
||||
gulp.series("clean-translations", "ensure-translations-build-dir")
|
||||
),
|
||||
"create-translations",
|
||||
"build-translation-fingerprints",
|
||||
"build-translation-write-metadata"
|
||||
@ -422,8 +433,10 @@ gulp.task(
|
||||
gulp.task(
|
||||
"build-supervisor-translations",
|
||||
gulp.series(
|
||||
"clean-translations",
|
||||
"ensure-translations-build-dir",
|
||||
gulp.parallel(
|
||||
"fetch-nightly-translations",
|
||||
gulp.series("clean-translations", "ensure-translations-build-dir")
|
||||
),
|
||||
"build-master-translation",
|
||||
"build-merged-translations",
|
||||
"build-translation-fragment-supervisor",
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
// Tasks to run webpack.
|
||||
const fs = require("fs");
|
||||
const gulp = require("gulp");
|
||||
@ -69,7 +68,6 @@ const doneHandler = (done) => (err, stats) => {
|
||||
}
|
||||
|
||||
if (stats.hasErrors() || stats.hasWarnings()) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(stats.toString("minimal"));
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
|
@ -81,13 +81,13 @@ module.exports = function (opts = {}) {
|
||||
opts.workerRegexp.flags
|
||||
);
|
||||
if (!workerRegexp.test(code)) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ms = new MagicString(code);
|
||||
// Reset the regexp
|
||||
workerRegexp.lastIndex = 0;
|
||||
while (true) {
|
||||
for (;;) {
|
||||
const match = workerRegexp.exec(code);
|
||||
if (!match) {
|
||||
break;
|
||||
@ -98,6 +98,7 @@ module.exports = function (opts = {}) {
|
||||
// Parse the optional options object
|
||||
if (match[3] && match[3].length > 0) {
|
||||
// FIXME: ooooof!
|
||||
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
||||
optionsObject = new Function(`return ${match[3].slice(1)};`)();
|
||||
}
|
||||
delete optionsObject.type;
|
||||
@ -110,12 +111,14 @@ module.exports = function (opts = {}) {
|
||||
}
|
||||
|
||||
// Find worker file and store it as a chunk with ID prefixed for our loader
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resolvedWorkerFile = (await this.resolve(workerFile, id)).id;
|
||||
let chunkRefId;
|
||||
if (resolvedWorkerFile in refIds) {
|
||||
chunkRefId = refIds[resolvedWorkerFile];
|
||||
} else {
|
||||
this.addWatchFile(resolvedWorkerFile);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const source = await getBundledWorker(
|
||||
resolvedWorkerFile,
|
||||
rollupOptions
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const path = require("path");
|
||||
|
||||
const commonjs = require("@rollup/plugin-commonjs");
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const webpack = require("webpack");
|
||||
const path = require("path");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
@ -103,7 +102,6 @@ const createWebpackConfig = ({
|
||||
? path.resolve(context, resource)
|
||||
: require.resolve(resource);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Error in Home Assistant ignore plugin",
|
||||
resource,
|
||||
|
@ -213,7 +213,7 @@
|
||||
</p>
|
||||
<ul>
|
||||
<li>Google Chrome (all platforms except iOS)</li>
|
||||
<li>Microsoft Edge (all platforms)</li>
|
||||
<li>Microsoft Edge (all platforms except iOS)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -88,7 +88,7 @@ class HcCast extends LitElement {
|
||||
>
|
||||
${(this.lovelaceConfig
|
||||
? this.lovelaceConfig.views
|
||||
: [generateDefaultViewConfig([], [], [], {}, () => "")]
|
||||
: [generateDefaultViewConfig({}, {}, {}, {}, () => "")]
|
||||
).map(
|
||||
(view, idx) => html`
|
||||
<paper-icon-item
|
||||
|
@ -44,7 +44,7 @@ class HcLayout extends LitElement {
|
||||
<div class="footer">
|
||||
<a href="./faq.html">Frequently Asked Questions</a> – Found a bug?
|
||||
<a
|
||||
href="https://github.com/home-assistant/home-assistant-polymer/issues"
|
||||
href="https://github.com/home-assistant/frontend/issues"
|
||||
target="_blank"
|
||||
>Let us know!</a
|
||||
>
|
||||
|
@ -138,7 +138,7 @@ if (!window.cardTools) {
|
||||
return cardTools.createThing("row", config);
|
||||
|
||||
const domain = config.entity.split(".", 1)[0];
|
||||
Object.assign(config, { type: DEFAULT_ROWS[domain] || "text" });
|
||||
Object.assign(config, { type: DEFAULT_ROWS[domain] || "simple" });
|
||||
return cardTools.createThing("entity-row", config);
|
||||
};
|
||||
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const generateMeanStatistics = (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
@ -29,13 +28,12 @@ const generateMeanStatistics = (
|
||||
const delta = Math.random() * maxDiff;
|
||||
const mean = lastVal + delta;
|
||||
statistics.push({
|
||||
statistic_id: id,
|
||||
start: currentDate.toISOString(),
|
||||
end: currentDate.toISOString(),
|
||||
start: currentDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
mean,
|
||||
min: mean - Math.random() * maxDiff,
|
||||
max: mean + Math.random() * maxDiff,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
last_reset: 0,
|
||||
state: mean,
|
||||
sum: null,
|
||||
});
|
||||
@ -51,7 +49,6 @@ const generateMeanStatistics = (
|
||||
};
|
||||
|
||||
const generateSumStatistics = (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
@ -67,13 +64,12 @@ const generateSumStatistics = (
|
||||
const add = Math.random() * maxDiff;
|
||||
sum += add;
|
||||
statistics.push({
|
||||
statistic_id: id,
|
||||
start: currentDate.toISOString(),
|
||||
end: currentDate.toISOString(),
|
||||
start: currentDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
last_reset: 0,
|
||||
state: initValue + sum,
|
||||
sum,
|
||||
});
|
||||
@ -88,7 +84,6 @@ const generateSumStatistics = (
|
||||
};
|
||||
|
||||
const generateCurvedStatistics = (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
_period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
@ -108,13 +103,12 @@ const generateCurvedStatistics = (
|
||||
const add = Math.random() * maxDiff;
|
||||
sum += i * add;
|
||||
statistics.push({
|
||||
statistic_id: id,
|
||||
start: currentDate.toISOString(),
|
||||
end: currentDate.toISOString(),
|
||||
start: currentDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
last_reset: "1970-01-01T00:00:00+00:00",
|
||||
last_reset: 0,
|
||||
state: initValue + sum,
|
||||
sum: metered ? sum : null,
|
||||
});
|
||||
@ -137,14 +131,13 @@ const statisticsFunctions: Record<
|
||||
) => StatisticValue[]
|
||||
> = {
|
||||
"sensor.energy_consumption_tarif_1": (
|
||||
id: string,
|
||||
_id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period = "hour"
|
||||
) => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
@ -153,20 +146,12 @@ const statisticsFunctions: Record<
|
||||
);
|
||||
}
|
||||
const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
|
||||
const morningLow = generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
morningEnd,
|
||||
period,
|
||||
0,
|
||||
0.7
|
||||
);
|
||||
const morningLow = generateSumStatistics(start, morningEnd, period, 0, 0.7);
|
||||
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
|
||||
const morningFinalVal = morningLow.length
|
||||
? morningLow[morningLow.length - 1].sum!
|
||||
: 0;
|
||||
const empty = generateSumStatistics(
|
||||
id,
|
||||
morningEnd,
|
||||
eveningStart,
|
||||
period,
|
||||
@ -174,7 +159,6 @@ const statisticsFunctions: Record<
|
||||
0
|
||||
);
|
||||
const eveningLow = generateSumStatistics(
|
||||
id,
|
||||
eveningStart,
|
||||
end,
|
||||
period,
|
||||
@ -184,14 +168,13 @@ const statisticsFunctions: Record<
|
||||
return [...morningLow, ...empty, ...eveningLow];
|
||||
},
|
||||
"sensor.energy_consumption_tarif_2": (
|
||||
id: string,
|
||||
_id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period = "hour"
|
||||
) => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
@ -202,7 +185,6 @@ const statisticsFunctions: Record<
|
||||
const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000);
|
||||
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
|
||||
const highTarif = generateSumStatistics(
|
||||
id,
|
||||
morningEnd,
|
||||
eveningStart,
|
||||
period,
|
||||
@ -212,9 +194,8 @@ const statisticsFunctions: Record<
|
||||
const highTarifFinalVal = highTarif.length
|
||||
? highTarif[highTarif.length - 1].sum!
|
||||
: 0;
|
||||
const morning = generateSumStatistics(id, start, morningEnd, period, 0, 0);
|
||||
const morning = generateSumStatistics(start, morningEnd, period, 0, 0);
|
||||
const evening = generateSumStatistics(
|
||||
id,
|
||||
eveningStart,
|
||||
end,
|
||||
period,
|
||||
@ -223,18 +204,17 @@ const statisticsFunctions: Record<
|
||||
);
|
||||
return [...morning, ...highTarif, ...evening];
|
||||
},
|
||||
"sensor.energy_production_tarif_1": (id, start, end, period = "hour") =>
|
||||
generateSumStatistics(id, start, end, period, 0, 0),
|
||||
"sensor.energy_production_tarif_1": (_id, start, end, period = "hour") =>
|
||||
generateSumStatistics(start, end, period, 0, 0),
|
||||
"sensor.energy_production_tarif_1_compensation": (
|
||||
id,
|
||||
_id,
|
||||
start,
|
||||
end,
|
||||
period = "hour"
|
||||
) => generateSumStatistics(id, start, end, period, 0, 0),
|
||||
"sensor.energy_production_tarif_2": (id, start, end, period = "hour") => {
|
||||
) => generateSumStatistics(start, end, period, 0, 0),
|
||||
"sensor.energy_production_tarif_2": (_id, start, end, period = "hour") => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
@ -246,7 +226,6 @@ const statisticsFunctions: Record<
|
||||
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
|
||||
const dayEnd = new Date(endOfDay(productionEnd));
|
||||
const production = generateCurvedStatistics(
|
||||
id,
|
||||
productionStart,
|
||||
productionEnd,
|
||||
period,
|
||||
@ -257,16 +236,8 @@ const statisticsFunctions: Record<
|
||||
const productionFinalVal = production.length
|
||||
? production[production.length - 1].sum!
|
||||
: 0;
|
||||
const morning = generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
productionStart,
|
||||
period,
|
||||
0,
|
||||
0
|
||||
);
|
||||
const morning = generateSumStatistics(start, productionStart, period, 0, 0);
|
||||
const evening = generateSumStatistics(
|
||||
id,
|
||||
productionEnd,
|
||||
dayEnd,
|
||||
period,
|
||||
@ -274,7 +245,6 @@ const statisticsFunctions: Record<
|
||||
0
|
||||
);
|
||||
const rest = generateSumStatistics(
|
||||
id,
|
||||
dayEnd,
|
||||
end,
|
||||
period,
|
||||
@ -283,10 +253,9 @@ const statisticsFunctions: Record<
|
||||
);
|
||||
return [...morning, ...production, ...evening, ...rest];
|
||||
},
|
||||
"sensor.solar_production": (id, start, end, period = "hour") => {
|
||||
"sensor.solar_production": (_id, start, end, period = "hour") => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
@ -298,7 +267,6 @@ const statisticsFunctions: Record<
|
||||
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
|
||||
const dayEnd = new Date(endOfDay(productionEnd));
|
||||
const production = generateCurvedStatistics(
|
||||
id,
|
||||
productionStart,
|
||||
productionEnd,
|
||||
period,
|
||||
@ -309,16 +277,8 @@ const statisticsFunctions: Record<
|
||||
const productionFinalVal = production.length
|
||||
? production[production.length - 1].sum!
|
||||
: 0;
|
||||
const morning = generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
productionStart,
|
||||
period,
|
||||
0,
|
||||
0
|
||||
);
|
||||
const morning = generateSumStatistics(start, productionStart, period, 0, 0);
|
||||
const evening = generateSumStatistics(
|
||||
id,
|
||||
productionEnd,
|
||||
dayEnd,
|
||||
period,
|
||||
@ -326,7 +286,6 @@ const statisticsFunctions: Record<
|
||||
0
|
||||
);
|
||||
const rest = generateSumStatistics(
|
||||
id,
|
||||
dayEnd,
|
||||
end,
|
||||
period,
|
||||
@ -362,7 +321,6 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
|
||||
statistics[id] =
|
||||
entityState && "last_reset" in entityState.attributes
|
||||
? generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
@ -370,7 +328,6 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
|
||||
state * (state > 80 ? 0.01 : 0.05)
|
||||
)
|
||||
: generateMeanStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
|
@ -1,3 +1,3 @@
|
||||
---
|
||||
title: Bar Sliders
|
||||
title: Bar Slider
|
||||
---
|
||||
|
@ -8,7 +8,7 @@ import "../../../../src/components/ha-card";
|
||||
const sliders: {
|
||||
id: string;
|
||||
label: string;
|
||||
mode?: "start" | "end" | "indicator";
|
||||
mode?: "start" | "end" | "cursor";
|
||||
class?: string;
|
||||
}[] = [
|
||||
{
|
||||
@ -22,9 +22,9 @@ const sliders: {
|
||||
mode: "end",
|
||||
},
|
||||
{
|
||||
id: "slider-indicator",
|
||||
label: "Slider (indicator mode)",
|
||||
mode: "indicator",
|
||||
id: "slider-cursor",
|
||||
label: "Slider (cursor mode)",
|
||||
mode: "cursor",
|
||||
},
|
||||
{
|
||||
id: "slider-start-custom",
|
||||
@ -39,9 +39,9 @@ const sliders: {
|
||||
class: "custom",
|
||||
},
|
||||
{
|
||||
id: "slider-indicator-custom",
|
||||
label: "Slider (indicator mode) and custom style",
|
||||
mode: "indicator",
|
||||
id: "slider-cursor-custom",
|
||||
label: "Slider (cursor mode) and custom style",
|
||||
mode: "cursor",
|
||||
class: "custom",
|
||||
},
|
||||
];
|
||||
|
3
gallery/src/pages/components/ha-bar-switch.markdown
Normal file
3
gallery/src/pages/components/ha-bar-switch.markdown
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Bar Switch
|
||||
---
|
145
gallery/src/pages/components/ha-bar-switch.ts
Normal file
145
gallery/src/pages/components/ha-bar-switch.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import {
|
||||
mdiGarage,
|
||||
mdiGarageOpen,
|
||||
mdiLightbulb,
|
||||
mdiLightbulbOff,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import "../../../../src/components/ha-bar-switch";
|
||||
import "../../../../src/components/ha-card";
|
||||
|
||||
const switches: {
|
||||
id: string;
|
||||
label: string;
|
||||
class?: string;
|
||||
reversed?: boolean;
|
||||
disabled?: boolean;
|
||||
}[] = [
|
||||
{
|
||||
id: "switch",
|
||||
label: "Switch",
|
||||
},
|
||||
{
|
||||
id: "switch-reversed",
|
||||
label: "Switch Reversed",
|
||||
reversed: true,
|
||||
},
|
||||
{
|
||||
id: "switch-custom",
|
||||
label: "Switch and custom style",
|
||||
class: "custom",
|
||||
},
|
||||
{
|
||||
id: "switch-disabled",
|
||||
label: "Disabled Switch",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-bar-switch")
|
||||
export class DemoHaBarSwitch extends LitElement {
|
||||
@state() private checked = false;
|
||||
|
||||
handleValueChanged(e: any) {
|
||||
this.checked = e.target.checked as boolean;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${repeat(switches, (sw) => {
|
||||
const { id, label, ...config } = sw;
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<label id=${id}>${label}</label>
|
||||
<pre>Config: ${JSON.stringify(config)}</pre>
|
||||
<ha-bar-switch
|
||||
.checked=${this.checked}
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
.pathOn=${mdiLightbulb}
|
||||
.pathOff=${mdiLightbulbOff}
|
||||
aria-labelledby=${id}
|
||||
disabled=${ifDefined(config.disabled)}
|
||||
reversed=${ifDefined(config.reversed)}
|
||||
>
|
||||
</ha-bar-switch>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Vertical</b></p>
|
||||
<div class="vertical-switches">
|
||||
${repeat(switches, (sw) => {
|
||||
const { id, label, ...config } = sw;
|
||||
return html`
|
||||
<ha-bar-switch
|
||||
.checked=${this.checked}
|
||||
vertical
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
aria-label=${label}
|
||||
.pathOn=${mdiGarageOpen}
|
||||
.pathOff=${mdiGarage}
|
||||
disabled=${ifDefined(config.disabled)}
|
||||
reversed=${ifDefined(config.reversed)}
|
||||
>
|
||||
</ha-bar-switch>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
--switch-bar-color-on: var(--rgb-green-color);
|
||||
--switch-bar-color-off: var(--rgb-red-color);
|
||||
--switch-bar-thickness: 100px;
|
||||
--switch-bar-border-radius: 24px;
|
||||
--switch-bar-padding: 6px;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.vertical-switches {
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
p.title {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.vertical-switches > *:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-bar-switch": DemoHaBarSwitch;
|
||||
}
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
---
|
||||
title: Chips
|
||||
title: Chip
|
||||
---
|
||||
|
@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Dialogs
|
||||
title: Dialog
|
||||
subtitle: Dialogs provide important prompts in a user flow.
|
||||
---
|
||||
|
||||
# Material Design 3
|
||||
|
||||
Our dialogs are based on the latest version of Material Design. Specs and guidelines can be found on it's [website](https://m3.material.io/components/dialogs/overview).
|
||||
Our dialogs are based on the latest version of Material Design. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
|
||||
|
||||
# Highlighted guidelines
|
||||
|
61
gallery/src/pages/components/ha-gauge.markdown
Normal file
61
gallery/src/pages/components/ha-gauge.markdown
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Gauge
|
||||
---
|
||||
|
||||
<style>
|
||||
ha-gauge {
|
||||
display: block;
|
||||
width: 200px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
# Gauge `<ha-gauge>`
|
||||
|
||||
A gauge that can be used to represent sensor data and provide visual feedback about the value and the corresponding severity (success, warning, error).
|
||||
|
||||
## Examples
|
||||
|
||||
Info color gauge
|
||||
<ha-gauge value="75" style="--gauge-color: var(--info-color)"></ha-gauge>
|
||||
|
||||
Success color gauge
|
||||
<ha-gauge value="25" style="--gauge-color: var(--success-color)" label="°C"></ha-gauge>
|
||||
|
||||
Warning color gauge
|
||||
<ha-gauge value="50" style="--gauge-color: var(--warning-color)" label="°C"></ha-gauge>
|
||||
|
||||
Error color gauge
|
||||
<ha-gauge value="75" style="--gauge-color: var(--error-color)" label="°C"></ha-gauge>
|
||||
|
||||
Gauge with background color
|
||||
<ha-gauge value="75" style="--gauge-color: var(--info-color); --primary-background-color: lightgray"></ha-gauge>
|
||||
|
||||
|
||||
## CSS variables
|
||||
|
||||
### Gauge
|
||||
|
||||
`primary-background-color`
|
||||
Background color of the dial (rounded arch)
|
||||
|
||||
`primary-text-color`
|
||||
Text color below dial (value and unit of measurement) plus needle color (if gauge is in needle mode)
|
||||
|
||||
#### Dial colors
|
||||
|
||||
`gauge-color`
|
||||
Used in the coding to control what color the gauge value is rendered with, but cannot be set via theme since its value will dynamically be set (either to `info-color` or to the matching severity variable if the severity color mode is used). To control the used colors, adjust the following variables.
|
||||
|
||||
`success-color`
|
||||
Dial color for the "green" severity level
|
||||
|
||||
`warning-color`
|
||||
Dial color for the "yellow" severity level
|
||||
|
||||
`error-color`
|
||||
Dial color for the "red" severity level
|
||||
|
||||
`info-color`
|
||||
Static dial color if not in severity color mode
|
1
gallery/src/pages/components/ha-gauge.ts
Normal file
1
gallery/src/pages/components/ha-gauge.ts
Normal file
@ -0,0 +1 @@
|
||||
import "../../../../src/components/ha-gauge";
|
39
gallery/src/pages/components/ha-switch.markdown
Normal file
39
gallery/src/pages/components/ha-switch.markdown
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Switch / Toggle
|
||||
---
|
||||
|
||||
<style>
|
||||
ha-switch {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
# Switch `<ha-switch>`
|
||||
|
||||
A toggle switch can represent two states: on and off.
|
||||
|
||||
## Examples
|
||||
|
||||
Switch in on state
|
||||
<ha-switch checked></ha-switch>
|
||||
|
||||
Switch in off state
|
||||
<ha-switch></ha-switch>
|
||||
|
||||
Disabled switch
|
||||
<ha-switch disabled></ha-switch>
|
||||
|
||||
## CSS variables
|
||||
|
||||
For the switch / toggle there are always two variables, one for the on / checked state and one for the off / unchecked state.
|
||||
|
||||
The track element (background rounded rectangle that the round circular handle travels on) is set to being half transparent, so the final color will also be impacted by the color behind the track.
|
||||
|
||||
`switch-checked-color` / `switch-unchecked-color`
|
||||
Set both the color of the round handle and the track behind it. If you want to control them separately, use the variables below instead.
|
||||
|
||||
`switch-checked-button-color` / `switch-unchecked-button-color`
|
||||
Color of the round handle
|
||||
|
||||
`switch-checked-track-color` / `switch-unchecked-track-color`
|
||||
Color of the track behind the round handle
|
1
gallery/src/pages/components/ha-switch.ts
Normal file
1
gallery/src/pages/components/ha-switch.ts
Normal file
@ -0,0 +1 @@
|
||||
import "../../../../src/components/ha-switch";
|
@ -1,3 +1,3 @@
|
||||
---
|
||||
title: Tips
|
||||
title: Tip
|
||||
---
|
||||
|
@ -98,6 +98,9 @@ const ENTITIES = [
|
||||
minimum: 0,
|
||||
maximum: 10,
|
||||
}),
|
||||
getEntity("text", "message", "Hello!", {
|
||||
friendly_name: "Message",
|
||||
}),
|
||||
|
||||
getEntity("light", "unavailable", "unavailable", {
|
||||
friendly_name: "Bed Light",
|
||||
@ -129,6 +132,9 @@ const ENTITIES = [
|
||||
friendly_name: "Who cooks",
|
||||
icon: "mdi:cheff",
|
||||
}),
|
||||
getEntity("text", "unavailable", "unavailable", {
|
||||
friendly_name: "Message",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@ -147,6 +153,7 @@ const CONFIGS = [
|
||||
- climate.ecobee
|
||||
- input_number.number
|
||||
- sensor.humidity
|
||||
- text.message
|
||||
`,
|
||||
},
|
||||
{
|
||||
@ -219,6 +226,7 @@ const CONFIGS = [
|
||||
- climate.unavailable
|
||||
- input_number.unavailable
|
||||
- input_select.unavailable
|
||||
- text.unavailable
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -23,13 +23,12 @@ const CONFIGS = [
|
||||
heading: "Basic example",
|
||||
config: `
|
||||
- type: gauge
|
||||
title: Humidity
|
||||
entity: sensor.outside_humidity
|
||||
name: Outside Humidity
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Custom Unit of Measurement",
|
||||
heading: "Custom unit of measurement",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.outside_temperature
|
||||
@ -38,7 +37,16 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Setting Severity Levels",
|
||||
heading: "Rendering needle",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.outside_humidity
|
||||
name: Outside Humidity
|
||||
needle: true
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Setting severity levels",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.brightness
|
||||
@ -50,7 +58,7 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Setting Severity Levels",
|
||||
heading: "Setting severity levels",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.brightness_medium
|
||||
@ -62,7 +70,7 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Setting Severity Levels",
|
||||
heading: "Setting severity levels",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.brightness_high
|
||||
@ -74,7 +82,7 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Setting Min (0) and Max (15) Values",
|
||||
heading: "Setting min (0) and mx (15) values",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.brightness
|
||||
@ -84,14 +92,14 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Invalid Entity",
|
||||
heading: "Invalid entity",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.invalid_entity
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Non-Numeric Value",
|
||||
heading: "Non-numeric value",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: plant.bonsai
|
||||
|
3
gallery/src/pages/misc/entity-state.markdown
Normal file
3
gallery/src/pages/misc/entity-state.markdown
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Entity State
|
||||
---
|
376
gallery/src/pages/misc/entity-state.ts
Normal file
376
gallery/src/pages/misc/entity-state.ts
Normal file
@ -0,0 +1,376 @@
|
||||
import {
|
||||
HassEntity,
|
||||
HassEntityAttributeBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../../../src/common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display";
|
||||
import { stateColorCss } from "../../../../src/common/entity/state_color";
|
||||
import { stateIconPath } from "../../../../src/common/entity/state_icon_path";
|
||||
import "../../../../src/components/data-table/ha-data-table";
|
||||
import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table";
|
||||
import "../../../../src/components/ha-chip";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
const SENSOR_DEVICE_CLASSES = [
|
||||
"apparent_power",
|
||||
"aqi",
|
||||
// "battery"
|
||||
"carbon_dioxide",
|
||||
"carbon_monoxide",
|
||||
"current",
|
||||
"date",
|
||||
"distance",
|
||||
"duration",
|
||||
"energy",
|
||||
"frequency",
|
||||
"gas",
|
||||
"humidity",
|
||||
"illuminance",
|
||||
"moisture",
|
||||
"monetary",
|
||||
"nitrogen_dioxide",
|
||||
"nitrogen_monoxide",
|
||||
"nitrous_oxide",
|
||||
"ozone",
|
||||
"pm1",
|
||||
"pm10",
|
||||
"pm25",
|
||||
"power_factor",
|
||||
"power",
|
||||
"precipitation",
|
||||
"precipitation_intensity",
|
||||
"pressure",
|
||||
"reactive_power",
|
||||
"signal_strength",
|
||||
"speed",
|
||||
"sulphur_dioxide",
|
||||
"temperature",
|
||||
"timestamp",
|
||||
"volatile_organic_compounds",
|
||||
"voltage",
|
||||
"volume",
|
||||
"water",
|
||||
"weight",
|
||||
"wind_speed",
|
||||
];
|
||||
|
||||
const BINARY_SENSOR_DEVICE_CLASSES = [
|
||||
"battery",
|
||||
"battery_charging",
|
||||
"carbon_monoxide",
|
||||
"cold",
|
||||
"connectivity",
|
||||
"door",
|
||||
"garage_door",
|
||||
"gas",
|
||||
"heat",
|
||||
"light",
|
||||
"lock",
|
||||
"moisture",
|
||||
"motion",
|
||||
"moving",
|
||||
"occupancy",
|
||||
"opening",
|
||||
"plug",
|
||||
"power",
|
||||
"presence",
|
||||
"problem",
|
||||
"running",
|
||||
"safety",
|
||||
"smoke",
|
||||
"sound",
|
||||
"tamper",
|
||||
"update",
|
||||
"vibration",
|
||||
"window",
|
||||
];
|
||||
|
||||
const ENTITIES: HassEntity[] = [
|
||||
// Alarm control panel
|
||||
createEntity("alarm_control_panel.disarmed", "disarmed"),
|
||||
createEntity("alarm_control_panel.armed_home", "armed_home"),
|
||||
createEntity("alarm_control_panel.armed_away", "armed_away"),
|
||||
createEntity("alarm_control_panel.armed_night", "armed_night"),
|
||||
createEntity("alarm_control_panel.armed_vacation", "armed_vacation"),
|
||||
createEntity(
|
||||
"alarm_control_panel.armed_custom_bypass",
|
||||
"armed_custom_bypass"
|
||||
),
|
||||
createEntity("alarm_control_panel.pending", "pending"),
|
||||
createEntity("alarm_control_panel.arming", "arming"),
|
||||
createEntity("alarm_control_panel.disarming", "disarming"),
|
||||
createEntity("alarm_control_panel.triggered", "triggered"),
|
||||
// Binary Sensor
|
||||
...BINARY_SENSOR_DEVICE_CLASSES.map((dc) =>
|
||||
createEntity(`binary_sensor.${dc}`, "on", dc)
|
||||
),
|
||||
// Button
|
||||
createEntity("button.restart", "unknown", "restart"),
|
||||
createEntity("button.update", "unknown", "update"),
|
||||
// Calendar
|
||||
createEntity("calendar.on", "on"),
|
||||
createEntity("calendar.off", "off"),
|
||||
// Climate
|
||||
createEntity("climate.off", "off"),
|
||||
createEntity("climate.heat", "heat"),
|
||||
createEntity("climate.cool", "cool"),
|
||||
createEntity("climate.heat_cool", "heat_cool"),
|
||||
createEntity("climate.auto", "auto"),
|
||||
createEntity("climate.dry", "dry"),
|
||||
createEntity("climate.fan_only", "fan_only"),
|
||||
// Cover
|
||||
createEntity("cover.opening", "opening"),
|
||||
createEntity("cover.open", "open"),
|
||||
createEntity("cover.closing", "closing"),
|
||||
createEntity("cover.closed", "closed"),
|
||||
createEntity("cover.awning", "open", "awning"),
|
||||
createEntity("cover.blind", "open", "blind"),
|
||||
createEntity("cover.curtain", "open", "curtain"),
|
||||
createEntity("cover.damper", "open", "damper"),
|
||||
createEntity("cover.door", "open", "door"),
|
||||
createEntity("cover.garage", "open", "garage"),
|
||||
createEntity("cover.gate", "open", "gate"),
|
||||
createEntity("cover.shade", "open", "shade"),
|
||||
createEntity("cover.shutter", "open", "shutter"),
|
||||
createEntity("cover.window", "open", "window"),
|
||||
// Device tracker/person
|
||||
createEntity("device_tracker.home", "home"),
|
||||
createEntity("device_tracker.not_home", "not_home"),
|
||||
createEntity("device_tracker.work", "work"),
|
||||
createEntity("person.home", "home"),
|
||||
createEntity("person.not_home", "not_home"),
|
||||
createEntity("person.work", "work"),
|
||||
// Fan
|
||||
createEntity("fan.on", "on"),
|
||||
createEntity("fan.off", "off"),
|
||||
// Humidifier
|
||||
createEntity("humidifier.on", "on"),
|
||||
createEntity("humidifier.off", "off"),
|
||||
// Light
|
||||
createEntity("light.on", "on"),
|
||||
createEntity("light.off", "off"),
|
||||
// Locks
|
||||
createEntity("lock.locked", "locked"),
|
||||
createEntity("lock.unlocked", "unlocked"),
|
||||
createEntity("lock.locking", "locking"),
|
||||
createEntity("lock.unlocking", "unlocking"),
|
||||
createEntity("lock.jammed", "jammed"),
|
||||
// Media Player
|
||||
createEntity("media_player.off", "off"),
|
||||
createEntity("media_player.on", "on"),
|
||||
createEntity("media_player.idle", "idle"),
|
||||
createEntity("media_player.playing", "playing"),
|
||||
createEntity("media_player.paused", "paused"),
|
||||
createEntity("media_player.standby", "standby"),
|
||||
createEntity("media_player.buffering", "buffering"),
|
||||
createEntity("media_player.tv_off", "off", "tv"),
|
||||
createEntity("media_player.tv_playing", "playing", "tv"),
|
||||
createEntity("media_player.tv_paused", "paused", "tv"),
|
||||
createEntity("media_player.tv_standby", "standby", "tv"),
|
||||
createEntity("media_player.receiver_off", "off", "receiver"),
|
||||
createEntity("media_player.receiver_playing", "playing", "receiver"),
|
||||
createEntity("media_player.receiver_paused", "paused", "receiver"),
|
||||
createEntity("media_player.receiver_standby", "standby", "receiver"),
|
||||
createEntity("media_player.speaker_off", "off", "speaker"),
|
||||
createEntity("media_player.speaker_playing", "playing", "speaker"),
|
||||
createEntity("media_player.speaker_paused", "paused", "speaker"),
|
||||
createEntity("media_player.speaker_standby", "standby", "speaker"),
|
||||
// Sensor
|
||||
...SENSOR_DEVICE_CLASSES.map((dc) => createEntity(`sensor.${dc}`, "10", dc)),
|
||||
// Battery sensor
|
||||
...[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) =>
|
||||
createEntity(`sensor.battery_${value}`, value.toString(), "battery")
|
||||
),
|
||||
// Siren
|
||||
createEntity("siren.off", "off"),
|
||||
createEntity("siren.on", "on"),
|
||||
// Switch
|
||||
createEntity("switch.off", "off"),
|
||||
createEntity("switch.on", "on"),
|
||||
createEntity("switch.outlet_off", "off", "outlet"),
|
||||
createEntity("switch.outlet_on", "on", "outlet"),
|
||||
createEntity("switch.switch_off", "off", "switch"),
|
||||
createEntity("switch.switch_on", "on", "switch"),
|
||||
// Vacuum
|
||||
createEntity("vacuum.cleaning", "cleaning"),
|
||||
createEntity("vacuum.docked", "docked"),
|
||||
createEntity("vacuum.paused", "paused"),
|
||||
createEntity("vacuum.idle", "idle"),
|
||||
createEntity("vacuum.returning", "returning"),
|
||||
createEntity("vacuum.error", "error"),
|
||||
createEntity("vacuum.cleaning", "cleaning"),
|
||||
createEntity("vacuum.off", "off"),
|
||||
createEntity("vacuum.on", "on"),
|
||||
// Update
|
||||
createEntity("update.off", "off", undefined, {
|
||||
installed_version: "1.0.0",
|
||||
latest_version: "2.0.0",
|
||||
}),
|
||||
createEntity("update.on", "on", undefined, {
|
||||
installed_version: "1.0.0",
|
||||
latest_version: "2.0.0",
|
||||
}),
|
||||
createEntity("update.installing", "on", undefined, {
|
||||
installed_version: "1.0.0",
|
||||
latest_version: "2.0.0",
|
||||
in_progress: true,
|
||||
}),
|
||||
createEntity("update.off", "off", "firmware", {
|
||||
installed_version: "1.0.0",
|
||||
latest_version: "2.0.0",
|
||||
}),
|
||||
createEntity("update.on", "on", "firmware", {
|
||||
installed_version: "1.0.0",
|
||||
latest_version: "2.0.0",
|
||||
}),
|
||||
];
|
||||
|
||||
function createEntity(
|
||||
entity_id: string,
|
||||
state: string,
|
||||
device_class?: string,
|
||||
attributes?: HassEntityAttributeBase | HassEntity["attributes"]
|
||||
): HassEntity {
|
||||
return {
|
||||
entity_id,
|
||||
state,
|
||||
attributes: {
|
||||
...attributes,
|
||||
device_class: device_class,
|
||||
},
|
||||
last_changed: new Date().toString(),
|
||||
last_updated: new Date().toString(),
|
||||
context: {
|
||||
id: "1",
|
||||
parent_id: null,
|
||||
user_id: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type EntityRowData = {
|
||||
stateObj: HassEntity;
|
||||
entity_id: string;
|
||||
state: string;
|
||||
device_class?: string;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
function createRowData(stateObj: HassEntity): EntityRowData {
|
||||
return {
|
||||
stateObj,
|
||||
entity_id: stateObj.entity_id,
|
||||
state: stateObj.state,
|
||||
device_class: stateObj.attributes.device_class,
|
||||
domain: computeDomain(stateObj.entity_id),
|
||||
};
|
||||
}
|
||||
|
||||
@customElement("demo-misc-entity-state")
|
||||
export class DemoEntityState extends LitElement {
|
||||
@property({ attribute: false }) hass?: HomeAssistant;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(hass: HomeAssistant): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer<EntityRowData> = {
|
||||
icon: {
|
||||
title: "Icon",
|
||||
template: (_, entry) => {
|
||||
const cssColor = stateColorCss(entry.stateObj);
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
style=${styleMap({
|
||||
color: `rgb(${cssColor})`,
|
||||
})}
|
||||
.path=${stateIconPath(entry.stateObj)}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
`;
|
||||
},
|
||||
},
|
||||
entity_id: {
|
||||
title: "Entity id",
|
||||
width: "30%",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
state: {
|
||||
title: "State",
|
||||
width: "20%",
|
||||
sortable: true,
|
||||
template: (_, entry) =>
|
||||
html`${computeStateDisplay(
|
||||
hass.localize,
|
||||
entry.stateObj,
|
||||
hass.locale
|
||||
)}`,
|
||||
},
|
||||
device_class: {
|
||||
title: "Device class",
|
||||
template: (dc) => html`${dc ?? "-"}`,
|
||||
width: "20%",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
domain: {
|
||||
title: "Domain",
|
||||
template: (_, entry) => html`${computeDomain(entry.entity_id)}`,
|
||||
width: "20%",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
};
|
||||
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
private _rows = memoizeOne((): EntityRowData[] =>
|
||||
ENTITIES.map(createRowData)
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.columns=${this._columns(this.hass)}
|
||||
.data=${this._rows()}
|
||||
auto-height
|
||||
></ha-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
.color {
|
||||
display: block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: rgb(--color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-misc-entity-state": DemoEntityState;
|
||||
}
|
||||
}
|
@ -1,11 +1,8 @@
|
||||
module.exports = {
|
||||
"*.{js,ts}": [
|
||||
"prettier --write",
|
||||
'eslint --ignore-pattern "**/build-scripts/**/*.js" --fix',
|
||||
],
|
||||
"*.{js,ts}": ["prettier --write", "eslint --fix"],
|
||||
"!(/translations)*.{json,css,md,html}": "prettier --write",
|
||||
"translations/*/*.json": (files) =>
|
||||
'printf "%s\n" "These files should not be modified. Instead, make the necessary modifications in src/translations/en.json. Please see translations/README.md for details." ' +
|
||||
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
|
||||
files.join(" ") +
|
||||
" >&2 && exit 1",
|
||||
};
|
||||
|
40
package.json
40
package.json
@ -92,8 +92,8 @@
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.4.1",
|
||||
"@thomasloven/round-slider": "0.5.4",
|
||||
"@vaadin/combo-box": "^23.2.0",
|
||||
"@vaadin/vaadin-themable-mixin": "^23.2.0",
|
||||
"@vaadin/combo-box": "^23.2.9",
|
||||
"@vaadin/vaadin-themable-mixin": "^23.2.9",
|
||||
"@vibrant/color": "^3.2.1-alpha.1",
|
||||
"@vibrant/core": "^3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||
@ -111,8 +111,8 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hammerjs": "^2.0.8",
|
||||
"hls.js": "^1.2.3",
|
||||
"home-assistant-js-websocket": "^8.0.0",
|
||||
"hls.js": "^1.2.5",
|
||||
"home-assistant-js-websocket": "^8.0.1",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
@ -129,6 +129,7 @@
|
||||
"regenerator-runtime": "^0.13.8",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"rrule": "^2.7.1",
|
||||
"sortablejs": "^1.14.0",
|
||||
"superstruct": "^0.15.2",
|
||||
"tinykeys": "^1.1.3",
|
||||
@ -148,19 +149,21 @@
|
||||
"xss": "^1.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/plugin-external-helpers": "^7.14.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.15.4",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.15.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
|
||||
"@babel/core": "^7.20.2",
|
||||
"@babel/plugin-external-helpers": "^7.18.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.20.2",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.20.2",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
||||
"@babel/plugin-syntax-top-level-await": "^7.14.5",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@koa/cors": "^3.1.0",
|
||||
"@octokit/auth-oauth-device": "^4.0.2",
|
||||
"@octokit/rest": "^19.0.4",
|
||||
"@open-wc/dev-server-hmr": "^0.0.2",
|
||||
"@rollup/plugin-babel": "^5.2.1",
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
@ -178,12 +181,13 @@
|
||||
"@types/mocha": "^8",
|
||||
"@types/qrcode": "^1.4.2",
|
||||
"@types/sortablejs": "^1",
|
||||
"@types/tar": "^6",
|
||||
"@types/webspeechapi": "^0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^4.32.0",
|
||||
"@typescript-eslint/parser": "^4.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
||||
"@typescript-eslint/parser": "^5.44.0",
|
||||
"@web/dev-server": "^0.0.24",
|
||||
"@web/dev-server-rollup": "^0.2.11",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-loader": "^9.1.0",
|
||||
"chai": "^4.3.4",
|
||||
"del": "^4.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
@ -209,6 +213,7 @@
|
||||
"html-minifier": "^4.0.0",
|
||||
"husky": "^8.0.1",
|
||||
"instant-mocha": "^1.3.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"lit-analyzer": "^1.2.1",
|
||||
"lodash.template": "^4.5.0",
|
||||
@ -229,9 +234,10 @@
|
||||
"sinon": "^11.0.0",
|
||||
"source-map-url": "^0.4.0",
|
||||
"systemjs": "^6.3.2",
|
||||
"tar": "^6.1.11",
|
||||
"terser-webpack-plugin": "^5.2.4",
|
||||
"ts-lit-plugin": "^1.2.1",
|
||||
"typescript": "^4.4.3",
|
||||
"typescript": "^4.9.3",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"webpack": "^5.55.1",
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20221108.0"
|
||||
version = "20221130.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -60,6 +60,7 @@ import {
|
||||
mdiWaterPercent,
|
||||
mdiWeatherCloudy,
|
||||
mdiWeatherPouring,
|
||||
mdiWeatherRainy,
|
||||
mdiWeatherWindy,
|
||||
mdiWeight,
|
||||
mdiWhiteBalanceSunny,
|
||||
@ -113,6 +114,7 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
siren: mdiBullhorn,
|
||||
simple_alarm: mdiBell,
|
||||
sun: mdiWhiteBalanceSunny,
|
||||
text: mdiFormTextbox,
|
||||
timer: mdiTimerOutline,
|
||||
updater: mdiCloudUpload,
|
||||
vacuum: mdiRobotVacuum,
|
||||
@ -147,6 +149,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
pm25: mdiMolecule,
|
||||
power: mdiFlash,
|
||||
power_factor: mdiAngleAcute,
|
||||
precipitation: mdiWeatherRainy,
|
||||
precipitation_intensity: mdiWeatherPouring,
|
||||
pressure: mdiGauge,
|
||||
reactive_power: mdiFlash,
|
||||
@ -180,6 +183,7 @@ export const DOMAINS_WITH_CARD = [
|
||||
"script",
|
||||
"select",
|
||||
"timer",
|
||||
"text",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
];
|
||||
@ -212,6 +216,7 @@ export const DOMAINS_INPUT_ROW = [
|
||||
"script",
|
||||
"select",
|
||||
"switch",
|
||||
"text",
|
||||
"vacuum",
|
||||
];
|
||||
|
||||
|
@ -9,7 +9,6 @@ if (__BUILD__ === "latest" && polyfillsLoaded) {
|
||||
|
||||
const formatRelTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
// @ts-expect-error
|
||||
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" })
|
||||
);
|
||||
|
||||
@ -25,7 +24,6 @@ export const relativeTime = (
|
||||
}
|
||||
return Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
// @ts-expect-error
|
||||
unit: diff.unit,
|
||||
unitDisplay: "long",
|
||||
}).format(Math.abs(diff.value));
|
||||
|
@ -8,10 +8,11 @@ export const alarmControlPanelColor = (state?: string): string | undefined => {
|
||||
return "alarm-armed";
|
||||
case "pending":
|
||||
return "alarm-pending";
|
||||
case "arming":
|
||||
case "disarming":
|
||||
return "alarm-arming";
|
||||
case "triggered":
|
||||
return "alarm-triggered";
|
||||
case "disarmed":
|
||||
return "alarm-disarmed";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
const NORMAL_DEVICE_CLASSES = new Set([
|
||||
"battery_charging",
|
||||
"connectivity",
|
||||
"light",
|
||||
"moving",
|
||||
"plug",
|
||||
"power",
|
||||
"presence",
|
||||
"running",
|
||||
const ALERTING_DEVICE_CLASSES = new Set([
|
||||
"battery",
|
||||
"carbon_monoxide",
|
||||
"gas",
|
||||
"heat",
|
||||
"problem",
|
||||
"safety",
|
||||
"smoke",
|
||||
"tamper",
|
||||
]);
|
||||
|
||||
export const binarySensorColor = (stateObj: HassEntity): string | undefined => {
|
||||
const deviceClass = stateObj?.attributes.device_class;
|
||||
|
||||
return deviceClass && NORMAL_DEVICE_CLASSES.has(deviceClass)
|
||||
? "binary-sensor"
|
||||
: "binary-sensor-danger";
|
||||
return deviceClass && ALERTING_DEVICE_CLASSES.has(deviceClass)
|
||||
? "binary-sensor-alerting"
|
||||
: "binary-sensor";
|
||||
};
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
const SECURE_DEVICE_CLASSES = new Set(["door", "gate", "garage", "window"]);
|
||||
|
||||
export const coverColor = (stateObj?: HassEntity): string | undefined => {
|
||||
const isSecure =
|
||||
stateObj?.attributes.device_class &&
|
||||
SECURE_DEVICE_CLASSES.has(stateObj.attributes.device_class);
|
||||
return isSecure ? "cover-secure" : "cover";
|
||||
};
|
@ -2,8 +2,6 @@ export const lockColor = (state?: string): string | undefined => {
|
||||
switch (state) {
|
||||
case "locked":
|
||||
return "lock-locked";
|
||||
case "unlocked":
|
||||
return "lock-unlocked";
|
||||
case "jammed":
|
||||
return "lock-jammed";
|
||||
case "locking":
|
||||
|
@ -8,25 +8,5 @@ export const sensorColor = (stateObj: HassEntity): string | undefined => {
|
||||
return batteryStateColor(stateObj);
|
||||
}
|
||||
|
||||
switch (deviceClass) {
|
||||
case "apparent_power":
|
||||
case "current":
|
||||
case "energy":
|
||||
case "gas":
|
||||
case "power_factor":
|
||||
case "power":
|
||||
case "reactive_power":
|
||||
case "voltage":
|
||||
return "sensor-energy";
|
||||
case "temperature":
|
||||
return "sensor-temperature";
|
||||
case "humidity":
|
||||
return "sensor-humidity";
|
||||
case "illuminance":
|
||||
return "sensor-illuminance";
|
||||
case "moisture":
|
||||
return "sensor-moisture";
|
||||
}
|
||||
|
||||
return "sensor";
|
||||
return undefined;
|
||||
};
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
|
||||
export const computeActiveState = (stateObj: HassEntity): string => {
|
||||
if (UNAVAILABLE_STATES.includes(stateObj.state)) {
|
||||
return stateObj.state;
|
||||
}
|
||||
|
||||
const domain = stateObj.entity_id.split(".")[0];
|
||||
let state = stateObj.state;
|
||||
|
||||
if (domain === "climate") {
|
||||
state = stateObj.attributes.hvac_action;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
@ -12,8 +12,10 @@ import {
|
||||
mdiCircle,
|
||||
mdiWindowShutter,
|
||||
mdiWindowShutterOpen,
|
||||
mdiBlinds,
|
||||
mdiBlindsOpen,
|
||||
mdiBlindsHorizontal,
|
||||
mdiBlindsHorizontalClosed,
|
||||
mdiRollerShade,
|
||||
mdiRollerShadeClosed,
|
||||
mdiWindowClosed,
|
||||
mdiWindowOpen,
|
||||
mdiArrowExpandHorizontal,
|
||||
@ -79,6 +81,16 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
|
||||
return mdiCurtains;
|
||||
}
|
||||
case "blind":
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return mdiArrowUpBox;
|
||||
case "closing":
|
||||
return mdiArrowDownBox;
|
||||
case "closed":
|
||||
return mdiBlindsHorizontalClosed;
|
||||
default:
|
||||
return mdiBlindsHorizontal;
|
||||
}
|
||||
case "shade":
|
||||
switch (state) {
|
||||
case "opening":
|
||||
@ -86,9 +98,9 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
|
||||
case "closing":
|
||||
return mdiArrowDownBox;
|
||||
case "closed":
|
||||
return mdiBlinds;
|
||||
return mdiRollerShadeClosed;
|
||||
default:
|
||||
return mdiBlindsOpen;
|
||||
return mdiRollerShade;
|
||||
}
|
||||
case "window":
|
||||
switch (state) {
|
||||
|
@ -1,35 +1,38 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { OFF_STATES } from "../../data/entity";
|
||||
import { OFF_STATES, UNAVAILABLE } from "../../data/entity";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
const NORMAL_UNKNOWN_DOMAIN = ["button", "input_button", "scene"];
|
||||
const NORMAL_OFF_DOMAIN = ["script"];
|
||||
|
||||
export function stateActive(stateObj: HassEntity): boolean {
|
||||
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const state = stateObj.state;
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
|
||||
if (
|
||||
OFF_STATES.includes(state) &&
|
||||
!(NORMAL_UNKNOWN_DOMAIN.includes(domain) && state === "unknown") &&
|
||||
!(NORMAL_OFF_DOMAIN.includes(domain) && state === "script")
|
||||
) {
|
||||
if (["button", "input_button", "scene"].includes(domain)) {
|
||||
return compareState !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
if (OFF_STATES.includes(compareState)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Custom cases
|
||||
switch (domain) {
|
||||
case "cover":
|
||||
return state === "open" || state === "opening";
|
||||
return !["closed", "closing"].includes(compareState);
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
return state !== "not_home";
|
||||
case "media-player":
|
||||
return state !== "idle" && state !== "standby";
|
||||
return compareState !== "not_home";
|
||||
case "alarm_control_panel":
|
||||
return compareState !== "disarmed";
|
||||
case "lock":
|
||||
return compareState !== "unlocked";
|
||||
case "media_player":
|
||||
return compareState !== "standby";
|
||||
case "vacuum":
|
||||
return state === "on" || state === "cleaning";
|
||||
return !["idle", "docked", "paused"].includes(compareState);
|
||||
case "plant":
|
||||
return state === "problem";
|
||||
return compareState === "problem";
|
||||
case "group":
|
||||
return ["on", "home", "open", "locked", "problem"].includes(compareState);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
@ -4,45 +4,47 @@ import { UpdateEntity, updateIsInstalling } from "../../data/update";
|
||||
import { alarmControlPanelColor } from "./color/alarm_control_panel_color";
|
||||
import { binarySensorColor } from "./color/binary_sensor_color";
|
||||
import { climateColor } from "./color/climate_color";
|
||||
import { coverColor } from "./color/cover_color";
|
||||
import { lockColor } from "./color/lock_color";
|
||||
import { sensorColor } from "./color/sensor_color";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stateActive } from "./state_active";
|
||||
|
||||
export const stateColorCss = (stateObj?: HassEntity) => {
|
||||
if (!stateObj || !stateActive(stateObj)) {
|
||||
export const stateColorCss = (stateObj?: HassEntity, state?: string) => {
|
||||
if (!stateObj || !stateActive(stateObj, state)) {
|
||||
return `var(--rgb-disabled-color)`;
|
||||
}
|
||||
|
||||
const color = stateColor(stateObj);
|
||||
const color = stateColor(stateObj, state);
|
||||
|
||||
if (color) {
|
||||
return `var(--rgb-state-${color}-color)`;
|
||||
}
|
||||
|
||||
return `var(--rgb-primary-color)`;
|
||||
return `var(--rgb-state-default-color)`;
|
||||
};
|
||||
|
||||
export const stateColor = (stateObj: HassEntity) => {
|
||||
const state = stateObj.state;
|
||||
export const stateColor = (stateObj: HassEntity, state?: string) => {
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
switch (domain) {
|
||||
case "alarm_control_panel":
|
||||
return alarmControlPanelColor(state);
|
||||
return alarmControlPanelColor(compareState);
|
||||
|
||||
case "binary_sensor":
|
||||
return binarySensorColor(stateObj);
|
||||
|
||||
case "cover":
|
||||
return coverColor(stateObj);
|
||||
return "cover";
|
||||
|
||||
case "climate":
|
||||
return climateColor(state);
|
||||
return climateColor(compareState);
|
||||
|
||||
case "fan":
|
||||
return "fan";
|
||||
|
||||
case "lock":
|
||||
return lockColor(state);
|
||||
return lockColor(compareState);
|
||||
|
||||
case "light":
|
||||
return "light";
|
||||
@ -53,18 +55,20 @@ export const stateColor = (stateObj: HassEntity) => {
|
||||
case "media_player":
|
||||
return "media-player";
|
||||
|
||||
case "person":
|
||||
case "device_tracker":
|
||||
return "person";
|
||||
|
||||
case "sensor":
|
||||
return sensorColor(stateObj);
|
||||
|
||||
case "vacuum":
|
||||
return "vacuum";
|
||||
|
||||
case "siren":
|
||||
return "siren";
|
||||
|
||||
case "sun":
|
||||
return state === "above_horizon" ? "sun-day" : "sun-night";
|
||||
return compareState === "above_horizon" ? "sun-day" : "sun-night";
|
||||
|
||||
case "switch":
|
||||
return "switch";
|
||||
|
||||
case "update":
|
||||
return updateIsInstalling(stateObj as UpdateEntity)
|
||||
|
@ -1,60 +1,32 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const iconColorCSS = css`
|
||||
ha-state-icon[data-domain="alert"][data-state="on"],
|
||||
ha-state-icon[data-domain="automation"][data-state="on"],
|
||||
ha-state-icon[data-domain="binary_sensor"][data-state="on"],
|
||||
ha-state-icon[data-domain="calendar"][data-state="on"],
|
||||
ha-state-icon[data-domain="camera"][data-state="streaming"],
|
||||
ha-state-icon[data-domain="cover"][data-state="open"],
|
||||
ha-state-icon[data-domain="device_tracker"][data-state="home"],
|
||||
ha-state-icon[data-domain="fan"][data-state="on"],
|
||||
ha-state-icon[data-domain="humidifier"][data-state="on"],
|
||||
ha-state-icon[data-domain="light"][data-state="on"],
|
||||
ha-state-icon[data-domain="input_boolean"][data-state="on"],
|
||||
ha-state-icon[data-domain="lock"][data-state="unlocked"],
|
||||
ha-state-icon[data-domain="media_player"][data-state="on"],
|
||||
ha-state-icon[data-domain="media_player"][data-state="paused"],
|
||||
ha-state-icon[data-domain="media_player"][data-state="playing"],
|
||||
ha-state-icon[data-domain="remote"][data-state="on"],
|
||||
ha-state-icon[data-domain="script"][data-state="on"],
|
||||
ha-state-icon[data-domain="sun"][data-state="above_horizon"],
|
||||
ha-state-icon[data-domain="switch"][data-state="on"],
|
||||
ha-state-icon[data-domain="timer"][data-state="active"],
|
||||
ha-state-icon[data-domain="vacuum"][data-state="cleaning"],
|
||||
ha-state-icon[data-domain="group"][data-state="on"],
|
||||
ha-state-icon[data-domain="group"][data-state="home"],
|
||||
ha-state-icon[data-domain="group"][data-state="open"],
|
||||
ha-state-icon[data-domain="group"][data-state="locked"],
|
||||
ha-state-icon[data-domain="group"][data-state="problem"] {
|
||||
ha-state-icon[data-active][data-domain="alert"],
|
||||
ha-state-icon[data-active][data-domain="automation"],
|
||||
ha-state-icon[data-active][data-domain="binary_sensor"],
|
||||
ha-state-icon[data-active][data-domain="calendar"],
|
||||
ha-state-icon[data-active][data-domain="camera"],
|
||||
ha-state-icon[data-active][data-domain="cover"],
|
||||
ha-state-icon[data-active][data-domain="device_tracker"],
|
||||
ha-state-icon[data-active][data-domain="fan"],
|
||||
ha-state-icon[data-active][data-domain="humidifier"],
|
||||
ha-state-icon[data-active][data-domain="light"],
|
||||
ha-state-icon[data-active][data-domain="input_boolean"],
|
||||
ha-state-icon[data-active][data-domain="lock"],
|
||||
ha-state-icon[data-active][data-domain="media_player"],
|
||||
ha-state-icon[data-active][data-domain="remote"],
|
||||
ha-state-icon[data-active][data-domain="script"],
|
||||
ha-state-icon[data-active][data-domain="sun"],
|
||||
ha-state-icon[data-active][data-domain="switch"],
|
||||
ha-state-icon[data-active][data-domain="timer"],
|
||||
ha-state-icon[data-active][data-domain="vacuum"],
|
||||
ha-state-icon[data-active][data-domain="group"] {
|
||||
color: var(--paper-item-icon-active-color, #fdd835);
|
||||
}
|
||||
|
||||
ha-state-icon[data-domain="climate"][data-state="cooling"] {
|
||||
color: var(--cool-color, var(--state-climate-cool-color));
|
||||
}
|
||||
|
||||
ha-state-icon[data-domain="climate"][data-state="heating"] {
|
||||
color: var(--heat-color, var(--state-climate-heat-color));
|
||||
}
|
||||
|
||||
ha-state-icon[data-domain="climate"][data-state="drying"] {
|
||||
color: var(--dry-color, var(--state-climate-dry-color));
|
||||
}
|
||||
|
||||
ha-state-icon[data-domain="alarm_control_panel"] {
|
||||
color: var(--alarm-color-armed, var(--label-badge-red));
|
||||
}
|
||||
ha-state-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
|
||||
color: var(--alarm-color-disarmed, var(--label-badge-green));
|
||||
}
|
||||
ha-state-icon[data-domain="alarm_control_panel"][data-state="pending"],
|
||||
ha-state-icon[data-domain="alarm_control_panel"][data-state="arming"] {
|
||||
color: var(--alarm-color-pending, var(--label-badge-yellow));
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
ha-state-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
|
||||
color: var(--alarm-color-triggered, var(--label-badge-red));
|
||||
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="pending"],
|
||||
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="arming"],
|
||||
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="triggered"] {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@ -70,10 +42,6 @@ export const iconColorCSS = css`
|
||||
}
|
||||
}
|
||||
|
||||
ha-state-icon[data-domain="plant"][data-state="problem"] {
|
||||
color: var(--state-icon-error-color);
|
||||
}
|
||||
|
||||
/* Color the icon if unavailable */
|
||||
ha-state-icon[data-state="unavailable"] {
|
||||
color: var(--state-unavailable-color);
|
||||
|
@ -198,7 +198,6 @@ export const loadPolyfillLocales = async (language: string) => {
|
||||
Intl.NumberFormat.__addLocaleData(await result.json());
|
||||
}
|
||||
if (
|
||||
// @ts-expect-error
|
||||
Intl.RelativeTimeFormat &&
|
||||
// @ts-ignore
|
||||
typeof Intl.RelativeTimeFormat.__addLocaleData === "function"
|
||||
|
@ -3,8 +3,10 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { rgb2hex } from "../../common/color/convert-color";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { stateActive } from "../../common/entity/state_active";
|
||||
import { stateColor } from "../../common/entity/state_color";
|
||||
import { numberFormatToLocale } from "../../common/number/format_number";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { TimelineEntity } from "../../data/history";
|
||||
@ -12,65 +14,55 @@ import { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { TimeLineData } from "./timeline-chart/const";
|
||||
|
||||
/** Binary sensor device classes for which the static colors for on/off are NOT inverted.
|
||||
* List the ones were "on" = good or normal state => should be rendered "green".
|
||||
* Note: It is now a "not inverted" list (compared to the past) since we now have more inverted ones.
|
||||
*/
|
||||
const BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED = new Set([
|
||||
"battery_charging",
|
||||
"connectivity",
|
||||
"light",
|
||||
"moving",
|
||||
"plug",
|
||||
"power",
|
||||
"presence",
|
||||
"running",
|
||||
]);
|
||||
|
||||
const STATIC_STATE_COLORS = new Set([
|
||||
"on",
|
||||
"off",
|
||||
"home",
|
||||
"not_home",
|
||||
"unavailable",
|
||||
"unknown",
|
||||
"idle",
|
||||
]);
|
||||
|
||||
const stateColorTokenMap: Map<string, string> = new Map();
|
||||
const stateColorMap: Map<string, string> = new Map();
|
||||
|
||||
let colorIndex = 0;
|
||||
|
||||
const invertOnOff = (entityState?: HassEntity) =>
|
||||
entityState &&
|
||||
computeDomain(entityState.entity_id) === "binary_sensor" &&
|
||||
"device_class" in entityState.attributes &&
|
||||
!BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED.has(
|
||||
entityState.attributes.device_class!
|
||||
);
|
||||
export const getStateColorToken = (
|
||||
stateString: string,
|
||||
entityState?: HassEntity
|
||||
) => {
|
||||
if (!entityState || !stateActive(entityState, stateString)) {
|
||||
return `disabled`;
|
||||
}
|
||||
const color = stateColor(entityState, stateString);
|
||||
if (color) {
|
||||
return `state-${color}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getColor = (
|
||||
stateString: string,
|
||||
entityState: HassEntity,
|
||||
computedStyles: CSSStyleDeclaration
|
||||
computedStyles: CSSStyleDeclaration,
|
||||
entityState?: HassEntity
|
||||
) => {
|
||||
// Inversion is only valid for "on" or "off" state
|
||||
if (
|
||||
(stateString === "on" || stateString === "off") &&
|
||||
invertOnOff(entityState)
|
||||
) {
|
||||
stateString = stateString === "on" ? "off" : "on";
|
||||
const stateColorToken = getStateColorToken(stateString, entityState);
|
||||
|
||||
if (stateColorToken) {
|
||||
if (stateColorTokenMap.has(stateColorToken)) {
|
||||
return stateColorTokenMap.get(stateColorToken);
|
||||
}
|
||||
const value = computedStyles.getPropertyValue(
|
||||
`--rgb-${stateColorToken}-color`
|
||||
);
|
||||
|
||||
if (value) {
|
||||
const parsedValue = value.split(",").map((v) => Number(v)) as [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
];
|
||||
const hexValue = rgb2hex(parsedValue);
|
||||
stateColorTokenMap.set(stateColorToken, hexValue);
|
||||
return hexValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (stateColorMap.has(stateString)) {
|
||||
return stateColorMap.get(stateString);
|
||||
}
|
||||
if (STATIC_STATE_COLORS.has(stateString)) {
|
||||
const color = computedStyles.getPropertyValue(
|
||||
`--state-${stateString}-color`
|
||||
);
|
||||
stateColorMap.set(stateString, color);
|
||||
return color;
|
||||
}
|
||||
const color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
stateColorMap.set(stateString, color);
|
||||
@ -281,8 +273,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
label: locState,
|
||||
color: getColor(
|
||||
prevState,
|
||||
this.hass.states[stateInfo.entity_id],
|
||||
computedStyles
|
||||
computedStyles,
|
||||
this.hass.states[stateInfo.entity_id]
|
||||
),
|
||||
});
|
||||
|
||||
@ -299,8 +291,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
label: locState,
|
||||
color: getColor(
|
||||
prevState,
|
||||
this.hass.states[stateInfo.entity_id],
|
||||
computedStyles
|
||||
computedStyles,
|
||||
this.hass.states[stateInfo.entity_id]
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -26,12 +26,13 @@ import {
|
||||
getStatisticMetadata,
|
||||
Statistics,
|
||||
statisticsHaveType,
|
||||
StatisticsMetaData,
|
||||
StatisticType,
|
||||
} from "../../data/recorder";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
|
||||
export type ExtendedStatisticType = StatisticType | "state";
|
||||
export type ExtendedStatisticType = StatisticType | "state" | "change";
|
||||
|
||||
export const statTypeMap: Record<ExtendedStatisticType, StatisticType> = {
|
||||
mean: "mean",
|
||||
@ -39,13 +40,20 @@ export const statTypeMap: Record<ExtendedStatisticType, StatisticType> = {
|
||||
max: "max",
|
||||
sum: "sum",
|
||||
state: "sum",
|
||||
change: "sum",
|
||||
};
|
||||
|
||||
@customElement("statistics-chart")
|
||||
class StatisticsChart extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public statisticsData!: Statistics;
|
||||
|
||||
@property({ attribute: false }) public metadata?: Record<
|
||||
string,
|
||||
StatisticsMetaData
|
||||
>;
|
||||
|
||||
@property() public names: boolean | Record<string, string> = false;
|
||||
|
||||
@property() public unit?: string;
|
||||
@ -76,7 +84,7 @@ class StatisticsChart extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
if (!this.hasUpdated || changedProps.has("unit")) {
|
||||
this._createOptions();
|
||||
}
|
||||
if (changedProps.has("statisticsData") || changedProps.has("statTypes")) {
|
||||
@ -120,7 +128,7 @@ class StatisticsChart extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _createOptions() {
|
||||
private _createOptions(unit?: string) {
|
||||
this._chartOptions = {
|
||||
parsing: false,
|
||||
animation: false,
|
||||
@ -154,8 +162,8 @@ class StatisticsChart extends LitElement {
|
||||
maxTicksLimit: 7,
|
||||
},
|
||||
title: {
|
||||
display: this.unit,
|
||||
text: this.unit,
|
||||
display: unit || this.unit,
|
||||
text: unit || this.unit,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -189,6 +197,7 @@ class StatisticsChart extends LitElement {
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.4,
|
||||
cubicInterpolationMode: "monotone",
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
bar: { borderWidth: 1.5, borderRadius: 4 },
|
||||
@ -220,12 +229,12 @@ class StatisticsChart extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const statisticsMetaData = await this._getStatisticsMetaData(
|
||||
Object.keys(this.statisticsData)
|
||||
);
|
||||
const statisticsMetaData =
|
||||
this.metadata ||
|
||||
(await this._getStatisticsMetaData(Object.keys(this.statisticsData)));
|
||||
|
||||
let colorIndex = 0;
|
||||
const statisticsData = Object.values(this.statisticsData);
|
||||
const statisticsData = Object.entries(this.statisticsData);
|
||||
const totalDataSets: ChartDataset<"line">[] = [];
|
||||
let endTime: Date;
|
||||
|
||||
@ -238,7 +247,7 @@ class StatisticsChart extends LitElement {
|
||||
// Get the highest date from the last date of each statistic
|
||||
new Date(
|
||||
Math.max(
|
||||
...statisticsData.map((stats) =>
|
||||
...statisticsData.map(([_, stats]) =>
|
||||
new Date(stats[stats.length - 1].start).getTime()
|
||||
)
|
||||
)
|
||||
@ -251,19 +260,19 @@ class StatisticsChart extends LitElement {
|
||||
let unit: string | undefined | null;
|
||||
|
||||
const names = this.names || {};
|
||||
statisticsData.forEach((stats) => {
|
||||
const firstStat = stats[0];
|
||||
const meta = statisticsMetaData?.[firstStat.statistic_id];
|
||||
let name = names[firstStat.statistic_id];
|
||||
statisticsData.forEach(([statistic_id, stats]) => {
|
||||
const meta = statisticsMetaData?.[statistic_id];
|
||||
let name = names[statistic_id];
|
||||
if (name === undefined) {
|
||||
name = getStatisticLabel(this.hass, firstStat.statistic_id, meta);
|
||||
name = getStatisticLabel(this.hass, statistic_id, meta);
|
||||
}
|
||||
|
||||
if (!this.unit) {
|
||||
if (unit === undefined) {
|
||||
unit = getDisplayUnit(this.hass, firstStat.statistic_id, meta);
|
||||
unit = getDisplayUnit(this.hass, statistic_id, meta);
|
||||
} else if (
|
||||
unit !== getDisplayUnit(this.hass, firstStat.statistic_id, meta)
|
||||
unit !== null &&
|
||||
unit !== getDisplayUnit(this.hass, statistic_id, meta)
|
||||
) {
|
||||
// Clear unit if not all statistics have same unit
|
||||
unit = null;
|
||||
@ -272,33 +281,38 @@ class StatisticsChart extends LitElement {
|
||||
|
||||
// array containing [value1, value2, etc]
|
||||
let prevValues: Array<number | null> | null = null;
|
||||
let prevEndTime: Date | undefined;
|
||||
|
||||
// The datasets for the current statistic
|
||||
const statDataSets: ChartDataset<"line">[] = [];
|
||||
|
||||
const pushData = (
|
||||
timestamp: Date,
|
||||
start: Date,
|
||||
end: Date,
|
||||
dataValues: Array<number | null> | null
|
||||
) => {
|
||||
if (!dataValues) return;
|
||||
if (timestamp > endTime) {
|
||||
if (start > end) {
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
return;
|
||||
}
|
||||
statDataSets.forEach((d, i) => {
|
||||
if (dataValues[i] === null && prevValues && prevValues[i] !== null) {
|
||||
// null data values show up as gaps in the chart.
|
||||
// If the current value for the dataset is null and the previous
|
||||
// value of the data set is not null, then add an 'end' point
|
||||
// to the chart for the previous value. Otherwise the gap will
|
||||
// be too big. It will go from the start of the previous data
|
||||
// value until the start of the next data value.
|
||||
d.data.push({ x: timestamp.getTime(), y: prevValues[i]! });
|
||||
if (
|
||||
prevEndTime &&
|
||||
prevValues &&
|
||||
prevEndTime.getTime() !== start.getTime()
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data.push({ x: prevEndTime.getTime(), y: prevValues[i]! });
|
||||
// @ts-expect-error
|
||||
d.data.push({ x: prevEndTime.getTime(), y: null });
|
||||
}
|
||||
d.data.push({ x: timestamp.getTime(), y: dataValues[i]! });
|
||||
d.data.push({ x: start.getTime(), y: dataValues[i]! });
|
||||
});
|
||||
prevValues = dataValues;
|
||||
prevEndTime = end;
|
||||
};
|
||||
|
||||
const color = getGraphColorByIndex(colorIndex, this._computedStyle!);
|
||||
@ -354,49 +368,49 @@ class StatisticsChart extends LitElement {
|
||||
|
||||
let prevDate: Date | null = null;
|
||||
// Process chart data.
|
||||
let prevSum: number | null = null;
|
||||
let firstSum: number | null | undefined = null;
|
||||
let prevSum: number | null | undefined = null;
|
||||
stats.forEach((stat) => {
|
||||
const date = new Date(stat.start);
|
||||
if (prevDate === date) {
|
||||
const startDate = new Date(stat.start);
|
||||
if (prevDate === startDate) {
|
||||
return;
|
||||
}
|
||||
prevDate = date;
|
||||
prevDate = startDate;
|
||||
const dataValues: Array<number | null> = [];
|
||||
statTypes.forEach((type) => {
|
||||
let val: number | null;
|
||||
let val: number | null | undefined;
|
||||
if (type === "sum") {
|
||||
if (prevSum === null) {
|
||||
if (firstSum === null || firstSum === undefined) {
|
||||
val = 0;
|
||||
prevSum = stat.sum;
|
||||
firstSum = stat.sum;
|
||||
} else {
|
||||
val = (stat.sum || 0) - prevSum;
|
||||
val = (stat.sum || 0) - firstSum;
|
||||
}
|
||||
} else if (type === "change") {
|
||||
if (prevSum === null || prevSum === undefined) {
|
||||
prevSum = stat.sum;
|
||||
return;
|
||||
}
|
||||
val = (stat.sum || 0) - prevSum;
|
||||
prevSum = stat.sum;
|
||||
} else {
|
||||
val = stat[type];
|
||||
}
|
||||
dataValues.push(val !== null ? Math.round(val * 100) / 100 : null);
|
||||
dataValues.push(
|
||||
val !== null && val !== undefined
|
||||
? Math.round(val * 100) / 100
|
||||
: null
|
||||
);
|
||||
});
|
||||
pushData(date, dataValues);
|
||||
pushData(startDate, new Date(stat.end), dataValues);
|
||||
});
|
||||
|
||||
// Add an entry for final values
|
||||
pushData(endTime, prevValues);
|
||||
|
||||
// Concat two arrays
|
||||
Array.prototype.push.apply(totalDataSets, statDataSets);
|
||||
});
|
||||
|
||||
if (unit !== null) {
|
||||
this._chartOptions = {
|
||||
...this._chartOptions,
|
||||
scales: {
|
||||
...this._chartOptions!.scales,
|
||||
y: {
|
||||
...(this._chartOptions!.scales!.y as Record<string, unknown>),
|
||||
title: { display: unit, text: unit },
|
||||
},
|
||||
},
|
||||
};
|
||||
if (unit) {
|
||||
this._createOptions(unit);
|
||||
}
|
||||
|
||||
this._chartData = {
|
||||
|
273
src/components/country-datalist.ts
Normal file
273
src/components/country-datalist.ts
Normal file
@ -0,0 +1,273 @@
|
||||
export const countries = [
|
||||
"AD",
|
||||
"AE",
|
||||
"AF",
|
||||
"AG",
|
||||
"AI",
|
||||
"AL",
|
||||
"AM",
|
||||
"AO",
|
||||
"AQ",
|
||||
"AR",
|
||||
"AS",
|
||||
"AT",
|
||||
"AU",
|
||||
"AW",
|
||||
"AX",
|
||||
"AZ",
|
||||
"BA",
|
||||
"BB",
|
||||
"BD",
|
||||
"BE",
|
||||
"BF",
|
||||
"BG",
|
||||
"BH",
|
||||
"BI",
|
||||
"BJ",
|
||||
"BL",
|
||||
"BM",
|
||||
"BN",
|
||||
"BO",
|
||||
"BQ",
|
||||
"BR",
|
||||
"BS",
|
||||
"BT",
|
||||
"BV",
|
||||
"BW",
|
||||
"BY",
|
||||
"BZ",
|
||||
"CA",
|
||||
"CC",
|
||||
"CD",
|
||||
"CF",
|
||||
"CG",
|
||||
"CH",
|
||||
"CI",
|
||||
"CK",
|
||||
"CL",
|
||||
"CM",
|
||||
"CN",
|
||||
"CO",
|
||||
"CR",
|
||||
"CU",
|
||||
"CV",
|
||||
"CW",
|
||||
"CX",
|
||||
"CY",
|
||||
"CZ",
|
||||
"DE",
|
||||
"DJ",
|
||||
"DK",
|
||||
"DM",
|
||||
"DO",
|
||||
"DZ",
|
||||
"EC",
|
||||
"EE",
|
||||
"EG",
|
||||
"EH",
|
||||
"ER",
|
||||
"ES",
|
||||
"ET",
|
||||
"FI",
|
||||
"FJ",
|
||||
"FK",
|
||||
"FM",
|
||||
"FO",
|
||||
"FR",
|
||||
"GA",
|
||||
"GB",
|
||||
"GD",
|
||||
"GE",
|
||||
"GF",
|
||||
"GG",
|
||||
"GH",
|
||||
"GI",
|
||||
"GL",
|
||||
"GM",
|
||||
"GN",
|
||||
"GP",
|
||||
"GQ",
|
||||
"GR",
|
||||
"GS",
|
||||
"GT",
|
||||
"GU",
|
||||
"GW",
|
||||
"GY",
|
||||
"HK",
|
||||
"HM",
|
||||
"HN",
|
||||
"HR",
|
||||
"HT",
|
||||
"HU",
|
||||
"ID",
|
||||
"IE",
|
||||
"IL",
|
||||
"IM",
|
||||
"IN",
|
||||
"IO",
|
||||
"IQ",
|
||||
"IR",
|
||||
"IS",
|
||||
"IT",
|
||||
"JE",
|
||||
"JM",
|
||||
"JO",
|
||||
"JP",
|
||||
"KE",
|
||||
"KG",
|
||||
"KH",
|
||||
"KI",
|
||||
"KM",
|
||||
"KN",
|
||||
"KP",
|
||||
"KR",
|
||||
"KW",
|
||||
"KY",
|
||||
"KZ",
|
||||
"LA",
|
||||
"LB",
|
||||
"LC",
|
||||
"LI",
|
||||
"LK",
|
||||
"LR",
|
||||
"LS",
|
||||
"LT",
|
||||
"LU",
|
||||
"LV",
|
||||
"LY",
|
||||
"MA",
|
||||
"MC",
|
||||
"MD",
|
||||
"ME",
|
||||
"MF",
|
||||
"MG",
|
||||
"MH",
|
||||
"MK",
|
||||
"ML",
|
||||
"MM",
|
||||
"MN",
|
||||
"MO",
|
||||
"MP",
|
||||
"MQ",
|
||||
"MR",
|
||||
"MS",
|
||||
"MT",
|
||||
"MU",
|
||||
"MV",
|
||||
"MW",
|
||||
"MX",
|
||||
"MY",
|
||||
"MZ",
|
||||
"NA",
|
||||
"NC",
|
||||
"NE",
|
||||
"NF",
|
||||
"NG",
|
||||
"NI",
|
||||
"NL",
|
||||
"NO",
|
||||
"NP",
|
||||
"NR",
|
||||
"NU",
|
||||
"NZ",
|
||||
"OM",
|
||||
"PA",
|
||||
"PE",
|
||||
"PF",
|
||||
"PG",
|
||||
"PH",
|
||||
"PK",
|
||||
"PL",
|
||||
"PM",
|
||||
"PN",
|
||||
"PR",
|
||||
"PS",
|
||||
"PT",
|
||||
"PW",
|
||||
"PY",
|
||||
"QA",
|
||||
"RE",
|
||||
"RO",
|
||||
"RS",
|
||||
"RU",
|
||||
"RW",
|
||||
"SA",
|
||||
"SB",
|
||||
"SC",
|
||||
"SD",
|
||||
"SE",
|
||||
"SG",
|
||||
"SH",
|
||||
"SI",
|
||||
"SJ",
|
||||
"SK",
|
||||
"SL",
|
||||
"SM",
|
||||
"SN",
|
||||
"SO",
|
||||
"SR",
|
||||
"SS",
|
||||
"ST",
|
||||
"SV",
|
||||
"SX",
|
||||
"SY",
|
||||
"SZ",
|
||||
"TC",
|
||||
"TD",
|
||||
"TF",
|
||||
"TG",
|
||||
"TH",
|
||||
"TJ",
|
||||
"TK",
|
||||
"TL",
|
||||
"TM",
|
||||
"TN",
|
||||
"TO",
|
||||
"TR",
|
||||
"TT",
|
||||
"TV",
|
||||
"TW",
|
||||
"TZ",
|
||||
"UA",
|
||||
"UG",
|
||||
"UM",
|
||||
"US",
|
||||
"UY",
|
||||
"UZ",
|
||||
"VA",
|
||||
"VC",
|
||||
"VE",
|
||||
"VG",
|
||||
"VI",
|
||||
"VN",
|
||||
"VU",
|
||||
"WF",
|
||||
"WS",
|
||||
"YE",
|
||||
"YT",
|
||||
"ZA",
|
||||
"ZM",
|
||||
"ZW",
|
||||
];
|
||||
|
||||
export const countryDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(undefined, {
|
||||
type: "region",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
|
||||
export const createCountryListEl = () => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "countries";
|
||||
for (const country of countries) {
|
||||
const option = document.createElement("option");
|
||||
option.value = country;
|
||||
option.innerText = countryDisplayNames
|
||||
? countryDisplayNames.of(country)!
|
||||
: country;
|
||||
list.appendChild(option);
|
||||
}
|
||||
return list;
|
||||
};
|
@ -158,13 +158,23 @@ export const currencies = [
|
||||
"ZWL",
|
||||
];
|
||||
|
||||
export const currencyDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(undefined, {
|
||||
type: "currency",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
|
||||
export const createCurrencyListEl = () => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "currencies";
|
||||
for (const currency of currencies) {
|
||||
const option = document.createElement("option");
|
||||
option.value = currency;
|
||||
option.innerHTML = currency;
|
||||
option.innerText = currencyDisplayNames
|
||||
? currencyDisplayNames.of(currency)!
|
||||
: currency;
|
||||
list.appendChild(option);
|
||||
}
|
||||
return list;
|
||||
|
@ -627,7 +627,7 @@ export class HaDataTable extends LitElement {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||
.mdc-data-table__row.clickable:not(.mdc-data-table__row--selected):hover {
|
||||
background-color: rgba(var(--rgb-primary-text-color), 0.04);
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,10 @@ import { Constructor } from "../types";
|
||||
|
||||
const Component = Vue.extend({
|
||||
props: {
|
||||
timePicker: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
twentyfourHours: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
@ -37,13 +41,19 @@ const Component = Vue.extend({
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
autoApply: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
// @ts-ignore
|
||||
return createElement(DateRangePicker, {
|
||||
props: {
|
||||
"time-picker": true,
|
||||
"auto-apply": false,
|
||||
// @ts-ignore
|
||||
"time-picker": this.timePicker,
|
||||
// @ts-ignore
|
||||
"auto-apply": this.autoApply,
|
||||
opens: "right",
|
||||
"show-dropdowns": false,
|
||||
// @ts-ignore
|
||||
|
@ -11,9 +11,10 @@ import {
|
||||
import { property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeActiveState } from "../../common/entity/compute_active_state";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { stateActive } from "../../common/entity/state_active";
|
||||
import { stateColor } from "../../common/entity/state_color";
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@ -35,6 +36,13 @@ export class StateBadge extends LitElement {
|
||||
|
||||
@state() private _iconStyle: { [name: string]: string } = {};
|
||||
|
||||
private get _stateColor() {
|
||||
const domain = this.stateObj
|
||||
? computeStateDomain(this.stateObj)
|
||||
: undefined;
|
||||
return this.stateColor || (domain === "light" && this.stateColor !== false);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
@ -50,15 +58,13 @@ export class StateBadge extends LitElement {
|
||||
}
|
||||
|
||||
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
|
||||
const active = this._stateColor && stateObj ? stateActive(stateObj) : false;
|
||||
|
||||
return html`<ha-state-icon
|
||||
style=${styleMap(this._iconStyle)}
|
||||
data-domain=${ifDefined(
|
||||
this.stateColor || (domain === "light" && this.stateColor !== false)
|
||||
? domain
|
||||
: undefined
|
||||
)}
|
||||
data-state=${stateObj ? computeActiveState(stateObj) : ""}
|
||||
?data-active=${active}
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
.icon=${this.overrideIcon}
|
||||
.state=${stateObj}
|
||||
></ha-state-icon>`;
|
||||
@ -69,7 +75,8 @@ export class StateBadge extends LitElement {
|
||||
if (
|
||||
!changedProps.has("stateObj") &&
|
||||
!changedProps.has("overrideImage") &&
|
||||
!changedProps.has("overrideIcon")
|
||||
!changedProps.has("overrideIcon") &&
|
||||
!changedProps.has("stateColor")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -100,11 +107,14 @@ export class StateBadge extends LitElement {
|
||||
}
|
||||
hostStyle.backgroundImage = `url(${imageUrl})`;
|
||||
this._showIcon = false;
|
||||
} else if (stateObj.state === "on") {
|
||||
if (this.stateColor !== false && stateObj.attributes.rgb_color) {
|
||||
} else if (stateActive(stateObj) && this._stateColor) {
|
||||
const iconColor = stateColor(stateObj);
|
||||
if (stateObj.attributes.rgb_color) {
|
||||
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
|
||||
} else if (iconColor) {
|
||||
iconStyle.color = `rgb(var(--rgb-state-${iconColor}-color))`;
|
||||
}
|
||||
if (stateObj.attributes.brightness && this.stateColor !== false) {
|
||||
if (stateObj.attributes.brightness) {
|
||||
const brightness = stateObj.attributes.brightness;
|
||||
if (typeof brightness !== "number") {
|
||||
const errorMessage = `Type error: state-badge expected number, but type of ${
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@ -9,22 +8,16 @@ import { computeDomain } from "../common/entity/compute_domain";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../data/area_registry";
|
||||
import {
|
||||
DeviceEntityLookup,
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../data/entity_registry";
|
||||
import { EntityRegistryEntry } from "../data/entity_registry";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
@ -42,7 +35,7 @@ const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
@ -88,34 +81,14 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
@state() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@state() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _filter?: string;
|
||||
private _suggestion?: string;
|
||||
|
||||
private _init = false;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
@ -287,14 +260,14 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this._devices && this._areas && this._entities) ||
|
||||
(!this._init && this.hass) ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
(this.comboBox as any).items = this._getAreas(
|
||||
this._areas!,
|
||||
this._devices!,
|
||||
this._entities!,
|
||||
Object.values(this.hass.areas),
|
||||
Object.values(this.hass.devices),
|
||||
Object.values(this.hass.entities),
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
@ -320,7 +293,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
? this.hass.localize("ui.components.area-picker.area")
|
||||
: this.label}
|
||||
.placeholder=${this.placeholder
|
||||
? this._area(this.placeholder)?.name
|
||||
? this.hass.areas[this.placeholder]?.name
|
||||
: undefined}
|
||||
.renderer=${rowRenderer}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@ -331,32 +304,30 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _area = memoizeOne((areaId: string): AreaRegistryEntry | undefined =>
|
||||
this._areas?.find((area) => area.area_id === areaId)
|
||||
);
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
this._filter = ev.detail.value;
|
||||
if (!this._filter) {
|
||||
const filter = ev.detail.value;
|
||||
if (!filter) {
|
||||
this.comboBox.filteredItems = this.comboBox.items;
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
if (!this.noAdd && this.comboBox._comboBox.filteredItems?.length === 0) {
|
||||
|
||||
const filteredItems = this.comboBox.items?.filter((item) =>
|
||||
item.name.toLowerCase().includes(filter!.toLowerCase())
|
||||
);
|
||||
if (!this.noAdd && filteredItems?.length === 0) {
|
||||
this._suggestion = filter;
|
||||
this.comboBox.filteredItems = [
|
||||
{
|
||||
area_id: "add_new_suggestion",
|
||||
name: this.hass.localize(
|
||||
"ui.components.area-picker.add_new_sugestion",
|
||||
{ name: this._filter }
|
||||
{ name: this._suggestion }
|
||||
),
|
||||
picture: null,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
this.comboBox.filteredItems = this.comboBox.items?.filter((item) =>
|
||||
item.name.toLowerCase().includes(this._filter!.toLowerCase())
|
||||
);
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,7 +365,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
"ui.components.area-picker.add_dialog.name"
|
||||
),
|
||||
defaultValue:
|
||||
newValue === "add_new_suggestion" ? this._filter : undefined,
|
||||
newValue === "add_new_suggestion" ? this._suggestion : undefined,
|
||||
confirm: async (name) => {
|
||||
if (!name) {
|
||||
return;
|
||||
@ -403,11 +374,11 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
const area = await createAreaRegistryEntry(this.hass, {
|
||||
name,
|
||||
});
|
||||
this._areas = [...this._areas!, area];
|
||||
const areas = [...Object.values(this.hass.areas), area];
|
||||
(this.comboBox as any).filteredItems = this._getAreas(
|
||||
this._areas!,
|
||||
this._devices!,
|
||||
this._entities!,
|
||||
areas,
|
||||
Object.values(this.hass.devices)!,
|
||||
Object.values(this.hass.entities)!,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
@ -427,10 +398,14 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel: () => {
|
||||
this._setValue(undefined);
|
||||
this._suggestion = undefined;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _setValue(value: string) {
|
||||
private _setValue(value?: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
|
@ -48,11 +48,14 @@ export class HaBarSlider extends LitElement {
|
||||
public disabled = false;
|
||||
|
||||
@property()
|
||||
public mode?: "start" | "end" | "indicator" = "start";
|
||||
public mode?: "start" | "end" | "cursor" = "start";
|
||||
|
||||
@property({ type: Boolean })
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public vertical = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-handle" })
|
||||
public showHandle = false;
|
||||
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
@ -65,16 +68,13 @@ export class HaBarSlider extends LitElement {
|
||||
@property({ type: Number })
|
||||
public max = 100;
|
||||
|
||||
@property()
|
||||
public label?: string;
|
||||
|
||||
private _mc?: HammerManager;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public pressed = false;
|
||||
|
||||
valueToPercentage(value: number) {
|
||||
return (value - this.min) / (this.max - this.min);
|
||||
return (this.boundedValue(value) - this.min) / (this.max - this.min);
|
||||
}
|
||||
|
||||
percentageToValue(value: number) {
|
||||
@ -244,11 +244,11 @@ export class HaBarSlider extends LitElement {
|
||||
})}
|
||||
>
|
||||
<div class="slider-track-background"></div>
|
||||
${this.mode === "indicator"
|
||||
${this.mode === "cursor"
|
||||
? html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"slider-track-indicator": true,
|
||||
"slider-track-cursor": true,
|
||||
vertical: this.vertical,
|
||||
})}
|
||||
></div>
|
||||
@ -259,6 +259,7 @@ export class HaBarSlider extends LitElement {
|
||||
"slider-track-bar": true,
|
||||
vertical: this.vertical,
|
||||
[this.mode ?? "start"]: true,
|
||||
"show-handle": this.showHandle,
|
||||
})}
|
||||
></div>
|
||||
`}
|
||||
@ -273,7 +274,7 @@ export class HaBarSlider extends LitElement {
|
||||
--slider-bar-color: rgb(var(--rgb-primary-color));
|
||||
--slider-bar-background: rgba(var(--rgb-disabled-color), 0.2);
|
||||
--slider-bar-thickness: 40px;
|
||||
--slider-bar-border-radius: 12px;
|
||||
--slider-bar-border-radius: 10px;
|
||||
height: var(--slider-bar-thickness);
|
||||
width: 100%;
|
||||
}
|
||||
@ -302,15 +303,21 @@ export class HaBarSlider extends LitElement {
|
||||
background: var(--slider-bar-background);
|
||||
}
|
||||
.slider .slider-track-bar {
|
||||
--border-radius: calc(var(--slider-bar-border-radius) / 2);
|
||||
--border-radius: var(--slider-bar-border-radius);
|
||||
--handle-size: 4px;
|
||||
--handle-margin: calc(var(--slider-bar-thickness) / 8);
|
||||
--slider-size: 100%;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--slider-bar-color);
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
.slider .slider-track-bar.show-handle {
|
||||
--slider-size: calc(
|
||||
100% - 2 * var(--handle-margin) - var(--handle-size)
|
||||
);
|
||||
}
|
||||
.slider .slider-track-bar::after {
|
||||
display: block;
|
||||
content: "";
|
||||
@ -322,7 +329,11 @@ export class HaBarSlider extends LitElement {
|
||||
.slider .slider-track-bar {
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate3d(calc((var(--value, 0) - 1) * 100%), 0, 0);
|
||||
transform: translate3d(
|
||||
calc((var(--value, 0) - 1) * var(--slider-size)),
|
||||
0,
|
||||
0
|
||||
);
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
}
|
||||
.slider .slider-track-bar:after {
|
||||
@ -335,7 +346,11 @@ export class HaBarSlider extends LitElement {
|
||||
.slider .slider-track-bar.end {
|
||||
right: 0;
|
||||
left: initial;
|
||||
transform: translate3d(calc(var(--value, 0) * 100%), 0, 0);
|
||||
transform: translate3d(
|
||||
calc(var(--value, 0) * var(--slider-size)),
|
||||
0,
|
||||
0
|
||||
);
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
}
|
||||
.slider .slider-track-bar.end::after {
|
||||
@ -346,7 +361,11 @@ export class HaBarSlider extends LitElement {
|
||||
.slider .slider-track-bar.vertical {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translate3d(0, calc((1 - var(--value, 0)) * 100%), 0);
|
||||
transform: translate3d(
|
||||
0,
|
||||
calc((1 - var(--value, 0)) * var(--slider-size)),
|
||||
0
|
||||
);
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
.slider .slider-track-bar.vertical:after {
|
||||
@ -360,7 +379,11 @@ export class HaBarSlider extends LitElement {
|
||||
.slider .slider-track-bar.vertical.end {
|
||||
top: 0;
|
||||
bottom: initial;
|
||||
transform: translate3d(0, calc((0 - var(--value, 0)) * 100%), 0);
|
||||
transform: translate3d(
|
||||
0,
|
||||
calc((0 - var(--value, 0)) * var(--slider-size)),
|
||||
0
|
||||
);
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
.slider .slider-track-bar.vertical.end::after {
|
||||
@ -368,7 +391,7 @@ export class HaBarSlider extends LitElement {
|
||||
bottom: var(--handle-margin);
|
||||
}
|
||||
|
||||
.slider .slider-track-indicator:after {
|
||||
.slider .slider-track-cursor:after {
|
||||
display: block;
|
||||
content: "";
|
||||
background-color: rgb(var(--rgb-secondary-text-color));
|
||||
@ -381,8 +404,8 @@ export class HaBarSlider extends LitElement {
|
||||
border-radius: var(--handle-size);
|
||||
}
|
||||
|
||||
.slider .slider-track-indicator {
|
||||
--indicator-size: calc(var(--slider-bar-thickness) / 4);
|
||||
.slider .slider-track-cursor {
|
||||
--cursor-size: calc(var(--slider-bar-thickness) / 4);
|
||||
--handle-size: 4px;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
@ -390,29 +413,29 @@ export class HaBarSlider extends LitElement {
|
||||
transition: left 180ms ease-in-out, bottom 180ms ease-in-out;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: calc(var(--value, 0) * (100% - var(--indicator-size)));
|
||||
width: var(--indicator-size);
|
||||
left: calc(var(--value, 0) * (100% - var(--cursor-size)));
|
||||
width: var(--cursor-size);
|
||||
}
|
||||
.slider .slider-track-indicator:after {
|
||||
.slider .slider-track-cursor:after {
|
||||
height: 50%;
|
||||
width: var(--handle-size);
|
||||
}
|
||||
|
||||
.slider .slider-track-indicator.vertical {
|
||||
.slider .slider-track-cursor.vertical {
|
||||
top: initial;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: calc(var(--value, 0) * (100% - var(--indicator-size)));
|
||||
height: var(--indicator-size);
|
||||
bottom: calc(var(--value, 0) * (100% - var(--cursor-size)));
|
||||
height: var(--cursor-size);
|
||||
width: 100%;
|
||||
}
|
||||
.slider .slider-track-indicator.vertical:after {
|
||||
.slider .slider-track-cursor.vertical:after {
|
||||
height: var(--handle-size);
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
:host([pressed]) .slider-track-bar,
|
||||
:host([pressed]) .slider-track-indicator {
|
||||
:host([pressed]) .slider-track-cursor {
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
|
174
src/components/ha-bar-switch.ts
Normal file
174
src/components/ha-bar-switch.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-bar-switch")
|
||||
export class HaBarSwitch extends LitElement {
|
||||
@property({ type: Boolean, attribute: "disabled" })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public vertical = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public reversed = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public checked?: boolean;
|
||||
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided on icon slot to pass an <ha-icon slot="icon-on"> in)
|
||||
@property({ type: String }) pathOn?: string;
|
||||
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
|
||||
@property({ type: String }) pathOff?: string;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.setAttribute("role", "switch");
|
||||
if (!this.hasAttribute("tabindex")) {
|
||||
this.setAttribute("tabindex", "0");
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("value")) {
|
||||
this.setAttribute("aria-checked", this.checked ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
private _toggle() {
|
||||
if (this.disabled) return;
|
||||
this.checked = !this.checked;
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("keydown", this._keydown);
|
||||
this.addEventListener("click", this._toggle);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", this._keydown);
|
||||
this.removeEventListener("click", this._toggle);
|
||||
}
|
||||
|
||||
private _keydown(ev: any) {
|
||||
if (ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
this._toggle();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="switch">
|
||||
<div class="button" aria-hidden="true">
|
||||
${this.checked
|
||||
? this.pathOn
|
||||
? html`<ha-svg-icon .path=${this.pathOn}></ha-svg-icon>`
|
||||
: html`<slot name="icon-on"></slot>`
|
||||
: this.pathOff
|
||||
? html`<ha-svg-icon .path=${this.pathOff}></ha-svg-icon>`
|
||||
: html`<slot name="icon-off"></slot>`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
--switch-bar-color-on: var(--rgb-primary-color);
|
||||
--switch-bar-color-off: var(--rgb-disabled-color);
|
||||
--switch-bar-thickness: 40px;
|
||||
--switch-bar-border-radius: 12px;
|
||||
--switch-bar-padding: 4px;
|
||||
--mdc-icon-size: 20px;
|
||||
height: var(--switch-bar-thickness);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.switch {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--switch-bar-border-radius);
|
||||
background-color: rgba(var(--switch-bar-color-off), 0.3);
|
||||
padding: var(--switch-bar-padding);
|
||||
transition: background-color 180ms ease-in-out;
|
||||
display: flex;
|
||||
}
|
||||
.switch .button {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: lightgrey;
|
||||
border-radius: calc(
|
||||
var(--switch-bar-border-radius) - var(--switch-bar-padding)
|
||||
);
|
||||
transition: transform 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
background-color: rgb(var(--switch-bar-color-off));
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:host([checked]) .switch {
|
||||
background-color: rgba(var(--switch-bar-color-on), 0.3);
|
||||
}
|
||||
:host([checked]) .switch .button {
|
||||
transform: translateX(100%);
|
||||
background-color: rgb(var(--switch-bar-color-on));
|
||||
}
|
||||
:host([reversed]) .switch {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
:host([reversed][checked]) .switch .button {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--switch-bar-thickness);
|
||||
height: 100%;
|
||||
}
|
||||
:host([vertical][checked]) .switch .button {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
:host([vertical]) .switch .button {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
}
|
||||
:host([vertical][reversed]) .switch {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
:host([vertical][reversed][checked]) .switch .button {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
:host([disabled]) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bar-switch": HaBarSwitch;
|
||||
}
|
||||
}
|
@ -9,14 +9,7 @@ import type {
|
||||
ComboBoxLightValueChangedEvent,
|
||||
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@ -113,6 +106,8 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
private _overlayMutationObserver?: MutationObserver;
|
||||
|
||||
private _bodyMutationObserver?: MutationObserver;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
this._comboBox?.open();
|
||||
@ -130,6 +125,10 @@ export class HaComboBox extends LitElement {
|
||||
this._overlayMutationObserver.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
}
|
||||
if (this._bodyMutationObserver) {
|
||||
this._bodyMutationObserver.disconnect();
|
||||
this._bodyMutationObserver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public get selectedItem() {
|
||||
@ -227,7 +226,7 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||
const opened = ev.detail.value;
|
||||
// delay this so we can handle click event before setting _opened
|
||||
// delay this so we can handle click event for toggle button before setting _opened
|
||||
setTimeout(() => {
|
||||
this.opened = opened;
|
||||
}, 0);
|
||||
@ -235,37 +234,61 @@ export class HaComboBox extends LitElement {
|
||||
fireEvent(this, ev.type, ev.detail);
|
||||
|
||||
if (opened) {
|
||||
this.removeInertOnOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
private removeInertOnOverlay() {
|
||||
if ("MutationObserver" in window && !this._overlayMutationObserver) {
|
||||
const overlay = document.querySelector<HTMLElement>(
|
||||
"vaadin-combo-box-overlay"
|
||||
);
|
||||
|
||||
if (!overlay) {
|
||||
return;
|
||||
if (overlay) {
|
||||
this._removeInert(overlay);
|
||||
}
|
||||
this._observeBody();
|
||||
} else {
|
||||
this._bodyMutationObserver?.disconnect();
|
||||
this._bodyMutationObserver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _observeBody() {
|
||||
if ("MutationObserver" in window && !this._bodyMutationObserver) {
|
||||
this._bodyMutationObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
||||
this._removeInert(node as HTMLElement);
|
||||
}
|
||||
});
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this._bodyMutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _removeInert(overlay: HTMLElement) {
|
||||
if (overlay.inert) {
|
||||
overlay.inert = false;
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
return;
|
||||
}
|
||||
if ("MutationObserver" in window && !this._overlayMutationObserver) {
|
||||
this._overlayMutationObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (
|
||||
mutation.type === "attributes" &&
|
||||
mutation.attributeName === "inert"
|
||||
) {
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
// @ts-expect-error
|
||||
overlay.inert = false;
|
||||
} else if (mutation.type === "childList") {
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
}
|
||||
});
|
||||
if (mutation.attributeName === "inert") {
|
||||
const target = mutation.target as HTMLElement;
|
||||
if (target.inert) {
|
||||
this._overlayMutationObserver?.disconnect();
|
||||
this._overlayMutationObserver = undefined;
|
||||
target.inert = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -273,19 +296,6 @@ export class HaComboBox extends LitElement {
|
||||
this._overlayMutationObserver.observe(overlay, {
|
||||
attributes: true,
|
||||
});
|
||||
this._overlayMutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
changedProps.has("filteredItems") ||
|
||||
(changedProps.has("items") && this.opened)
|
||||
) {
|
||||
this.removeInertOnOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,14 +5,12 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import {
|
||||
canClose,
|
||||
canOpen,
|
||||
canStop,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
isClosing,
|
||||
isFullyClosed,
|
||||
isFullyOpen,
|
||||
isOpening,
|
||||
} from "../data/cover";
|
||||
import { UNAVAILABLE } from "../data/entity";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@ -37,7 +35,7 @@ class HaCoverControls extends LitElement {
|
||||
"ui.dialogs.more_info_control.cover.open_cover"
|
||||
)}
|
||||
@click=${this._onOpenTap}
|
||||
.disabled=${this._computeOpenDisabled()}
|
||||
.disabled=${!canOpen(this.stateObj)}
|
||||
.path=${computeOpenIcon(this.stateObj)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
@ -50,7 +48,7 @@ class HaCoverControls extends LitElement {
|
||||
)}
|
||||
.path=${mdiStop}
|
||||
@click=${this._onStopTap}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.disabled=${!canStop(this.stateObj)}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
@ -60,7 +58,7 @@ class HaCoverControls extends LitElement {
|
||||
"ui.dialogs.more_info_control.cover.close_cover"
|
||||
)}
|
||||
@click=${this._onCloseTap}
|
||||
.disabled=${this._computeClosedDisabled()}
|
||||
.disabled=${!canClose(this.stateObj)}
|
||||
.path=${computeCloseIcon(this.stateObj)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
@ -68,27 +66,6 @@ class HaCoverControls extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeOpenDisabled(): boolean {
|
||||
if (this.stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
(isFullyOpen(this.stateObj) || isOpening(this.stateObj)) && !assumedState
|
||||
);
|
||||
}
|
||||
|
||||
private _computeClosedDisabled(): boolean {
|
||||
if (this.stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
(isFullyClosed(this.stateObj) || isClosing(this.stateObj)) &&
|
||||
!assumedState
|
||||
);
|
||||
}
|
||||
|
||||
private _onOpenTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this.hass.callService("cover", "open_cover", {
|
||||
|
@ -4,12 +4,12 @@ import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import {
|
||||
canCloseTilt,
|
||||
canOpenTilt,
|
||||
canStopTilt,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
isFullyClosedTilt,
|
||||
isFullyOpenTilt,
|
||||
} from "../data/cover";
|
||||
import { UNAVAILABLE } from "../data/entity";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@ -36,7 +36,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
)}
|
||||
.path=${mdiArrowTopRight}
|
||||
@click=${this._onOpenTiltTap}
|
||||
.disabled=${this._computeOpenDisabled()}
|
||||
.disabled=${!canOpenTilt(this.stateObj)}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
@ -50,7 +50,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
)}
|
||||
.path=${mdiStop}
|
||||
@click=${this._onStopTiltTap}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.disabled=${!canStopTilt(this.stateObj)}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
@ -64,26 +64,10 @@ class HaCoverTiltControls extends LitElement {
|
||||
)}
|
||||
.path=${mdiArrowBottomLeft}
|
||||
@click=${this._onCloseTiltTap}
|
||||
.disabled=${this._computeClosedDisabled()}
|
||||
.disabled=${!canCloseTilt(this.stateObj)}
|
||||
></ha-icon-button>`;
|
||||
}
|
||||
|
||||
private _computeOpenDisabled(): boolean {
|
||||
if (this.stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return isFullyOpenTilt(this.stateObj) && !assumedState;
|
||||
}
|
||||
|
||||
private _computeClosedDisabled(): boolean {
|
||||
if (this.stateObj.state === UNAVAILABLE) {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return isFullyClosedTilt(this.stateObj) && !assumedState;
|
||||
}
|
||||
|
||||
private _onOpenTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this.hass.callService("cover", "open_cover_tilt", {
|
||||
|
@ -35,6 +35,10 @@ export class HaDateInput extends LitElement {
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public min?: string;
|
||||
|
||||
@property() public max?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
@ -65,7 +69,8 @@ export class HaDateInput extends LitElement {
|
||||
return;
|
||||
}
|
||||
showDatePickerDialog(this, {
|
||||
min: "1970-01-01",
|
||||
min: this.min || "1970-01-01",
|
||||
max: this.max,
|
||||
value: this.value,
|
||||
onChange: (value) => this._valueChanged(value),
|
||||
locale: this.locale.language,
|
||||
@ -86,6 +91,9 @@ export class HaDateInput extends LitElement {
|
||||
ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { formatDateTime } from "../common/datetime/format_date_time";
|
||||
import { formatDate } from "../common/datetime/format_date";
|
||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
@ -35,6 +36,10 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
@property() public ranges?: DateRangePickerRanges;
|
||||
|
||||
@property() public autoApply = false;
|
||||
|
||||
@property() public timePicker = true;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) private _hour24format = false;
|
||||
@ -55,6 +60,8 @@ export class HaDateRangePicker extends LitElement {
|
||||
return html`
|
||||
<date-range-picker
|
||||
?disabled=${this.disabled}
|
||||
?auto-apply=${this.autoApply}
|
||||
?time-picker=${this.timePicker}
|
||||
twentyfour-hours=${this._hour24format}
|
||||
start-date=${this.startDate}
|
||||
end-date=${this.endDate}
|
||||
@ -64,7 +71,9 @@ export class HaDateRangePicker extends LitElement {
|
||||
<div slot="input" class="date-range-inputs">
|
||||
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
|
||||
<ha-textfield
|
||||
.value=${formatDateTime(this.startDate, this.hass.locale)}
|
||||
.value=${this.timePicker
|
||||
? formatDateTime(this.startDate, this.hass.locale)
|
||||
: formatDate(this.startDate, this.hass.locale)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.start_date"
|
||||
)}
|
||||
@ -73,7 +82,9 @@ export class HaDateRangePicker extends LitElement {
|
||||
readonly
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
.value=${formatDateTime(this.endDate, this.hass.locale)}
|
||||
.value=${this.timePicker
|
||||
? formatDateTime(this.endDate, this.hass.locale)
|
||||
: formatDate(this.endDate, this.hass.locale)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.end_date"
|
||||
)}
|
||||
|
@ -85,7 +85,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
.selector=${item.selector}
|
||||
.value=${getValue(this.data, item)}
|
||||
.label=${this._computeLabel(item, this.data)}
|
||||
.disabled=${this.disabled}
|
||||
.disabled=${this.disabled || item.disabled}
|
||||
.helper=${this._computeHelper(item)}
|
||||
.required=${item.required || false}
|
||||
.context=${this._generateContext(item)}
|
||||
@ -95,7 +95,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
data: getValue(this.data, item),
|
||||
label: this._computeLabel(item, this.data),
|
||||
helper: this._computeHelper(item),
|
||||
disabled: this.disabled,
|
||||
disabled: this.disabled || item.disabled,
|
||||
hass: this.hass,
|
||||
computeLabel: this.computeLabel,
|
||||
computeHelper: this.computeHelper,
|
||||
|
@ -20,6 +20,7 @@ export interface HaFormBaseSchema {
|
||||
// This value is applied if no data is submitted for this field
|
||||
default?: HaFormData;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
description?: {
|
||||
suffix?: string;
|
||||
// This value will be set initially when form is loaded
|
||||
|
@ -89,23 +89,25 @@ export class HaSelectSelector extends LitElement {
|
||||
!this.value || this.value === "" ? [] : (this.value as string[]);
|
||||
|
||||
return html`
|
||||
<ha-chip-set>
|
||||
${value?.map(
|
||||
(item, idx) =>
|
||||
html`
|
||||
<ha-chip hasTrailingIcon>
|
||||
${options.find((option) => option.value === item)?.label ||
|
||||
item}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiClose}
|
||||
.idx=${idx}
|
||||
@click=${this._removeItem}
|
||||
></ha-svg-icon>
|
||||
</ha-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>
|
||||
${value?.length
|
||||
? html`<ha-chip-set>
|
||||
${value.map(
|
||||
(item, idx) =>
|
||||
html`
|
||||
<ha-chip hasTrailingIcon>
|
||||
${options.find((option) => option.value === item)
|
||||
?.label || item}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiClose}
|
||||
.idx=${idx}
|
||||
@click=${this._removeItem}
|
||||
></ha-svg-icon>
|
||||
</ha-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>`
|
||||
: ""}
|
||||
|
||||
<ha-combo-box
|
||||
item-value-path="value"
|
||||
@ -116,7 +118,7 @@ export class HaSelectSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${this._filter}
|
||||
.items=${options.filter(
|
||||
.filteredItems=${options.filter(
|
||||
(option) => !option.disabled && !value?.includes(option.value)
|
||||
)}
|
||||
@filter-changed=${this._filterChanged}
|
||||
|
42
src/components/ha-selector/ha-selector-ui-color.ts
Normal file
42
src/components/ha-selector/ha-selector-ui-color.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { ActionConfig } from "../../data/lovelace";
|
||||
import { UiColorSelector } from "../../data/selector";
|
||||
import "../../panels/lovelace/components/hui-color-picker";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-selector-ui-color")
|
||||
export class HaSelectorUiColor extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: UiColorSelector;
|
||||
|
||||
@property() public value?: ActionConfig;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hui-color-picker
|
||||
.label=${this.label}
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-color-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-ui-color": HaSelectorUiColor;
|
||||
}
|
||||
}
|
@ -10,8 +10,8 @@ const LOAD_ELEMENTS = {
|
||||
area: () => import("./ha-selector-area"),
|
||||
attribute: () => import("./ha-selector-attribute"),
|
||||
boolean: () => import("./ha-selector-boolean"),
|
||||
"color-rgb": () => import("./ha-selector-color-rgb"),
|
||||
"config-entry": () => import("./ha-selector-config-entry"),
|
||||
color_rgb: () => import("./ha-selector-color-rgb"),
|
||||
config_entry: () => import("./ha-selector-config-entry"),
|
||||
date: () => import("./ha-selector-date"),
|
||||
datetime: () => import("./ha-selector-datetime"),
|
||||
device: () => import("./ha-selector-device"),
|
||||
@ -32,8 +32,9 @@ const LOAD_ELEMENTS = {
|
||||
media: () => import("./ha-selector-media"),
|
||||
theme: () => import("./ha-selector-theme"),
|
||||
location: () => import("./ha-selector-location"),
|
||||
"color-temp": () => import("./ha-selector-color-temp"),
|
||||
color_temp: () => import("./ha-selector-color-temp"),
|
||||
"ui-action": () => import("./ha-selector-ui-action"),
|
||||
"ui-color": () => import("./ha-selector-ui-color"),
|
||||
};
|
||||
|
||||
@customElement("ha-selector")
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base";
|
||||
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
|
||||
import { TemplateResult, html, PropertyValues, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
|
||||
@customElement("ha-textfield")
|
||||
export class HaTextField extends TextFieldBase {
|
||||
@ -17,6 +17,8 @@ export class HaTextField extends TextFieldBase {
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
@query("input") public formElement!: HTMLInputElement;
|
||||
|
||||
override updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
|
15
src/components/language-datalist.ts
Normal file
15
src/components/language-datalist.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const createLanguageListEl = (hass: HomeAssistant) => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "languages";
|
||||
for (const [language, metadata] of Object.entries(
|
||||
hass.translationMetadata.translations
|
||||
)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = language;
|
||||
option.innerText = metadata.nativeName;
|
||||
list.appendChild(option);
|
||||
}
|
||||
return list;
|
||||
};
|
@ -133,11 +133,11 @@ export class HaMap extends ReactiveElement {
|
||||
if (
|
||||
!changedProps.has("darkMode") &&
|
||||
(!changedProps.has("hass") ||
|
||||
(oldHass && oldHass.themes.darkMode === this.hass.themes.darkMode))
|
||||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
|
||||
const darkMode = this.darkMode ?? this.hass.themes?.darkMode;
|
||||
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
|
||||
}
|
||||
|
||||
|
128
src/components/tile/ha-tile-button.ts
Normal file
128
src/components/tile/ha-tile-button.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { Ripple } from "@material/mwc-ripple";
|
||||
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
queryAsync,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "../ha-icon";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
@customElement("ha-tile-button")
|
||||
export class HaTileButton extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||
|
||||
@state() private _shouldRenderRipple = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
.title=${this.label}
|
||||
.disabled=${Boolean(this.disabled)}
|
||||
@focus=${this.handleRippleFocus}
|
||||
@blur=${this.handleRippleBlur}
|
||||
@mousedown=${this.handleRippleActivate}
|
||||
@mouseup=${this.handleRippleDeactivate}
|
||||
@mouseenter=${this.handleRippleMouseEnter}
|
||||
@mouseleave=${this.handleRippleMouseLeave}
|
||||
@touchstart=${this.handleRippleActivate}
|
||||
@touchend=${this.handleRippleDeactivate}
|
||||
@touchcancel=${this.handleRippleDeactivate}
|
||||
>
|
||||
<slot></slot>
|
||||
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
|
||||
this._shouldRenderRipple = true;
|
||||
return this._ripple;
|
||||
});
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleRippleActivate(evt?: Event) {
|
||||
this._rippleHandlers.startPress(evt);
|
||||
}
|
||||
|
||||
private handleRippleDeactivate() {
|
||||
this._rippleHandlers.endPress();
|
||||
}
|
||||
|
||||
private handleRippleMouseEnter() {
|
||||
this._rippleHandlers.startHover();
|
||||
}
|
||||
|
||||
private handleRippleMouseLeave() {
|
||||
this._rippleHandlers.endHover();
|
||||
}
|
||||
|
||||
private handleRippleFocus() {
|
||||
this._rippleHandlers.startFocus();
|
||||
}
|
||||
|
||||
private handleRippleBlur() {
|
||||
this._rippleHandlers.endFocus();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
--icon-color: rgb(var(--color, var(--rgb-primary-text-color)));
|
||||
--bg-color: rgba(var(--color, var(--rgb-disabled-color)), 0.2);
|
||||
--mdc-ripple-color: rgba(var(--color, var(--rgb-disabled-color)));
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.button {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background-color: var(--bg-color);
|
||||
transition: background-color 280ms ease-in-out, transform 180ms ease-out;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
line-height: 0;
|
||||
outline: none;
|
||||
}
|
||||
.button ::slotted(*) {
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--icon-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
.button:disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: rgba(var(--rgb-disabled-color), 0.2);
|
||||
}
|
||||
.button:disabled ::slotted(*) {
|
||||
color: rgb(var(--rgb-disabled-color));
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-tile-button": HaTileButton;
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import { customElement, property } from "lit/decorators";
|
||||
export class HaTileInfo extends LitElement {
|
||||
@property() public primary?: string;
|
||||
|
||||
@property() public secondary?: string;
|
||||
@property() public secondary?: string | TemplateResult<1>;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
|
69
src/components/tile/ha-tile-slider.ts
Normal file
69
src/components/tile/ha-tile-slider.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "../ha-bar-slider";
|
||||
|
||||
@customElement("ha-tile-slider")
|
||||
export class HaTileSlider extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
@property()
|
||||
public mode?: "start" | "end" | "cursor" = "start";
|
||||
|
||||
@property({ type: Boolean, attribute: "show-handle" })
|
||||
public showHandle = false;
|
||||
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public step = 1;
|
||||
|
||||
@property({ type: Number })
|
||||
public min = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
public max = 100;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-bar-slider
|
||||
.disabled=${this.disabled}
|
||||
.mode=${this.mode}
|
||||
.value=${this.value}
|
||||
.step=${this.step}
|
||||
.min=${this.min}
|
||||
.max=${this.max}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
.showHandle=${this.showHandle}
|
||||
>
|
||||
</ha-bar-slider>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-bar-slider {
|
||||
--slider-bar-color: var(
|
||||
--tile-slider-bar-color,
|
||||
rgb(var(--rgb-primary-color))
|
||||
);
|
||||
--slider-bar-background: var(
|
||||
--tile-slider-bar-background,
|
||||
rgba(var(--rgb-disabled-color), 0.2)
|
||||
);
|
||||
--slider-bar-thickness: 40px;
|
||||
--slider-bar-border-radius: 10px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-tile-slider": HaTileSlider;
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ export const createTimezoneListEl = () => {
|
||||
Object.keys(timezones).forEach((key) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = key;
|
||||
option.innerHTML = timezones[key];
|
||||
option.innerText = timezones[key];
|
||||
list.appendChild(option);
|
||||
});
|
||||
return list;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { getColorByIndex } from "../common/color/colors";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import type { CalendarEvent, HomeAssistant } from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface Calendar {
|
||||
entity_id: string;
|
||||
@ -9,6 +9,46 @@ export interface Calendar {
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
/** Object used to render a calendar event in fullcalendar. */
|
||||
export interface CalendarEvent {
|
||||
title: string;
|
||||
start: string;
|
||||
end?: string;
|
||||
backgroundColor?: string;
|
||||
borderColor?: string;
|
||||
calendar: string;
|
||||
eventData: CalendarEventData;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** Data returned from the core APIs. */
|
||||
export interface CalendarEventData {
|
||||
uid?: string;
|
||||
recurrence_id?: string;
|
||||
summary: string;
|
||||
dtstart: string;
|
||||
dtend: string;
|
||||
rrule?: string;
|
||||
}
|
||||
|
||||
export interface CalendarEventMutableParams {
|
||||
summary: string;
|
||||
dtstart: string;
|
||||
dtend: string;
|
||||
rrule?: string;
|
||||
}
|
||||
|
||||
// The scope of a delete/update for a recurring event
|
||||
export enum RecurrenceRange {
|
||||
THISEVENT = "",
|
||||
THISANDFUTURE = "THISANDFUTURE",
|
||||
}
|
||||
|
||||
export const enum CalendarEntityFeature {
|
||||
CREATE_EVENT = 1,
|
||||
DELETE_EVENT = 2,
|
||||
}
|
||||
|
||||
export const fetchCalendarEvents = async (
|
||||
hass: HomeAssistant,
|
||||
start: Date,
|
||||
@ -37,18 +77,26 @@ export const fetchCalendarEvents = async (
|
||||
const cal = calendars[idx];
|
||||
result.forEach((ev) => {
|
||||
const eventStart = getCalendarDate(ev.start);
|
||||
if (!eventStart) {
|
||||
const eventEnd = getCalendarDate(ev.end);
|
||||
if (!eventStart || !eventEnd) {
|
||||
return;
|
||||
}
|
||||
const eventEnd = getCalendarDate(ev.end);
|
||||
const eventData: CalendarEventData = {
|
||||
uid: ev.uid,
|
||||
summary: ev.summary,
|
||||
dtstart: eventStart,
|
||||
dtend: eventEnd,
|
||||
recurrence_id: ev.recurrence_id,
|
||||
rrule: ev.rrule,
|
||||
};
|
||||
const event: CalendarEvent = {
|
||||
start: eventStart,
|
||||
end: eventEnd,
|
||||
title: ev.summary,
|
||||
summary: ev.summary,
|
||||
backgroundColor: cal.backgroundColor,
|
||||
borderColor: cal.backgroundColor,
|
||||
calendar: cal.entity_id,
|
||||
eventData: eventData,
|
||||
};
|
||||
|
||||
calEvents.push(event);
|
||||
@ -83,3 +131,40 @@ export const getCalendars = (hass: HomeAssistant): Calendar[] =>
|
||||
name: computeStateName(hass.states[eid]),
|
||||
backgroundColor: getColorByIndex(idx),
|
||||
}));
|
||||
|
||||
export const createCalendarEvent = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
event: CalendarEventMutableParams
|
||||
) =>
|
||||
hass.callWS<void>({
|
||||
type: "calendar/event/create",
|
||||
entity_id: entityId,
|
||||
event: event,
|
||||
});
|
||||
|
||||
export const updateCalendarEvent = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
event: CalendarEventMutableParams
|
||||
) =>
|
||||
hass.callWS<void>({
|
||||
type: "calendar/event/update",
|
||||
entity_id: entityId,
|
||||
event: event,
|
||||
});
|
||||
|
||||
export const deleteCalendarEvent = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
uid: string,
|
||||
recurrence_id?: string,
|
||||
recurrence_range?: RecurrenceRange
|
||||
) =>
|
||||
hass.callWS<void>({
|
||||
type: "calendar/event/delete",
|
||||
entity_id: entityId,
|
||||
uid,
|
||||
recurrence_id,
|
||||
recurrence_range,
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ export const DISCOVERY_SOURCES = [
|
||||
"bluetooth",
|
||||
"dhcp",
|
||||
"discovery",
|
||||
"hardware",
|
||||
"hassio",
|
||||
"homekit",
|
||||
"integration_discovery",
|
||||
|
@ -11,6 +11,8 @@ export interface ConfigUpdateValues {
|
||||
external_url?: string | null;
|
||||
internal_url?: string | null;
|
||||
currency?: string | null;
|
||||
country?: string | null;
|
||||
language?: string | null;
|
||||
}
|
||||
|
||||
export interface CheckConfigResult {
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { UNAVAILABLE } from "./entity";
|
||||
|
||||
export const enum CoverEntityFeature {
|
||||
OPEN = 1,
|
||||
@ -57,6 +58,46 @@ export function isTiltOnly(stateObj: CoverEntity) {
|
||||
return supportsTilt && !supportsCover;
|
||||
}
|
||||
|
||||
export function canOpen(stateObj: CoverEntity) {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (!isFullyOpen(stateObj) && !isOpening(stateObj)) || assumedState;
|
||||
}
|
||||
|
||||
export function canClose(stateObj: CoverEntity): boolean {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (!isFullyClosed(stateObj) && !isClosing(stateObj)) || assumedState;
|
||||
}
|
||||
|
||||
export function canStop(stateObj: CoverEntity): boolean {
|
||||
return stateObj.state !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
export function canOpenTilt(stateObj: CoverEntity): boolean {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return !isFullyOpenTilt(stateObj) || assumedState;
|
||||
}
|
||||
|
||||
export function canCloseTilt(stateObj: CoverEntity): boolean {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return !isFullyClosedTilt(stateObj) || assumedState;
|
||||
}
|
||||
|
||||
export function canStopTilt(stateObj: CoverEntity): boolean {
|
||||
return stateObj.state !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
interface CoverEntityAttributes extends HassEntityAttributeBase {
|
||||
current_position?: number;
|
||||
current_tilt_position?: number;
|
||||
|
@ -410,7 +410,8 @@ const getEnergyData = async (
|
||||
end,
|
||||
energyStatIds,
|
||||
period,
|
||||
energyUnits
|
||||
energyUnits,
|
||||
["sum"]
|
||||
)),
|
||||
...(await fetchStatistics(
|
||||
hass!,
|
||||
@ -418,7 +419,8 @@ const getEnergyData = async (
|
||||
end,
|
||||
waterStatIds,
|
||||
period,
|
||||
waterUnits
|
||||
waterUnits,
|
||||
["sum"]
|
||||
)),
|
||||
};
|
||||
|
||||
@ -443,15 +445,17 @@ const getEnergyData = async (
|
||||
endCompare,
|
||||
energyStatIds,
|
||||
period,
|
||||
energyUnits
|
||||
energyUnits,
|
||||
["sum"]
|
||||
)),
|
||||
...(await fetchStatistics(
|
||||
hass!,
|
||||
startMinHour,
|
||||
compareStartMinHour,
|
||||
end,
|
||||
waterStatIds,
|
||||
period,
|
||||
waterUnits
|
||||
waterUnits,
|
||||
["sum"]
|
||||
)),
|
||||
};
|
||||
}
|
||||
@ -485,8 +489,8 @@ const getEnergyData = async (
|
||||
if (stat.length && new Date(stat[0].start) > startMinHour) {
|
||||
stat.unshift({
|
||||
...stat[0],
|
||||
start: startMinHour.toISOString(),
|
||||
end: startMinHour.toISOString(),
|
||||
start: startMinHour.getTime(),
|
||||
end: startMinHour.getTime(),
|
||||
sum: 0,
|
||||
state: 0,
|
||||
});
|
||||
|
@ -7,3 +7,5 @@ export interface LogProvider {
|
||||
|
||||
export const fetchErrorLog = (hass: HomeAssistant) =>
|
||||
hass.callApi<string>("GET", "error_log");
|
||||
|
||||
export const getErrorLogDownloadUrl = "/api/error_log";
|
||||
|
@ -1,4 +1,24 @@
|
||||
export const SUPPORT_SET_SPEED = 1;
|
||||
export const SUPPORT_OSCILLATE = 2;
|
||||
export const SUPPORT_DIRECTION = 4;
|
||||
export const SUPPORT_PRESET_MODE = 8;
|
||||
import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export const enum FanEntityFeature {
|
||||
SET_SPEED = 1,
|
||||
OSCILLATE = 2,
|
||||
DIRECTION = 4,
|
||||
PRESET_MODE = 8,
|
||||
}
|
||||
|
||||
interface FanEntityAttributes extends HassEntityAttributeBase {
|
||||
direction?: number;
|
||||
oscillating?: boolean;
|
||||
percentage?: number;
|
||||
percentage_step?: number;
|
||||
preset_mode?: string;
|
||||
preset_modes?: string[];
|
||||
}
|
||||
|
||||
export interface FanEntity extends HassEntityBase {
|
||||
attributes: FanEntityAttributes;
|
||||
}
|
||||
|
@ -26,7 +26,9 @@ export interface HardwareInfo {
|
||||
}
|
||||
|
||||
export interface HardwareInfoEntry {
|
||||
board: HardwareInfoBoardInfo;
|
||||
board: HardwareInfoBoardInfo | null;
|
||||
dongle: HardwareInfoDongleInfo | null;
|
||||
config_entries: string[];
|
||||
name: string;
|
||||
url?: string;
|
||||
}
|
||||
@ -38,6 +40,14 @@ export interface HardwareInfoBoardInfo {
|
||||
hassio_board_id?: string;
|
||||
}
|
||||
|
||||
export interface HardwareInfoDongleInfo {
|
||||
manufacturer: string;
|
||||
description: string;
|
||||
pid?: string;
|
||||
vid?: string;
|
||||
serial_number?: string;
|
||||
}
|
||||
|
||||
export interface SystemStatusStreamMessage {
|
||||
cpu_percent: number;
|
||||
memory_free_mb: number;
|
||||
|
@ -183,6 +183,11 @@ export const fetchHassioLogs = async (hass: HomeAssistant, provider: string) =>
|
||||
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs`
|
||||
);
|
||||
|
||||
export const getHassioLogDownloadUrl = (provider: string) =>
|
||||
`/api/hassio/${
|
||||
provider.includes("_") ? `addons/${provider}` : provider
|
||||
}/logs`;
|
||||
|
||||
export const setSupervisorOption = async (
|
||||
hass: HomeAssistant,
|
||||
data: SupervisorOptions
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export type IntegrationType = "device" | "helper" | "hub" | "service";
|
||||
export type IntegrationType =
|
||||
| "device"
|
||||
| "helper"
|
||||
| "hub"
|
||||
| "service"
|
||||
| "hardware";
|
||||
|
||||
export interface IntegrationManifest {
|
||||
is_built_in: boolean;
|
||||
|
@ -10,7 +10,7 @@ import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE_STATES } from "./entity";
|
||||
import { UNAVAILABLE, UNKNOWN } from "./entity";
|
||||
|
||||
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
||||
export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"];
|
||||
@ -61,7 +61,9 @@ const triggerPhrases = {
|
||||
};
|
||||
|
||||
const DATA_CACHE: {
|
||||
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
|
||||
[cacheKey: string]: {
|
||||
[entityId: string]: Promise<LogbookEntry[]> | undefined;
|
||||
};
|
||||
} = {};
|
||||
|
||||
export const getLogbookDataForContext = async (
|
||||
@ -115,11 +117,11 @@ const getLogbookDataCache = async (
|
||||
}
|
||||
|
||||
if (entityIdKey in DATA_CACHE[cacheKey]) {
|
||||
return DATA_CACHE[cacheKey][entityIdKey];
|
||||
return DATA_CACHE[cacheKey][entityIdKey]!;
|
||||
}
|
||||
|
||||
if (entityId && DATA_CACHE[cacheKey][ALL_ENTITIES]) {
|
||||
const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES];
|
||||
const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES]!;
|
||||
return entities.filter(
|
||||
(entity) => entity.entity_id && entityId.includes(entity.entity_id)
|
||||
);
|
||||
@ -131,7 +133,7 @@ const getLogbookDataCache = async (
|
||||
endDate,
|
||||
entityId
|
||||
);
|
||||
return DATA_CACHE[cacheKey][entityIdKey];
|
||||
return DATA_CACHE[cacheKey][entityIdKey]!;
|
||||
};
|
||||
|
||||
const getLogbookDataFromServer = (
|
||||
@ -398,11 +400,17 @@ export const localizeStateMessage = (
|
||||
break;
|
||||
|
||||
case "lock":
|
||||
if (state === "unlocked") {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
|
||||
}
|
||||
if (state === "locked") {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
|
||||
switch (state) {
|
||||
case "unlocked":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
|
||||
case "locking":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_locking`);
|
||||
case "unlocking":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_unlocking`);
|
||||
case "locked":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
|
||||
case "jammed":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_jammed`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -415,7 +423,11 @@ export const localizeStateMessage = (
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.turned_off`);
|
||||
}
|
||||
|
||||
if (UNAVAILABLE_STATES.includes(state)) {
|
||||
if (state === UNKNOWN) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.became_unknown`);
|
||||
}
|
||||
|
||||
if (state === UNAVAILABLE) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.became_unavailable`);
|
||||
}
|
||||
|
||||
|
39
src/data/matter.ts
Normal file
39
src/data/matter.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const commissionMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
code: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "matter/commission",
|
||||
code,
|
||||
});
|
||||
|
||||
export const acceptSharedMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
pin: number
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "matter/commission_on_network",
|
||||
pin,
|
||||
});
|
||||
|
||||
export const matterSetWifi = (
|
||||
hass: HomeAssistant,
|
||||
network_name: string,
|
||||
password: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "matter/set_wifi_credentials",
|
||||
network_name,
|
||||
password,
|
||||
});
|
||||
|
||||
export const matterSetThread = (
|
||||
hass: HomeAssistant,
|
||||
thread_operation_dataset: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "matter/set_thread",
|
||||
thread_operation_dataset,
|
||||
});
|
@ -9,15 +9,14 @@ export interface Statistics {
|
||||
}
|
||||
|
||||
export interface StatisticValue {
|
||||
statistic_id: string;
|
||||
start: string;
|
||||
end: string;
|
||||
last_reset: string | null;
|
||||
max: number | null;
|
||||
mean: number | null;
|
||||
min: number | null;
|
||||
sum: number | null;
|
||||
state: number | null;
|
||||
start: number;
|
||||
end: number;
|
||||
last_reset?: number | null;
|
||||
max?: number | null;
|
||||
mean?: number | null;
|
||||
min?: number | null;
|
||||
sum?: number | null;
|
||||
state?: number | null;
|
||||
}
|
||||
|
||||
export interface Statistic {
|
||||
@ -91,6 +90,16 @@ export interface StatisticsUnitConfiguration {
|
||||
volume?: "L" | "gal" | "ft³" | "m³";
|
||||
}
|
||||
|
||||
const statisticTypes = [
|
||||
"last_reset",
|
||||
"max",
|
||||
"mean",
|
||||
"min",
|
||||
"state",
|
||||
"sum",
|
||||
] as const;
|
||||
export type StatisticsTypes = typeof statisticTypes[number][];
|
||||
|
||||
export interface StatisticsValidationResults {
|
||||
[statisticId: string]: StatisticsValidationResult[];
|
||||
}
|
||||
@ -119,7 +128,8 @@ export const fetchStatistics = (
|
||||
endTime?: Date,
|
||||
statistic_ids?: string[],
|
||||
period: "5minute" | "hour" | "day" | "week" | "month" = "hour",
|
||||
units?: StatisticsUnitConfiguration
|
||||
units?: StatisticsUnitConfiguration,
|
||||
types?: StatisticsTypes
|
||||
) =>
|
||||
hass.callWS<Statistics>({
|
||||
type: "recorder/statistics_during_period",
|
||||
@ -128,6 +138,7 @@ export const fetchStatistics = (
|
||||
statistic_ids,
|
||||
period,
|
||||
units,
|
||||
types,
|
||||
});
|
||||
|
||||
export const fetchStatistic = (
|
||||
@ -189,11 +200,11 @@ export const calculateStatisticSumGrowth = (
|
||||
return null;
|
||||
}
|
||||
const endSum = values[values.length - 1].sum;
|
||||
if (endSum === null) {
|
||||
if (endSum === null || endSum === undefined) {
|
||||
return null;
|
||||
}
|
||||
const startSum = values[0].sum;
|
||||
if (startSum === null) {
|
||||
if (startSum === null || startSum === undefined) {
|
||||
return endSum;
|
||||
}
|
||||
return endSum - startSum;
|
||||
@ -248,17 +259,19 @@ export const statisticsMetaHasType = (
|
||||
export const adjustStatisticsSum = (
|
||||
hass: HomeAssistant,
|
||||
statistic_id: string,
|
||||
start_time: string,
|
||||
start_time: number,
|
||||
adjustment: number,
|
||||
adjustment_unit_of_measurement: string | null
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
): Promise<void> => {
|
||||
const start_time_iso = new Date(start_time).toISOString();
|
||||
return hass.callWS({
|
||||
type: "recorder/adjust_sum_statistics",
|
||||
statistic_id,
|
||||
start_time,
|
||||
start_time_iso,
|
||||
adjustment,
|
||||
adjustment_unit_of_measurement,
|
||||
});
|
||||
};
|
||||
|
||||
export const getStatisticLabel = (
|
||||
hass: HomeAssistant,
|
||||
|
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