Compare commits

..

1 Commits

Author SHA1 Message Date
Zack
be6fef1824 Align entity registry buttons 2022-08-31 10:30:38 -05:00
702 changed files with 209009 additions and 26828 deletions

View File

@@ -13,7 +13,6 @@ on:
env: env:
NODE_VERSION: 16 NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144 NODE_OPTIONS: --max_old_space_size=6144
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs: jobs:
lint: lint:

View File

@@ -26,10 +26,10 @@ jobs:
CI: true CI: true
- name: Build Demo - name: Build Demo
run: ./node_modules/.bin/gulp build-demo run: ./node_modules/.bin/gulp build-demo
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify - name: Deploy to Netlify
run: npx netlify-cli deploy --dir=demo/dist --prod uses: netlify/actions/cli@master
env: env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
with:
args: deploy --dir=demo/dist --prod

View File

@@ -49,8 +49,9 @@ jobs:
run: | run: |
pip install build pip install build
yarn install yarn install
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/build_frontend script/build_frontend
rm -rf dist home_assistant_frontend.egg-info rm -rf dist home_assistant_frontend.egg-info
python3 -m build python3 -m build

View File

@@ -52,11 +52,11 @@ jobs:
python3 -m pip install twine build python3 -m pip install twine build
export TWINE_USERNAME="__token__" export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}" export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release script/release
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@v0.1.15 uses: softprops/action-gh-release@v0.1.14
with: with:
files: | files: |
dist/*.whl dist/*.whl
@@ -75,7 +75,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2022.10.1 uses: home-assistant/wheels@2022.06.7
with: with:
abi: cp310 abi: cp310
tag: musllinux_1_2 tag: musllinux_1_2

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 90 days stale policy - name: 90 days stale policy
uses: actions/stale@v6.0.1 uses: actions/stale@v5.1.1
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90 days-before-stale: 90

1
.gitignore vendored
View File

@@ -5,7 +5,6 @@
build build
hass_frontend/* hass_frontend/*
dist dist
translations
# yarn # yarn
.yarn/* .yarn/*

8
.vscode/tasks.json vendored
View File

@@ -191,13 +191,7 @@
"runOptions": { "runOptions": {
"instanceLimit": 1 "instanceLimit": 1
} }
}, }
{
"label": "Setup and fetch nightly translations",
"type": "gulp",
"task": "setup-and-fetch-nightly-translations",
"problemMatcher": []
}
], ],
"inputs": [ "inputs": [
{ {

785
.yarn/releases/yarn-3.2.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools" spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.2.3.cjs yarnPath: .yarn/releases/yarn-3.2.0.cjs

7
build-scripts/.eslintrc Normal file
View File

@@ -0,0 +1,7 @@
{
"rules": {
"import/no-extraneous-dependencies": 0,
"no-restricted-syntax": 0,
"no-console": 0
}
}

View File

@@ -1,12 +1,7 @@
{ {
"extends": "../.eslintrc.json", "extends": "../.eslintrc.json",
"rules": { "rules": {
"no-console": "off", "import/no-extraneous-dependencies": 0,
"import/no-extraneous-dependencies": "off", "global-require": 0
"import/extensions": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"@typescript-eslint/no-var-requires": "off",
"prefer-arrow-callback": "off"
} }
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path"); const path = require("path");
// Currently only supports CommonJS modules, as require is synchronous. `import` would need babel running asynchronous. // Currently only supports CommonJS modules, as require is synchronous. `import` would need babel running asynchronous.
@@ -28,6 +29,7 @@ module.exports = function inlineConstants(babel, options, cwd) {
const absolute = module.startsWith(".") const absolute = module.startsWith(".")
? require.resolve(module, { paths: [cwd] }) ? require.resolve(module, { paths: [cwd] })
: module; : module;
// eslint-disable-next-line import/no-dynamic-require
return [absolute, require(absolute)]; return [absolute, require(absolute)];
}) })
); );

View File

@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path"); const path = require("path");
const env = require("./env.js"); const env = require("./env.js");
const paths = require("./paths.js"); const paths = require("./paths.js");
// Files from NPM Packages that should not be imported // Files from NPM Packages that should not be imported
// eslint-disable-next-line unused-imports/no-unused-vars
module.exports.ignorePackages = ({ latestBuild }) => [ module.exports.ignorePackages = ({ latestBuild }) => [
// Part of yaml.js and only used for !!js functions that we don't use // Part of yaml.js and only used for !!js functions that we don't use
require.resolve("esprima"), require.resolve("esprima"),

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const paths = require("./paths.js"); const paths = require("./paths.js");

View File

@@ -1,12 +1,17 @@
const del = require("del");
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs/promises"); const fs = require("fs");
const mapStream = require("map-stream"); const mapStream = require("map-stream");
const inDirFrontend = "translations/frontend"; const inDirFrontend = "translations/frontend";
const inDirBackend = "translations/backend"; const inDirBackend = "translations/backend";
const downloadDir = "translations/downloads";
const srcMeta = "src/translations/translationMetadata.json"; const srcMeta = "src/translations/translationMetadata.json";
const encoding = "utf8"; const encoding = "utf8";
const tasks = [];
function hasHtml(data) { function hasHtml(data) {
return /<[a-z][\s\S]*>/i.test(data); return /<[a-z][\s\S]*>/i.test(data);
} }
@@ -41,29 +46,50 @@ function checkHtml() {
}); });
} }
// Backend translations do not currently pass HTML check so are excluded here for now let taskName = "clean-downloaded-translations";
gulp.task("check-translations-html", function () { gulp.task(taskName, function () {
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml()); return del([`${downloadDir}/**`]);
}); });
tasks.push(taskName);
gulp.task("check-all-files-exist", async function () { taskName = "check-translations-html";
const file = await fs.readFile(srcMeta, { encoding }); gulp.task(taskName, function () {
return gulp.src(`${downloadDir}/*.json`).pipe(checkHtml());
});
tasks.push(taskName);
taskName = "check-all-files-exist";
gulp.task(taskName, function () {
const file = fs.readFileSync(srcMeta, { encoding });
const meta = JSON.parse(file); const meta = JSON.parse(file);
const writings = [];
Object.keys(meta).forEach((lang) => { Object.keys(meta).forEach((lang) => {
writings.push( if (!fs.existsSync(`${inDirFrontend}/${lang}.json`)) {
fs.writeFile(`${inDirFrontend}/${lang}.json`, JSON.stringify({}), { fs.writeFileSync(`${inDirFrontend}/${lang}.json`, JSON.stringify({}));
flag: "wx", }
}), if (!fs.existsSync(`${inDirBackend}/${lang}.json`)) {
fs.writeFile(`${inDirBackend}/${lang}.json`, JSON.stringify({}), { fs.writeFileSync(`${inDirBackend}/${lang}.json`, JSON.stringify({}));
flag: "wx", }
})
);
}); });
await Promise.allSettled(writings); return Promise.resolve();
}); });
tasks.push(taskName);
taskName = "move-downloaded-translations";
gulp.task(taskName, function () {
return gulp.src(`${downloadDir}/*.json`).pipe(gulp.dest(inDirFrontend));
});
tasks.push(taskName);
taskName = "check-downloaded-translations";
gulp.task( gulp.task(
"check-downloaded-translations", taskName,
gulp.series("check-translations-html", "check-all-files-exist") gulp.series(
"check-translations-html",
"move-downloaded-translations",
"check-all-files-exist",
"clean-downloaded-translations"
)
); );
tasks.push(taskName);
module.exports = tasks;

View File

@@ -1,4 +1,6 @@
// Tasks to generate entry HTML // Tasks to generate entry HTML
/* eslint-disable import/no-dynamic-require */
/* eslint-disable global-require */
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs-extra"); const fs = require("fs-extra");
const path = require("path"); const path = require("path");
@@ -89,9 +91,7 @@ gulp.task("gen-pages-prod", (done) => {
}); });
gulp.task("gen-index-app-dev", (done) => { gulp.task("gen-index-app-dev", (done) => {
let latestAppJS; let latestAppJS, latestCoreJS, latestCustomPanelJS;
let latestCoreJS;
let latestCustomPanelJS;
if (env.useWDS()) { if (env.useWDS()) {
latestAppJS = "http://localhost:8000/src/entrypoints/app.ts"; latestAppJS = "http://localhost:8000/src/entrypoints/app.ts";

View File

@@ -1,170 +0,0 @@
// 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"
)
);

View File

@@ -1,3 +1,4 @@
/* eslint-disable */
// Run demo develop mode // Run demo develop mode
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs"); const fs = require("fs");
@@ -40,7 +41,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
} }
processed.add(pageId); processed.add(pageId);
const [category] = pageId.split("/", 2); const [category, name] = pageId.split("/", 2);
const demoFile = path.resolve(pageDir, `${pageId}.ts`); const demoFile = path.resolve(pageDir, `${pageId}.ts`);
const descriptionFile = path.resolve(pageDir, `${pageId}.markdown`); const descriptionFile = path.resolve(pageDir, `${pageId}.markdown`);

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const del = require("del"); const del = require("del");
const path = require("path"); const path = require("path");
const gulp = require("gulp"); const gulp = require("gulp");

View File

@@ -5,9 +5,9 @@ const rollup = require("rollup");
const handler = require("serve-handler"); const handler = require("serve-handler");
const http = require("http"); const http = require("http");
const log = require("fancy-log"); const log = require("fancy-log");
const open = require("open");
const rollupConfig = require("../rollup"); const rollupConfig = require("../rollup");
const paths = require("../paths"); const paths = require("../paths");
const open = require("open");
const bothBuilds = (createConfigFunc, params) => const bothBuilds = (createConfigFunc, params) =>
gulp.series( gulp.series(
@@ -30,11 +30,11 @@ const bothBuilds = (createConfigFunc, params) =>
); );
function createServer(serveOptions) { function createServer(serveOptions) {
const server = http.createServer((request, response) => const server = http.createServer((request, response) => {
handler(request, response, { return handler(request, response, {
public: serveOptions.root, public: serveOptions.root,
}) });
); });
server.listen( server.listen(
serveOptions.port, serveOptions.port,

View File

@@ -1,5 +1,7 @@
// Generate service worker. // Generate service worker.
// Based on manifest, create a file with the content as service_worker.js // 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 gulp = require("gulp");
const path = require("path"); const path = require("path");
const fs = require("fs-extra"); const fs = require("fs-extra");

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const crypto = require("crypto"); const crypto = require("crypto");
const del = require("del"); const del = require("del");
const path = require("path"); const path = require("path");
@@ -13,8 +15,6 @@ const { mapFiles } = require("../util");
const env = require("../env"); const env = require("../env");
const paths = require("../paths"); const paths = require("../paths");
require("./fetch-nightly_translations");
const inFrontendDir = "translations/frontend"; const inFrontendDir = "translations/frontend";
const inBackendDir = "translations/backend"; const inBackendDir = "translations/backend";
const workDir = "build/translations"; const workDir = "build/translations";
@@ -23,13 +23,10 @@ const coreDir = workDir + "/core";
const outDir = workDir + "/output"; const outDir = workDir + "/output";
let mergeBackend = false; let mergeBackend = false;
gulp.task( gulp.task("translations-enable-merge-backend", (done) => {
"translations-enable-merge-backend", mergeBackend = true;
gulp.parallel((done) => { done();
mergeBackend = true; });
done();
}, "allow-setup-fetch-nightly-translations")
);
// Panel translations which should be split from the core translations. // Panel translations which should be split from the core translations.
const TRANSLATION_FRAGMENTS = Object.keys( const TRANSLATION_FRAGMENTS = Object.keys(
@@ -173,24 +170,17 @@ gulp.task("build-master-translation", () => {
.pipe(transform((data, file) => lokaliseTransform(data, data, file))) .pipe(transform((data, file) => lokaliseTransform(data, data, file)))
.pipe( .pipe(
merge({ merge({
fileName: "en.json", fileName: "translationMaster.json",
}) })
) )
.pipe(gulp.dest(fullDir)); .pipe(gulp.dest(workDir));
}); });
gulp.task("build-merged-translations", () => gulp.task("build-merged-translations", () =>
gulp gulp
.src( .src([inFrontendDir + "/*.json", workDir + "/test.json"], {
[ allowEmpty: true,
inFrontendDir + "/*.json", })
"!" + inFrontendDir + "/en.json",
workDir + "/test.json",
],
{
allowEmpty: true,
}
)
.pipe(transform((data, file) => lokaliseTransform(data, data, file))) .pipe(transform((data, file) => lokaliseTransform(data, data, file)))
.pipe( .pipe(
flatmap((stream, file) => { flatmap((stream, file) => {
@@ -203,7 +193,7 @@ gulp.task("build-merged-translations", () =>
// than a base translation + region. // than a base translation + region.
const tr = path.basename(file.history[0], ".json"); const tr = path.basename(file.history[0], ".json");
const subtags = tr.split("-"); const subtags = tr.split("-");
const src = [fullDir + "/en.json"]; const src = [workDir + "/translationMaster.json"];
for (let i = 1; i <= subtags.length; i++) { for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join("-"); const lang = subtags.slice(0, i).join("-");
if (lang === "test") { if (lang === "test") {
@@ -388,6 +378,7 @@ gulp.task("build-translation-write-metadata", () =>
if (value.nativeName) { if (value.nativeName) {
newData[key] = value; newData[key] = value;
} else { } else {
// eslint-disable-next-line no-console
console.warn( console.warn(
`Skipping language ${key}. Native name was not translated.` `Skipping language ${key}. Native name was not translated.`
); );
@@ -420,10 +411,8 @@ gulp.task(
gulp.task( gulp.task(
"build-translations", "build-translations",
gulp.series( gulp.series(
gulp.parallel( "clean-translations",
"fetch-nightly-translations", "ensure-translations-build-dir",
gulp.series("clean-translations", "ensure-translations-build-dir")
),
"create-translations", "create-translations",
"build-translation-fingerprints", "build-translation-fingerprints",
"build-translation-write-metadata" "build-translation-write-metadata"
@@ -433,10 +422,8 @@ gulp.task(
gulp.task( gulp.task(
"build-supervisor-translations", "build-supervisor-translations",
gulp.series( gulp.series(
gulp.parallel( "clean-translations",
"fetch-nightly-translations", "ensure-translations-build-dir",
gulp.series("clean-translations", "ensure-translations-build-dir")
),
"build-master-translation", "build-master-translation",
"build-merged-translations", "build-merged-translations",
"build-translation-fragment-supervisor", "build-translation-fragment-supervisor",

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// Tasks to run webpack. // Tasks to run webpack.
const fs = require("fs"); const fs = require("fs");
const gulp = require("gulp"); const gulp = require("gulp");
@@ -68,6 +69,7 @@ const doneHandler = (done) => (err, stats) => {
} }
if (stats.hasErrors() || stats.hasWarnings()) { if (stats.hasErrors() || stats.hasWarnings()) {
// eslint-disable-next-line no-console
console.log(stats.toString("minimal")); console.log(stats.toString("minimal"));
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path"); const path = require("path");
module.exports = { module.exports = {

View File

@@ -81,13 +81,13 @@ module.exports = function (opts = {}) {
opts.workerRegexp.flags opts.workerRegexp.flags
); );
if (!workerRegexp.test(code)) { if (!workerRegexp.test(code)) {
return undefined; return;
} }
const ms = new MagicString(code); const ms = new MagicString(code);
// Reset the regexp // Reset the regexp
workerRegexp.lastIndex = 0; workerRegexp.lastIndex = 0;
for (;;) { while (true) {
const match = workerRegexp.exec(code); const match = workerRegexp.exec(code);
if (!match) { if (!match) {
break; break;
@@ -98,7 +98,6 @@ module.exports = function (opts = {}) {
// Parse the optional options object // Parse the optional options object
if (match[3] && match[3].length > 0) { if (match[3] && match[3].length > 0) {
// FIXME: ooooof! // FIXME: ooooof!
// eslint-disable-next-line @typescript-eslint/no-implied-eval
optionsObject = new Function(`return ${match[3].slice(1)};`)(); optionsObject = new Function(`return ${match[3].slice(1)};`)();
} }
delete optionsObject.type; delete optionsObject.type;
@@ -111,14 +110,12 @@ module.exports = function (opts = {}) {
} }
// Find worker file and store it as a chunk with ID prefixed for our loader // 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; const resolvedWorkerFile = (await this.resolve(workerFile, id)).id;
let chunkRefId; let chunkRefId;
if (resolvedWorkerFile in refIds) { if (resolvedWorkerFile in refIds) {
chunkRefId = refIds[resolvedWorkerFile]; chunkRefId = refIds[resolvedWorkerFile];
} else { } else {
this.addWatchFile(resolvedWorkerFile); this.addWatchFile(resolvedWorkerFile);
// eslint-disable-next-line no-await-in-loop
const source = await getBundledWorker( const source = await getBundledWorker(
resolvedWorkerFile, resolvedWorkerFile,
rollupOptions rollupOptions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path"); const path = require("path");
const commonjs = require("@rollup/plugin-commonjs"); const commonjs = require("@rollup/plugin-commonjs");

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const webpack = require("webpack"); const webpack = require("webpack");
const path = require("path"); const path = require("path");
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
@@ -102,6 +103,7 @@ const createWebpackConfig = ({
? path.resolve(context, resource) ? path.resolve(context, resource)
: require.resolve(resource); : require.resolve(resource);
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console
console.error( console.error(
"Error in Home Assistant ignore plugin", "Error in Home Assistant ignore plugin",
resource, resource,

View File

@@ -213,7 +213,7 @@
</p> </p>
<ul> <ul>
<li>Google Chrome (all platforms except iOS)</li> <li>Google Chrome (all platforms except iOS)</li>
<li>Microsoft Edge (all platforms except iOS)</li> <li>Microsoft Edge (all platforms)</li>
</ul> </ul>
</div> </div>

View File

@@ -88,7 +88,7 @@ class HcCast extends LitElement {
> >
${(this.lovelaceConfig ${(this.lovelaceConfig
? this.lovelaceConfig.views ? this.lovelaceConfig.views
: [generateDefaultViewConfig({}, {}, {}, {}, () => "")] : [generateDefaultViewConfig([], [], [], {}, () => "")]
).map( ).map(
(view, idx) => html` (view, idx) => html`
<paper-icon-item <paper-icon-item

View File

@@ -44,7 +44,7 @@ class HcLayout extends LitElement {
<div class="footer"> <div class="footer">
<a href="./faq.html">Frequently Asked Questions</a> Found a bug? <a href="./faq.html">Frequently Asked Questions</a> Found a bug?
<a <a
href="https://github.com/home-assistant/frontend/issues" href="https://github.com/home-assistant/home-assistant-polymer/issues"
target="_blank" target="_blank"
>Let us know!</a >Let us know!</a
> >

View File

@@ -508,7 +508,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
origin_addresses: ["XYZ"], origin_addresses: ["XYZ"],
status: "OK", status: "OK",
mode: "driving", mode: "driving",
units: "us_customary", units: "imperial",
duration_in_traffic: "41 mins", duration_in_traffic: "41 mins",
duration: "44 mins", duration: "44 mins",
distance: "34.3 mi", distance: "34.3 mi",
@@ -527,7 +527,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
origin_addresses: ["XYZ"], origin_addresses: ["XYZ"],
status: "OK", status: "OK",
mode: "driving", mode: "driving",
units: "us_customary", units: "imperial",
duration_in_traffic: "37 mins", duration_in_traffic: "37 mins",
duration: "37 mins", duration: "37 mins",
distance: "30.2 mi", distance: "30.2 mi",

View File

@@ -1196,7 +1196,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
left: "15%", left: "15%",
}, },
type: "state-icon", type: "state-icon",
entity: "binary_sensor.water_leak_sensor_158d00026e26dc", entity: "binary_sensor.water_leak_sensor_158d0002338651",
}, },
{ {
prefix: "Kitchen: ", prefix: "Kitchen: ",
@@ -1206,7 +1206,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
top: "89%", top: "89%",
left: "32%", left: "32%",
}, },
entity: "binary_sensor.water_leak_sensor_158d00026e26dc", entity: "binary_sensor.water_leak_sensor_158d0002338651",
}, },
{ {
style: { style: {
@@ -1215,7 +1215,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
left: "60%", left: "60%",
}, },
type: "state-icon", type: "state-icon",
entity: "binary_sensor.water_leak_sensor_158d0002338651", entity: "binary_sensor.water_leak_sensor_158d00026e26dc",
}, },
{ {
prefix: "Bathroom: ", prefix: "Bathroom: ",
@@ -1225,7 +1225,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
top: "89%", top: "89%",
left: "77%", left: "77%",
}, },
entity: "binary_sensor.water_leak_sensor_158d0002338651", entity: "binary_sensor.water_leak_sensor_158d00026e26dc",
}, },
], ],
type: "picture-elements", type: "picture-elements",

View File

@@ -138,7 +138,7 @@ if (!window.cardTools) {
return cardTools.createThing("row", config); return cardTools.createThing("row", config);
const domain = config.entity.split(".", 1)[0]; const domain = config.entity.split(".", 1)[0];
Object.assign(config, { type: DEFAULT_ROWS[domain] || "simple" }); Object.assign(config, { type: DEFAULT_ROWS[domain] || "text" });
return cardTools.createThing("entity-row", config); return cardTools.createThing("entity-row", config);
}; };

View File

@@ -20,7 +20,6 @@ import { mockHistory } from "./stubs/history";
import { mockLovelace } from "./stubs/lovelace"; import { mockLovelace } from "./stubs/lovelace";
import { mockMediaPlayer } from "./stubs/media_player"; import { mockMediaPlayer } from "./stubs/media_player";
import { mockPersistentNotification } from "./stubs/persistent_notification"; import { mockPersistentNotification } from "./stubs/persistent_notification";
import { mockRecorder } from "./stubs/recorder";
import { mockShoppingList } from "./stubs/shopping_list"; import { mockShoppingList } from "./stubs/shopping_list";
import { mockSystemLog } from "./stubs/system_log"; import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template"; import { mockTemplate } from "./stubs/template";
@@ -46,7 +45,6 @@ class HaDemo extends HomeAssistantAppEl {
mockAuth(hass); mockAuth(hass);
mockTranslations(hass); mockTranslations(hass);
mockHistory(hass); mockHistory(hass);
mockRecorder(hass);
mockShoppingList(hass); mockShoppingList(hass);
mockSystemLog(hass); mockSystemLog(hass);
mockTemplate(hass); mockTemplate(hass);
@@ -63,14 +61,12 @@ class HaDemo extends HomeAssistantAppEl {
area_id: null, area_id: null,
disabled_by: null, disabled_by: null,
entity_id: "sensor.co2_intensity", entity_id: "sensor.co2_intensity",
id: "sensor.co2_intensity",
name: null, name: null,
icon: null, icon: null,
platform: "co2signal", platform: "co2signal",
hidden_by: null, hidden_by: null,
entity_category: null, entity_category: null,
has_entity_name: false, has_entity_name: false,
unique_id: "co2_intensity",
}, },
{ {
config_entry_id: "co2signal", config_entry_id: "co2signal",
@@ -78,14 +74,12 @@ class HaDemo extends HomeAssistantAppEl {
area_id: null, area_id: null,
disabled_by: null, disabled_by: null,
entity_id: "sensor.grid_fossil_fuel_percentage", entity_id: "sensor.grid_fossil_fuel_percentage",
id: "sensor.co2_intensity",
name: null, name: null,
icon: null, icon: null,
platform: "co2signal", platform: "co2signal",
hidden_by: null, hidden_by: null,
entity_category: null, entity_category: null,
has_entity_name: false, has_entity_name: false,
unique_id: "grid_fossil_fuel_percentage",
}, },
]); ]);
@@ -122,9 +116,3 @@ class HaDemo extends HomeAssistantAppEl {
} }
customElements.define("ha-demo", HaDemo); customElements.define("ha-demo", HaDemo);
declare global {
interface HTMLElementTagNameMap {
"ha-demo": HaDemo;
}
}

View File

@@ -1,101 +1,90 @@
import { format, startOfToday, startOfTomorrow } from "date-fns/esm"; import { format, startOfToday, startOfTomorrow } from "date-fns/esm";
import { import { EnergySolarForecasts } from "../../../src/data/energy";
EnergyInfo,
EnergyPreferences,
EnergySolarForecasts,
FossilEnergyConsumption,
} from "../../../src/data/energy";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEnergy = (hass: MockHomeAssistant) => { export const mockEnergy = (hass: MockHomeAssistant) => {
hass.mockWS( hass.mockWS("energy/get_prefs", () => ({
"energy/get_prefs", energy_sources: [
(): EnergyPreferences => ({ {
energy_sources: [ type: "grid",
{ flow_from: [
type: "grid", {
flow_from: [ stat_energy_from: "sensor.energy_consumption_tarif_1",
{ stat_cost: "sensor.energy_consumption_tarif_1_cost",
stat_energy_from: "sensor.energy_consumption_tarif_1", entity_energy_from: "sensor.energy_consumption_tarif_1",
stat_cost: "sensor.energy_consumption_tarif_1_cost", entity_energy_price: null,
entity_energy_price: null, number_energy_price: null,
number_energy_price: null, },
}, {
{ stat_energy_from: "sensor.energy_consumption_tarif_2",
stat_energy_from: "sensor.energy_consumption_tarif_2", stat_cost: "sensor.energy_consumption_tarif_2_cost",
stat_cost: "sensor.energy_consumption_tarif_2_cost", entity_energy_from: "sensor.energy_consumption_tarif_2",
entity_energy_price: null, entity_energy_price: null,
number_energy_price: null, number_energy_price: null,
}, },
], ],
flow_to: [ flow_to: [
{ {
stat_energy_to: "sensor.energy_production_tarif_1", stat_energy_to: "sensor.energy_production_tarif_1",
stat_compensation: stat_compensation: "sensor.energy_production_tarif_1_compensation",
"sensor.energy_production_tarif_1_compensation", entity_energy_to: "sensor.energy_production_tarif_1",
entity_energy_price: null, entity_energy_price: null,
number_energy_price: null, number_energy_price: null,
}, },
{ {
stat_energy_to: "sensor.energy_production_tarif_2", stat_energy_to: "sensor.energy_production_tarif_2",
stat_compensation: stat_compensation: "sensor.energy_production_tarif_2_compensation",
"sensor.energy_production_tarif_2_compensation", entity_energy_to: "sensor.energy_production_tarif_2",
entity_energy_price: null, entity_energy_price: null,
number_energy_price: null, number_energy_price: null,
}, },
], ],
cost_adjustment_day: 0, cost_adjustment_day: 0,
}, },
{ {
type: "solar", type: "solar",
stat_energy_from: "sensor.solar_production", stat_energy_from: "sensor.solar_production",
config_entry_solar_forecast: ["solar_forecast"], config_entry_solar_forecast: ["solar_forecast"],
}, },
/* { /* {
type: "battery", type: "battery",
stat_energy_from: "sensor.battery_output", stat_energy_from: "sensor.battery_output",
stat_energy_to: "sensor.battery_input", stat_energy_to: "sensor.battery_input",
}, */ }, */
{ {
type: "gas", type: "gas",
stat_energy_from: "sensor.energy_gas", stat_energy_from: "sensor.energy_gas",
stat_cost: "sensor.energy_gas_cost", stat_cost: "sensor.energy_gas_cost",
entity_energy_price: null, entity_energy_from: "sensor.energy_gas",
number_energy_price: null, entity_energy_price: null,
}, number_energy_price: null,
], },
device_consumption: [ ],
{ device_consumption: [
stat_consumption: "sensor.energy_car", {
}, stat_consumption: "sensor.energy_car",
{ },
stat_consumption: "sensor.energy_ac", {
}, stat_consumption: "sensor.energy_ac",
{ },
stat_consumption: "sensor.energy_washing_machine", {
}, stat_consumption: "sensor.energy_washing_machine",
{ },
stat_consumption: "sensor.energy_dryer", {
}, stat_consumption: "sensor.energy_dryer",
{ },
stat_consumption: "sensor.energy_heat_pump", {
}, stat_consumption: "sensor.energy_heat_pump",
{ },
stat_consumption: "sensor.energy_boiler", {
}, stat_consumption: "sensor.energy_boiler",
], },
}) ],
); }));
hass.mockWS( hass.mockWS("energy/info", () => ({ cost_sensors: [] }));
"energy/info", hass.mockWS("energy/fossil_energy_consumption", ({ period }) => ({
(): EnergyInfo => ({ cost_sensors: {}, solar_forecast_domains: [] }) start: period === "month" ? 250 : period === "day" ? 10 : 2,
); }));
hass.mockWS(
"energy/fossil_energy_consumption",
({ period }): FossilEnergyConsumption => ({
start: period === "month" ? 250 : period === "day" ? 10 : 2,
})
);
const todayString = format(startOfToday(), "yyyy-MM-dd"); const todayString = format(startOfToday(), "yyyy-MM-dd");
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd"); const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
hass.mockWS( hass.mockWS(

View File

@@ -1,4 +1,12 @@
import {
addDays,
addHours,
addMonths,
differenceInHours,
endOfDay,
} from "date-fns/esm";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { StatisticValue } from "../../../src/data/history";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
interface HistoryQueryParams { interface HistoryQueryParams {
@@ -64,6 +72,331 @@ const generateHistory = (state, deltas) => {
const incrementalUnits = ["clients", "queries", "ads"]; const incrementalUnits = ["clients", "queries", "ads"];
const generateMeanStatistics = (
id: string,
start: Date,
end: Date,
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number
) => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let lastVal = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const delta = Math.random() * maxDiff;
const mean = lastVal + delta;
statistics.push({
statistic_id: id,
start: currentDate.toISOString(),
end: currentDate.toISOString(),
mean,
min: mean - Math.random() * maxDiff,
max: mean + Math.random() * maxDiff,
last_reset: "1970-01-01T00:00:00+00:00",
state: mean,
sum: null,
});
lastVal = mean;
currentDate =
period === "day"
? addDays(currentDate, 1)
: period === "month"
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
}
return statistics;
};
const generateSumStatistics = (
id: string,
start: Date,
end: Date,
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number
) => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let sum = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const add = Math.random() * maxDiff;
sum += add;
statistics.push({
statistic_id: id,
start: currentDate.toISOString(),
end: currentDate.toISOString(),
mean: null,
min: null,
max: null,
last_reset: "1970-01-01T00:00:00+00:00",
state: initValue + sum,
sum,
});
currentDate =
period === "day"
? addDays(currentDate, 1)
: period === "month"
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
}
return statistics;
};
const generateCurvedStatistics = (
id: string,
start: Date,
end: Date,
_period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number,
metered: boolean
) => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let sum = initValue;
const hours = differenceInHours(end, start) - 1;
let i = 0;
let half = false;
const now = new Date();
while (end > currentDate && currentDate < now) {
const add = Math.random() * maxDiff;
sum += i * add;
statistics.push({
statistic_id: id,
start: currentDate.toISOString(),
end: currentDate.toISOString(),
mean: null,
min: null,
max: null,
last_reset: "1970-01-01T00:00:00+00:00",
state: initValue + sum,
sum: metered ? sum : null,
});
currentDate = addHours(currentDate, 1);
if (!half && i > hours / 2) {
half = true;
}
i += half ? -1 : 1;
}
return statistics;
};
const statisticsFunctions: Record<
string,
(
id: string,
start: Date,
end: Date,
period: "5minute" | "hour" | "day" | "month"
) => StatisticValue[]
> = {
"sensor.energy_consumption_tarif_1": (
id: string,
start: Date,
end: Date,
period = "hour"
) => {
if (period !== "hour") {
return generateSumStatistics(
id,
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
const morningLow = generateSumStatistics(
id,
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,
morningFinalVal,
0
);
const eveningLow = generateSumStatistics(
id,
eveningStart,
end,
period,
morningFinalVal,
0.7
);
return [...morningLow, ...empty, ...eveningLow];
},
"sensor.energy_consumption_tarif_2": (
id: string,
start: Date,
end: Date,
period = "hour"
) => {
if (period !== "hour") {
return generateSumStatistics(
id,
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
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,
0,
0.3
);
const highTarifFinalVal = highTarif.length
? highTarif[highTarif.length - 1].sum!
: 0;
const morning = generateSumStatistics(id, start, morningEnd, period, 0, 0);
const evening = generateSumStatistics(
id,
eveningStart,
end,
period,
highTarifFinalVal,
0
);
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_compensation": (
id,
start,
end,
period = "hour"
) => generateSumStatistics(id, start, end, period, 0, 0),
"sensor.energy_production_tarif_2": (id, start, end, period = "hour") => {
if (period !== "hour") {
return generateSumStatistics(
id,
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd));
const production = generateCurvedStatistics(
id,
productionStart,
productionEnd,
period,
0,
0.15,
true
);
const productionFinalVal = production.length
? production[production.length - 1].sum!
: 0;
const morning = generateSumStatistics(
id,
start,
productionStart,
period,
0,
0
);
const evening = generateSumStatistics(
id,
productionEnd,
dayEnd,
period,
productionFinalVal,
0
);
const rest = generateSumStatistics(
id,
dayEnd,
end,
period,
productionFinalVal,
1
);
return [...morning, ...production, ...evening, ...rest];
},
"sensor.solar_production": (id, start, end, period = "hour") => {
if (period !== "hour") {
return generateSumStatistics(
id,
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd));
const production = generateCurvedStatistics(
id,
productionStart,
productionEnd,
period,
0,
0.3,
true
);
const productionFinalVal = production.length
? production[production.length - 1].sum!
: 0;
const morning = generateSumStatistics(
id,
start,
productionStart,
period,
0,
0
);
const evening = generateSumStatistics(
id,
productionEnd,
dayEnd,
period,
productionFinalVal,
0
);
const rest = generateSumStatistics(
id,
dayEnd,
end,
period,
productionFinalVal,
2
);
return [...morning, ...production, ...evening, ...rest];
},
};
export const mockHistory = (mockHass: MockHomeAssistant) => { export const mockHistory = (mockHass: MockHomeAssistant) => {
mockHass.mockAPI( mockHass.mockAPI(
new RegExp("history/period/.+"), new RegExp("history/period/.+"),
@@ -133,4 +466,43 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
return results; return results;
} }
); );
mockHass.mockWS("recorder/get_statistics_metadata", () => []);
mockHass.mockWS("history/list_statistic_ids", () => []);
mockHass.mockWS(
"history/statistics_during_period",
({ statistic_ids, start_time, end_time, period }, hass) => {
const start = new Date(start_time);
const end = end_time ? new Date(end_time) : new Date();
const statistics: Record<string, StatisticValue[]> = {};
statistic_ids.forEach((id: string) => {
if (id in statisticsFunctions) {
statistics[id] = statisticsFunctions[id](id, start, end, period);
} else {
const entityState = hass.states[id];
const state = entityState ? Number(entityState.state) : 1;
statistics[id] =
entityState && "last_reset" in entityState.attributes
? generateSumStatistics(
id,
start,
end,
period,
state,
state * (state > 80 ? 0.01 : 0.05)
)
: generateMeanStatistics(
id,
start,
end,
period,
state,
state * (state > 80 ? 0.05 : 0.1)
);
}
});
return statistics;
}
);
}; };

View File

@@ -1,342 +0,0 @@
import {
addDays,
addHours,
addMonths,
differenceInHours,
endOfDay,
} from "date-fns";
import {
Statistics,
StatisticsMetaData,
StatisticValue,
} from "../../../src/data/recorder";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
const generateMeanStatistics = (
start: Date,
end: Date,
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number
): StatisticValue[] => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let lastVal = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const delta = Math.random() * maxDiff;
const mean = lastVal + delta;
statistics.push({
start: currentDate.getTime(),
end: currentDate.getTime(),
mean,
min: mean - Math.random() * maxDiff,
max: mean + Math.random() * maxDiff,
last_reset: 0,
state: mean,
sum: null,
});
lastVal = mean;
currentDate =
period === "day"
? addDays(currentDate, 1)
: period === "month"
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
}
return statistics;
};
const generateSumStatistics = (
start: Date,
end: Date,
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number
): StatisticValue[] => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let sum = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const add = Math.random() * maxDiff;
sum += add;
statistics.push({
start: currentDate.getTime(),
end: currentDate.getTime(),
mean: null,
min: null,
max: null,
last_reset: 0,
state: initValue + sum,
sum,
});
currentDate =
period === "day"
? addDays(currentDate, 1)
: period === "month"
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
}
return statistics;
};
const generateCurvedStatistics = (
start: Date,
end: Date,
_period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number,
metered: boolean
): StatisticValue[] => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let sum = initValue;
const hours = differenceInHours(end, start) - 1;
let i = 0;
let half = false;
const now = new Date();
while (end > currentDate && currentDate < now) {
const add = Math.random() * maxDiff;
sum += i * add;
statistics.push({
start: currentDate.getTime(),
end: currentDate.getTime(),
mean: null,
min: null,
max: null,
last_reset: 0,
state: initValue + sum,
sum: metered ? sum : null,
});
currentDate = addHours(currentDate, 1);
if (!half && i > hours / 2) {
half = true;
}
i += half ? -1 : 1;
}
return statistics;
};
const statisticsFunctions: Record<
string,
(
id: string,
start: Date,
end: Date,
period: "5minute" | "hour" | "day" | "month"
) => StatisticValue[]
> = {
"sensor.energy_consumption_tarif_1": (
_id: string,
start: Date,
end: Date,
period = "hour"
) => {
if (period !== "hour") {
return generateSumStatistics(
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
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(
morningEnd,
eveningStart,
period,
morningFinalVal,
0
);
const eveningLow = generateSumStatistics(
eveningStart,
end,
period,
morningFinalVal,
0.7
);
return [...morningLow, ...empty, ...eveningLow];
},
"sensor.energy_consumption_tarif_2": (
_id: string,
start: Date,
end: Date,
period = "hour"
) => {
if (period !== "hour") {
return generateSumStatistics(
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const highTarif = generateSumStatistics(
morningEnd,
eveningStart,
period,
0,
0.3
);
const highTarifFinalVal = highTarif.length
? highTarif[highTarif.length - 1].sum!
: 0;
const morning = generateSumStatistics(start, morningEnd, period, 0, 0);
const evening = generateSumStatistics(
eveningStart,
end,
period,
highTarifFinalVal,
0
);
return [...morning, ...highTarif, ...evening];
},
"sensor.energy_production_tarif_1": (_id, start, end, period = "hour") =>
generateSumStatistics(start, end, period, 0, 0),
"sensor.energy_production_tarif_1_compensation": (
_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(
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd));
const production = generateCurvedStatistics(
productionStart,
productionEnd,
period,
0,
0.15,
true
);
const productionFinalVal = production.length
? production[production.length - 1].sum!
: 0;
const morning = generateSumStatistics(start, productionStart, period, 0, 0);
const evening = generateSumStatistics(
productionEnd,
dayEnd,
period,
productionFinalVal,
0
);
const rest = generateSumStatistics(
dayEnd,
end,
period,
productionFinalVal,
1
);
return [...morning, ...production, ...evening, ...rest];
},
"sensor.solar_production": (_id, start, end, period = "hour") => {
if (period !== "hour") {
return generateSumStatistics(
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd));
const production = generateCurvedStatistics(
productionStart,
productionEnd,
period,
0,
0.3,
true
);
const productionFinalVal = production.length
? production[production.length - 1].sum!
: 0;
const morning = generateSumStatistics(start, productionStart, period, 0, 0);
const evening = generateSumStatistics(
productionEnd,
dayEnd,
period,
productionFinalVal,
0
);
const rest = generateSumStatistics(
dayEnd,
end,
period,
productionFinalVal,
2
);
return [...morning, ...production, ...evening, ...rest];
},
};
export const mockRecorder = (mockHass: MockHomeAssistant) => {
mockHass.mockWS(
"recorder/get_statistics_metadata",
(): StatisticsMetaData[] => []
);
mockHass.mockWS(
"recorder/list_statistic_ids",
(): StatisticsMetaData[] => []
);
mockHass.mockWS(
"recorder/statistics_during_period",
({ statistic_ids, start_time, end_time, period }, hass): Statistics => {
const start = new Date(start_time);
const end = end_time ? new Date(end_time) : new Date();
const statistics: Record<string, StatisticValue[]> = {};
statistic_ids.forEach((id: string) => {
if (id in statisticsFunctions) {
statistics[id] = statisticsFunctions[id](id, start, end, period);
} else {
const entityState = hass.states[id];
const state = entityState ? Number(entityState.state) : 1;
statistics[id] =
entityState && "last_reset" in entityState.attributes
? generateSumStatistics(
start,
end,
period,
state,
state * (state > 80 ? 0.01 : 0.05)
)
: generateMeanStatistics(
start,
end,
period,
state,
state * (state > 80 ? 0.05 : 0.1)
);
}
});
return statistics;
}
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,56 +0,0 @@
---
title: When to use remove, delete, add and create
subtitle: The difference between remove/delete and add/create.
---
# Remove vs Delete
Remove and Delete are quite similar, but can be frustrating if used inconsistently.
## Remove
Take away and set aside, but kept in existence.
For example:
* Removing a user's permission
* Removing a user from a group
* Removing links between items
* Removing a widget
* Removing a link
* Removing an item from a cart
## Delete
Erase, rendered nonexistent or nonrecoverable.
For example:
* Deleting a field
* Deleting a value in a field
* Deleting a task
* Deleting a group
* Deleting a permission
* Deleting a calendar event
# Add vs Create
In most cases, Create can be paired with Delete, and Add can be paired with Remove.
## Add
An already-exisiting item.
For example:
* Adding a permission to a user
* Adding a user to a group
* Adding links between items
* Adding a widget
* Adding a link
* Adding an item to a cart
## Create
Something made from scratch.
For example:
* Creating a new field
* Creating a new value in a field
* Creating a new task
* Creating a new group
* Creating a new permission
* Creating a new calendar event
Based on this is [UX magazine article](https://uxmag.com/articles/ui-copy-remove-vs-delete2-banner).

View File

@@ -36,7 +36,6 @@ const conditions = [
{ condition: "sun", after: "sunset" }, { condition: "sun", after: "sunset" },
{ condition: "sun", after: "sunrise", offset: "-01:00" }, { condition: "sun", after: "sunrise", offset: "-01:00" },
{ condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" }, { condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" },
{ condition: "trigger", id: "motion" },
{ condition: "time" }, { condition: "time" },
{ condition: "template" }, { condition: "template" },
]; ];

View File

@@ -1,5 +1,5 @@
/* eslint-disable lit/no-template-arrow */ /* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html, css } from "lit"; import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
@@ -47,8 +47,6 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
class DemoHaAutomationEditorAction extends LitElement { class DemoHaAutomationEditorAction extends LitElement {
@state() private hass!: HomeAssistant; @state() private hass!: HomeAssistant;
@state() private _disabled = false;
private data: any = SCHEMAS.map((info) => info.actions); private data: any = SCHEMAS.map((info) => info.actions);
constructor() { constructor() {
@@ -69,15 +67,6 @@ class DemoHaAutomationEditorAction extends LitElement {
this.requestUpdate(); this.requestUpdate();
}; };
return html` return html`
<div class="options">
<ha-formfield label="Disabled">
<ha-switch
.name=${"disabled"}
.checked=${this._disabled}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
</div>
${SCHEMAS.map( ${SCHEMAS.map(
(info, sampleIdx) => html` (info, sampleIdx) => html`
<demo-black-white-row <demo-black-white-row
@@ -92,7 +81,6 @@ class DemoHaAutomationEditorAction extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.actions=${this.data[sampleIdx]} .actions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx} .sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${valueChanged} @value-changed=${valueChanged}
></ha-automation-action> ></ha-automation-action>
` `
@@ -102,20 +90,6 @@ class DemoHaAutomationEditorAction extends LitElement {
)} )}
`; `;
} }
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}
static styles = css`
.options {
max-width: 800px;
margin: 16px auto;
}
.options ha-formfield {
margin-right: 16px;
}
`;
} }
declare global { declare global {

View File

@@ -1,5 +1,5 @@
/* eslint-disable lit/no-template-arrow */ /* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html, css } from "lit"; import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
@@ -83,8 +83,6 @@ const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
class DemoHaAutomationEditorCondition extends LitElement { class DemoHaAutomationEditorCondition extends LitElement {
@state() private hass!: HomeAssistant; @state() private hass!: HomeAssistant;
@state() private _disabled = false;
private data: any = SCHEMAS.map((info) => info.conditions); private data: any = SCHEMAS.map((info) => info.conditions);
constructor() { constructor() {
@@ -105,15 +103,6 @@ class DemoHaAutomationEditorCondition extends LitElement {
this.requestUpdate(); this.requestUpdate();
}; };
return html` return html`
<div class="options">
<ha-formfield label="Disabled">
<ha-switch
.name=${"disabled"}
.checked=${this._disabled}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
</div>
${SCHEMAS.map( ${SCHEMAS.map(
(info, sampleIdx) => html` (info, sampleIdx) => html`
<demo-black-white-row <demo-black-white-row
@@ -128,7 +117,6 @@ class DemoHaAutomationEditorCondition extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.conditions=${this.data[sampleIdx]} .conditions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx} .sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${valueChanged} @value-changed=${valueChanged}
></ha-automation-condition> ></ha-automation-condition>
` `
@@ -138,20 +126,6 @@ class DemoHaAutomationEditorCondition extends LitElement {
)} )}
`; `;
} }
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}
static styles = css`
.options {
max-width: 800px;
margin: 16px auto;
}
.options ha-formfield {
margin-right: 16px;
}
`;
} }
declare global { declare global {

View File

@@ -1,5 +1,5 @@
/* eslint-disable lit/no-template-arrow */ /* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html, css } from "lit"; import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
@@ -107,8 +107,6 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
class DemoHaAutomationEditorTrigger extends LitElement { class DemoHaAutomationEditorTrigger extends LitElement {
@state() private hass!: HomeAssistant; @state() private hass!: HomeAssistant;
@state() private _disabled = false;
private data: any = SCHEMAS.map((info) => info.triggers); private data: any = SCHEMAS.map((info) => info.triggers);
constructor() { constructor() {
@@ -129,15 +127,6 @@ class DemoHaAutomationEditorTrigger extends LitElement {
this.requestUpdate(); this.requestUpdate();
}; };
return html` return html`
<div class="options">
<ha-formfield label="Disabled">
<ha-switch
.name=${"disabled"}
.checked=${this._disabled}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
</div>
${SCHEMAS.map( ${SCHEMAS.map(
(info, sampleIdx) => html` (info, sampleIdx) => html`
<demo-black-white-row <demo-black-white-row
@@ -152,7 +141,6 @@ class DemoHaAutomationEditorTrigger extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.triggers=${this.data[sampleIdx]} .triggers=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx} .sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${valueChanged} @value-changed=${valueChanged}
></ha-automation-trigger> ></ha-automation-trigger>
` `
@@ -162,20 +150,6 @@ class DemoHaAutomationEditorTrigger extends LitElement {
)} )}
`; `;
} }
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}
static styles = css`
.options {
max-width: 800px;
margin: 16px auto;
}
.options ha-formfield {
margin-right: 16px;
}
`;
} }
declare global { declare global {

View File

@@ -2,6 +2,8 @@
title: "Logo" title: "Logo"
--- ---
![Using our logo](/images/using-our-logo.png)
# Using our logo # Using our logo
As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color. As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color.

View File

@@ -1,11 +1,11 @@
--- ---
title: Dialog title: Dialgos
subtitle: Dialogs provide important prompts in a user flow. subtitle: Dialogs provide important prompts in a user flow.
--- ---
# Material Design 3 # Material Desing 3
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). 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).
# Highlighted guidelines # Highlighted guidelines

View File

@@ -1,3 +0,0 @@
---
title: Bar Slider
---

View File

@@ -1,169 +0,0 @@
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-slider";
import "../../../../src/components/ha-card";
const sliders: {
id: string;
label: string;
mode?: "start" | "end" | "cursor";
class?: string;
}[] = [
{
id: "slider-start",
label: "Slider (start mode)",
mode: "start",
},
{
id: "slider-end",
label: "Slider (end mode)",
mode: "end",
},
{
id: "slider-cursor",
label: "Slider (cursor mode)",
mode: "cursor",
},
{
id: "slider-start-custom",
label: "Slider (start mode) and custom style",
mode: "start",
class: "custom",
},
{
id: "slider-end-custom",
label: "Slider (end mode) and custom style",
mode: "end",
class: "custom",
},
{
id: "slider-cursor-custom",
label: "Slider (cursor mode) and custom style",
mode: "cursor",
class: "custom",
},
];
@customElement("demo-components-ha-bar-slider")
export class DemoHaBarSlider extends LitElement {
@state() private value = 50;
@state() private sliderPosition?: number;
handleValueChanged(e: CustomEvent) {
this.value = e.detail.value as number;
}
handleSliderMoved(e: CustomEvent) {
this.sliderPosition = e.detail.value as number;
}
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
<p><b>Slider values</b></p>
<table>
<tbody>
<tr>
<td>position</td>
<td>${this.sliderPosition ?? "-"}</td>
</tr>
<tr>
<td>value</td>
<td>${this.value ?? "-"}</td>
</tr>
</tbody>
</table>
</div>
</ha-card>
${repeat(sliders, (slider) => {
const { id, label, ...config } = slider;
return html`
<ha-card>
<div class="card-content">
<label id=${id}>${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-bar-slider
.value=${this.value}
.mode=${config.mode}
class=${ifDefined(config.class)}
@value-changed=${this.handleValueChanged}
@slider-moved=${this.handleSliderMoved}
aria-labelledby=${id}
>
</ha-bar-slider>
</div>
</ha-card>
`;
})}
<ha-card>
<div class="card-content">
<p class="title"><b>Vertical</b></p>
<div class="vertical-sliders">
${repeat(sliders, (slider) => {
const { id, label, ...config } = slider;
return html`
<ha-bar-slider
.value=${this.value}
.mode=${config.mode}
vertical
class=${ifDefined(config.class)}
@value-changed=${this.handleValueChanged}
@slider-moved=${this.handleSliderMoved}
aria-label=${label}
>
</ha-bar-slider>
`;
})}
</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 {
--slider-bar-color: #ffcf4c;
--slider-bar-background: #ffcf4c64;
--slider-bar-thickness: 100px;
--slider-bar-border-radius: 24px;
}
.vertical-sliders {
height: 300px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
p.title {
margin-bottom: 12px;
}
.vertical-sliders > *:not(:last-child) {
margin-right: 4px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-bar-slider": DemoHaBarSlider;
}
}

View File

@@ -1,3 +0,0 @@
---
title: Bar Switch
---

View File

@@ -1,145 +0,0 @@
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;
}
}

View File

@@ -1,3 +1,3 @@
--- ---
title: Chip title: Chips
--- ---

View File

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

View File

@@ -1 +0,0 @@
import "../../../../src/components/ha-gauge";

View File

@@ -195,48 +195,6 @@ const SCHEMAS: {
}, },
}, },
}, },
select_disabled_list: {
name: "Select disabled option",
selector: {
select: {
options: [
{ label: "Option 1", value: "Option 1" },
{ label: "Option 2", value: "Option 2" },
{ label: "Option 3", value: "Option 3", disabled: true },
],
mode: "list",
},
},
},
select_disabled_multiple: {
name: "Select disabled option",
selector: {
select: {
multiple: true,
options: [
{ label: "Option 1", value: "Option 1" },
{ label: "Option 2", value: "Option 2" },
{ label: "Option 3", value: "Option 3", disabled: true },
],
mode: "list",
},
},
},
select_disabled: {
name: "Select disabled option",
selector: {
select: {
options: [
{ label: "Option 1", value: "Option 1" },
{ label: "Option 2", value: "Option 2" },
{ label: "Option 3", value: "Option 3", disabled: true },
{ label: "Option 4", value: "Option 4", disabled: true },
{ label: "Option 5", value: "Option 5", disabled: true },
{ label: "Option 6", value: "Option 6" },
],
},
},
},
select_custom: { select_custom: {
name: "Select (Custom)", name: "Select (Custom)",
selector: { selector: {

View File

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

View File

@@ -1 +0,0 @@
import "../../../../src/components/ha-switch";

View File

@@ -1,3 +1,3 @@
--- ---
title: Tip title: Tips
--- ---

View File

@@ -98,9 +98,6 @@ const ENTITIES = [
minimum: 0, minimum: 0,
maximum: 10, maximum: 10,
}), }),
getEntity("text", "message", "Hello!", {
friendly_name: "Message",
}),
getEntity("light", "unavailable", "unavailable", { getEntity("light", "unavailable", "unavailable", {
friendly_name: "Bed Light", friendly_name: "Bed Light",
@@ -132,9 +129,6 @@ const ENTITIES = [
friendly_name: "Who cooks", friendly_name: "Who cooks",
icon: "mdi:cheff", icon: "mdi:cheff",
}), }),
getEntity("text", "unavailable", "unavailable", {
friendly_name: "Message",
}),
]; ];
const CONFIGS = [ const CONFIGS = [
@@ -153,7 +147,6 @@ const CONFIGS = [
- climate.ecobee - climate.ecobee
- input_number.number - input_number.number
- sensor.humidity - sensor.humidity
- text.message
`, `,
}, },
{ {
@@ -226,7 +219,6 @@ const CONFIGS = [
- climate.unavailable - climate.unavailable
- input_number.unavailable - input_number.unavailable
- input_select.unavailable - input_select.unavailable
- text.unavailable
`, `,
}, },
{ {

View File

@@ -23,12 +23,13 @@ const CONFIGS = [
heading: "Basic example", heading: "Basic example",
config: ` config: `
- type: gauge - type: gauge
title: Humidity
entity: sensor.outside_humidity entity: sensor.outside_humidity
name: Outside Humidity name: Outside Humidity
`, `,
}, },
{ {
heading: "Custom unit of measurement", heading: "Custom Unit of Measurement",
config: ` config: `
- type: gauge - type: gauge
entity: sensor.outside_temperature entity: sensor.outside_temperature
@@ -37,16 +38,7 @@ const CONFIGS = [
`, `,
}, },
{ {
heading: "Rendering needle", heading: "Setting Severity Levels",
config: `
- type: gauge
entity: sensor.outside_humidity
name: Outside Humidity
needle: true
`,
},
{
heading: "Setting severity levels",
config: ` config: `
- type: gauge - type: gauge
entity: sensor.brightness entity: sensor.brightness
@@ -58,7 +50,7 @@ const CONFIGS = [
`, `,
}, },
{ {
heading: "Setting severity levels", heading: "Setting Severity Levels",
config: ` config: `
- type: gauge - type: gauge
entity: sensor.brightness_medium entity: sensor.brightness_medium
@@ -70,7 +62,7 @@ const CONFIGS = [
`, `,
}, },
{ {
heading: "Setting severity levels", heading: "Setting Severity Levels",
config: ` config: `
- type: gauge - type: gauge
entity: sensor.brightness_high entity: sensor.brightness_high
@@ -82,7 +74,7 @@ const CONFIGS = [
`, `,
}, },
{ {
heading: "Setting min (0) and mx (15) values", heading: "Setting Min (0) and Max (15) Values",
config: ` config: `
- type: gauge - type: gauge
entity: sensor.brightness entity: sensor.brightness
@@ -92,14 +84,14 @@ const CONFIGS = [
`, `,
}, },
{ {
heading: "Invalid entity", heading: "Invalid Entity",
config: ` config: `
- type: gauge - type: gauge
entity: sensor.invalid_entity entity: sensor.invalid_entity
`, `,
}, },
{ {
heading: "Non-numeric value", heading: "Non-Numeric Value",
config: ` config: `
- type: gauge - type: gauge
entity: plant.bonsai entity: plant.bonsai

View File

@@ -1,3 +0,0 @@
---
title: Entity State
---

View File

@@ -1,377 +0,0 @@
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,
hass.entities
)}`,
},
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;
}
}

View File

@@ -191,12 +191,10 @@ const createEntityRegistryEntries = (
hidden_by: null, hidden_by: null,
entity_category: null, entity_category: null,
entity_id: "binary_sensor.updater", entity_id: "binary_sensor.updater",
id: "binary_sensor.updater",
name: null, name: null,
icon: null, icon: null,
platform: "updater", platform: "updater",
has_entity_name: false, has_entity_name: false,
unique_id: "updater",
}, },
]; ];
@@ -283,7 +281,7 @@ export class DemoIntegrationCard extends LitElement {
.deviceRegistryEntries=${createDeviceRegistryEntries( .deviceRegistryEntries=${createDeviceRegistryEntries(
info.items[0] info.items[0]
)} )}
?entryDisabled=${info.disabled} ?disabled=${info.disabled}
.selectedConfigEntryId=${info.highlight} .selectedConfigEntryId=${info.highlight}
></ha-integration-card> ></ha-integration-card>
` `

View File

@@ -1,7 +1,16 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import { CoverEntityFeature } from "../../../../src/data/cover"; import {
SUPPORT_OPEN,
SUPPORT_STOP,
SUPPORT_CLOSE,
SUPPORT_SET_POSITION,
SUPPORT_OPEN_TILT,
SUPPORT_STOP_TILT,
SUPPORT_CLOSE_TILT,
SUPPORT_SET_TILT_POSITION,
} from "../../../../src/data/cover";
import "../../../../src/dialogs/more-info/more-info-content"; import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { import {
@@ -13,127 +22,113 @@ import "../../components/demo-more-infos";
const ENTITIES = [ const ENTITIES = [
getEntity("cover", "position_buttons", "on", { getEntity("cover", "position_buttons", "on", {
friendly_name: "Position Buttons", friendly_name: "Position Buttons",
supported_features: supported_features: SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE,
CoverEntityFeature.OPEN +
CoverEntityFeature.STOP +
CoverEntityFeature.CLOSE,
}), }),
getEntity("cover", "position_slider_half", "on", { getEntity("cover", "position_slider_half", "on", {
friendly_name: "Position Half-Open", friendly_name: "Position Half-Open",
supported_features: supported_features:
CoverEntityFeature.OPEN + SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
CoverEntityFeature.STOP +
CoverEntityFeature.CLOSE +
CoverEntityFeature.SET_POSITION,
current_position: 50, current_position: 50,
}), }),
getEntity("cover", "position_slider_open", "on", { getEntity("cover", "position_slider_open", "on", {
friendly_name: "Position Open", friendly_name: "Position Open",
supported_features: supported_features:
CoverEntityFeature.OPEN + SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
CoverEntityFeature.STOP +
CoverEntityFeature.CLOSE +
CoverEntityFeature.SET_POSITION,
current_position: 100, current_position: 100,
}), }),
getEntity("cover", "position_slider_closed", "on", { getEntity("cover", "position_slider_closed", "on", {
friendly_name: "Position Closed", friendly_name: "Position Closed",
supported_features: supported_features:
CoverEntityFeature.OPEN + SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
CoverEntityFeature.STOP +
CoverEntityFeature.CLOSE +
CoverEntityFeature.SET_POSITION,
current_position: 0, current_position: 0,
}), }),
getEntity("cover", "tilt_buttons", "on", { getEntity("cover", "tilt_buttons", "on", {
friendly_name: "Tilt Buttons", friendly_name: "Tilt Buttons",
supported_features: supported_features:
CoverEntityFeature.OPEN_TILT + SUPPORT_OPEN_TILT + SUPPORT_STOP_TILT + SUPPORT_CLOSE_TILT,
CoverEntityFeature.STOP_TILT +
CoverEntityFeature.CLOSE_TILT,
}), }),
getEntity("cover", "tilt_slider_half", "on", { getEntity("cover", "tilt_slider_half", "on", {
friendly_name: "Tilt Half-Open", friendly_name: "Tilt Half-Open",
supported_features: supported_features:
CoverEntityFeature.OPEN_TILT + SUPPORT_OPEN_TILT +
CoverEntityFeature.STOP_TILT + SUPPORT_STOP_TILT +
CoverEntityFeature.CLOSE_TILT + SUPPORT_CLOSE_TILT +
CoverEntityFeature.SET_TILT_POSITION, SUPPORT_SET_TILT_POSITION,
current_tilt_position: 50, current_tilt_position: 50,
}), }),
getEntity("cover", "tilt_slider_open", "on", { getEntity("cover", "tilt_slider_open", "on", {
friendly_name: "Tilt Open", friendly_name: "Tilt Open",
supported_features: supported_features:
CoverEntityFeature.OPEN_TILT + SUPPORT_OPEN_TILT +
CoverEntityFeature.STOP_TILT + SUPPORT_STOP_TILT +
CoverEntityFeature.CLOSE_TILT + SUPPORT_CLOSE_TILT +
CoverEntityFeature.SET_TILT_POSITION, SUPPORT_SET_TILT_POSITION,
current_tilt_position: 100, current_tilt_position: 100,
}), }),
getEntity("cover", "tilt_slider_closed", "on", { getEntity("cover", "tilt_slider_closed", "on", {
friendly_name: "Tilt Closed", friendly_name: "Tilt Closed",
supported_features: supported_features:
CoverEntityFeature.OPEN_TILT + SUPPORT_OPEN_TILT +
CoverEntityFeature.STOP_TILT + SUPPORT_STOP_TILT +
CoverEntityFeature.CLOSE_TILT + SUPPORT_CLOSE_TILT +
CoverEntityFeature.SET_TILT_POSITION, SUPPORT_SET_TILT_POSITION,
current_tilt_position: 0, current_tilt_position: 0,
}), }),
getEntity("cover", "position_slider_tilt_slider", "on", { getEntity("cover", "position_slider_tilt_slider", "on", {
friendly_name: "Both Sliders", friendly_name: "Both Sliders",
supported_features: supported_features:
CoverEntityFeature.OPEN + SUPPORT_OPEN +
CoverEntityFeature.STOP + SUPPORT_STOP +
CoverEntityFeature.CLOSE + SUPPORT_CLOSE +
CoverEntityFeature.SET_POSITION + SUPPORT_SET_POSITION +
CoverEntityFeature.OPEN_TILT + SUPPORT_OPEN_TILT +
CoverEntityFeature.STOP_TILT + SUPPORT_STOP_TILT +
CoverEntityFeature.CLOSE_TILT + SUPPORT_CLOSE_TILT +
CoverEntityFeature.SET_TILT_POSITION, SUPPORT_SET_TILT_POSITION,
current_position: 30, current_position: 30,
current_tilt_position: 70, current_tilt_position: 70,
}), }),
getEntity("cover", "position_tilt_slider", "on", { getEntity("cover", "position_tilt_slider", "on", {
friendly_name: "Position & Tilt Slider", friendly_name: "Position & Tilt Slider",
supported_features: supported_features:
CoverEntityFeature.OPEN + SUPPORT_OPEN +
CoverEntityFeature.STOP + SUPPORT_STOP +
CoverEntityFeature.CLOSE + SUPPORT_CLOSE +
CoverEntityFeature.OPEN_TILT + SUPPORT_OPEN_TILT +
CoverEntityFeature.STOP_TILT + SUPPORT_STOP_TILT +
CoverEntityFeature.CLOSE_TILT + SUPPORT_CLOSE_TILT +
CoverEntityFeature.SET_TILT_POSITION, SUPPORT_SET_TILT_POSITION,
current_tilt_position: 70, current_tilt_position: 70,
}), }),
getEntity("cover", "position_slider_tilt", "on", { getEntity("cover", "position_slider_tilt", "on", {
friendly_name: "Position Slider & Tilt", friendly_name: "Position Slider & Tilt",
supported_features: supported_features:
CoverEntityFeature.OPEN + SUPPORT_OPEN +
CoverEntityFeature.STOP + SUPPORT_STOP +
CoverEntityFeature.CLOSE + SUPPORT_CLOSE +
CoverEntityFeature.SET_POSITION + SUPPORT_SET_POSITION +
CoverEntityFeature.OPEN_TILT + SUPPORT_OPEN_TILT +
CoverEntityFeature.STOP_TILT + SUPPORT_STOP_TILT +
CoverEntityFeature.CLOSE_TILT, SUPPORT_CLOSE_TILT,
current_position: 30, current_position: 30,
}), }),
getEntity("cover", "position_slider_only_tilt_slider", "on", { getEntity("cover", "position_slider_only_tilt_slider", "on", {
friendly_name: "Position Slider Only & Tilt Buttons", friendly_name: "Position Slider Only & Tilt Buttons",
supported_features: supported_features:
CoverEntityFeature.SET_POSITION + SUPPORT_SET_POSITION +
CoverEntityFeature.OPEN_TILT + SUPPORT_OPEN_TILT +
CoverEntityFeature.STOP_TILT + SUPPORT_STOP_TILT +
CoverEntityFeature.CLOSE_TILT, SUPPORT_CLOSE_TILT,
current_position: 30, current_position: 30,
}), }),
getEntity("cover", "position_slider_only_tilt", "on", { getEntity("cover", "position_slider_only_tilt", "on", {
friendly_name: "Position Slider Only & Tilt", friendly_name: "Position Slider Only & Tilt",
supported_features: supported_features:
CoverEntityFeature.SET_POSITION + SUPPORT_SET_POSITION +
CoverEntityFeature.OPEN_TILT + SUPPORT_OPEN_TILT +
CoverEntityFeature.STOP_TILT + SUPPORT_STOP_TILT +
CoverEntityFeature.CLOSE_TILT + SUPPORT_CLOSE_TILT +
CoverEntityFeature.SET_TILT_POSITION, SUPPORT_SET_TILT_POSITION,
current_position: 30, current_position: 30,
current_tilt_position: 70, current_tilt_position: 70,
}), }),

View File

@@ -1,3 +0,0 @@
---
title: Input Number
---

View File

@@ -1,60 +0,0 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
const ENTITIES = [
getEntity("input_number", "box1", 0, {
friendly_name: "Box1",
min: 0,
max: 100,
step: 1,
initial: 0,
mode: "box",
unit_of_measurement: "items",
}),
getEntity("input_number", "slider1", 0, {
friendly_name: "Slider1",
min: 0,
max: 100,
step: 1,
initial: 0,
mode: "slider",
unit_of_measurement: "items",
}),
];
@customElement("demo-more-info-input-number")
class DemoMoreInfoInputNumber extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-input-number": DemoMoreInfoInputNumber;
}
}

View File

@@ -1,7 +1,12 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import { LightColorMode, LightEntityFeature } from "../../../../src/data/light"; import {
LightColorModes,
SUPPORT_EFFECT,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
} from "../../../../src/data/light";
import "../../../../src/dialogs/more-info/more-info-content"; import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { import {
@@ -17,8 +22,8 @@ const ENTITIES = [
getEntity("light", "kitchen_light", "on", { getEntity("light", "kitchen_light", "on", {
friendly_name: "Brightness Light", friendly_name: "Brightness Light",
brightness: 200, brightness: 200,
supported_color_modes: [LightColorMode.BRIGHTNESS], supported_color_modes: [LightColorModes.BRIGHTNESS],
color_mode: LightColorMode.BRIGHTNESS, color_mode: LightColorModes.BRIGHTNESS,
}), }),
getEntity("light", "color_temperature_light", "on", { getEntity("light", "color_temperature_light", "on", {
friendly_name: "White Color Temperature Light", friendly_name: "White Color Temperature Light",
@@ -27,10 +32,10 @@ const ENTITIES = [
min_mireds: 30, min_mireds: 30,
max_mireds: 150, max_mireds: 150,
supported_color_modes: [ supported_color_modes: [
LightColorMode.BRIGHTNESS, LightColorModes.BRIGHTNESS,
LightColorMode.COLOR_TEMP, LightColorModes.COLOR_TEMP,
], ],
color_mode: LightColorMode.COLOR_TEMP, color_mode: LightColorModes.COLOR_TEMP,
}), }),
getEntity("light", "color_hs_light", "on", { getEntity("light", "color_hs_light", "on", {
friendly_name: "Color HS Light", friendly_name: "Color HS Light",
@@ -39,16 +44,13 @@ const ENTITIES = [
rgb_color: [30, 100, 255], rgb_color: [30, 100, 255],
min_mireds: 30, min_mireds: 30,
max_mireds: 150, max_mireds: 150,
supported_features: supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
LightEntityFeature.EFFECT +
LightEntityFeature.FLASH +
LightEntityFeature.TRANSITION,
supported_color_modes: [ supported_color_modes: [
LightColorMode.BRIGHTNESS, LightColorModes.BRIGHTNESS,
LightColorMode.COLOR_TEMP, LightColorModes.COLOR_TEMP,
LightColorMode.HS, LightColorModes.HS,
], ],
color_mode: LightColorMode.HS, color_mode: LightColorModes.HS,
effect_list: ["random", "colorloop"], effect_list: ["random", "colorloop"],
}), }),
getEntity("light", "color_rgb_ct_light", "on", { getEntity("light", "color_rgb_ct_light", "on", {
@@ -57,28 +59,22 @@ const ENTITIES = [
color_temp: 75, color_temp: 75,
min_mireds: 30, min_mireds: 30,
max_mireds: 150, max_mireds: 150,
supported_features: supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
LightEntityFeature.EFFECT +
LightEntityFeature.FLASH +
LightEntityFeature.TRANSITION,
supported_color_modes: [ supported_color_modes: [
LightColorMode.BRIGHTNESS, LightColorModes.BRIGHTNESS,
LightColorMode.COLOR_TEMP, LightColorModes.COLOR_TEMP,
LightColorMode.RGB, LightColorModes.RGB,
], ],
color_mode: LightColorMode.COLOR_TEMP, color_mode: LightColorModes.COLOR_TEMP,
effect_list: ["random", "colorloop"], effect_list: ["random", "colorloop"],
}), }),
getEntity("light", "color_RGB_light", "on", { getEntity("light", "color_RGB_light", "on", {
friendly_name: "Color Effects Light", friendly_name: "Color Effects Light",
brightness: 255, brightness: 255,
rgb_color: [30, 100, 255], rgb_color: [30, 100, 255],
supported_features: supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
LightEntityFeature.EFFECT + supported_color_modes: [LightColorModes.BRIGHTNESS, LightColorModes.RGB],
LightEntityFeature.FLASH + color_mode: LightColorModes.RGB,
LightEntityFeature.TRANSITION,
supported_color_modes: [LightColorMode.BRIGHTNESS, LightColorMode.RGB],
color_mode: LightColorMode.RGB,
effect_list: ["random", "colorloop"], effect_list: ["random", "colorloop"],
}), }),
getEntity("light", "color_rgbw_light", "on", { getEntity("light", "color_rgbw_light", "on", {
@@ -87,16 +83,13 @@ const ENTITIES = [
rgbw_color: [30, 100, 255, 125], rgbw_color: [30, 100, 255, 125],
min_mireds: 30, min_mireds: 30,
max_mireds: 150, max_mireds: 150,
supported_features: supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
LightEntityFeature.EFFECT +
LightEntityFeature.FLASH +
LightEntityFeature.TRANSITION,
supported_color_modes: [ supported_color_modes: [
LightColorMode.BRIGHTNESS, LightColorModes.BRIGHTNESS,
LightColorMode.COLOR_TEMP, LightColorModes.COLOR_TEMP,
LightColorMode.RGBW, LightColorModes.RGBW,
], ],
color_mode: LightColorMode.RGBW, color_mode: LightColorModes.RGBW,
effect_list: ["random", "colorloop"], effect_list: ["random", "colorloop"],
}), }),
getEntity("light", "color_rgbww_light", "on", { getEntity("light", "color_rgbww_light", "on", {
@@ -105,16 +98,13 @@ const ENTITIES = [
rgbww_color: [30, 100, 255, 125, 10], rgbww_color: [30, 100, 255, 125, 10],
min_mireds: 30, min_mireds: 30,
max_mireds: 150, max_mireds: 150,
supported_features: supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
LightEntityFeature.EFFECT +
LightEntityFeature.FLASH +
LightEntityFeature.TRANSITION,
supported_color_modes: [ supported_color_modes: [
LightColorMode.BRIGHTNESS, LightColorModes.BRIGHTNESS,
LightColorMode.COLOR_TEMP, LightColorModes.COLOR_TEMP,
LightColorMode.RGBWW, LightColorModes.RGBWW,
], ],
color_mode: LightColorMode.RGBWW, color_mode: LightColorModes.RGBWW,
effect_list: ["random", "colorloop"], effect_list: ["random", "colorloop"],
}), }),
getEntity("light", "color_xy_light", "on", { getEntity("light", "color_xy_light", "on", {
@@ -124,16 +114,13 @@ const ENTITIES = [
rgb_color: [30, 100, 255], rgb_color: [30, 100, 255],
min_mireds: 30, min_mireds: 30,
max_mireds: 150, max_mireds: 150,
supported_features: supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
LightEntityFeature.EFFECT +
LightEntityFeature.FLASH +
LightEntityFeature.TRANSITION,
supported_color_modes: [ supported_color_modes: [
LightColorMode.BRIGHTNESS, LightColorModes.BRIGHTNESS,
LightColorMode.COLOR_TEMP, LightColorModes.COLOR_TEMP,
LightColorMode.XY, LightColorModes.XY,
], ],
color_mode: LightColorMode.XY, color_mode: LightColorModes.XY,
effect_list: ["random", "colorloop"], effect_list: ["random", "colorloop"],
}), }),
]; ];

View File

@@ -139,13 +139,6 @@ const ENTITIES = [
title: undefined, title: undefined,
friendly_name: "Installing without title", friendly_name: "Installing without title",
}), }),
getEntity("update", "update21", "on", {
...base_attributes,
in_progress: true,
friendly_name: "Update with in_progress true and UPDATE_SUPPORT_PROGRESS",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
}),
]; ];
@customElement("demo-more-info-update") @customElement("demo-more-info-update")

View File

@@ -118,7 +118,7 @@ class HassioAddonRepositoryEl extends LitElement {
} }
private _addonTapped(ev) { private _addonTapped(ev) {
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}?store=true`); navigate(`/hassio/addon/${ev.currentTarget.addon.slug}`);
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -53,13 +53,7 @@ class HassioAddonDashboard extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@state() private _error?: string; @state() _error?: string;
private _backPath = new URLSearchParams(window.parent.location.search).get(
"store"
)
? "/hassio/store"
: "/hassio/dashboard";
private _computeTail = memoizeOne((route: Route) => { private _computeTail = memoizeOne((route: Route) => {
const dividerPos = route.path.indexOf("/", 1); const dividerPos = route.path.indexOf("/", 1);
@@ -125,7 +119,6 @@ class HassioAddonDashboard extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${route} .route=${route}
.tabs=${addonTabs} .tabs=${addonTabs}
.backPath=${this._backPath}
supervisor supervisor
> >
<span slot="header">${this.addon.name}</span> <span slot="header">${this.addon.name}</span>

View File

@@ -1024,13 +1024,10 @@ class HassioAddonInfo extends LitElement {
button.progress = true; button.progress = true;
const confirmed = await showConfirmationDialog(this, { const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("dialog.uninstall_addon.title", { title: this.addon.name,
name: this.addon.name, text: "Are you sure you want to uninstall this add-on?",
}), confirmText: "uninstall add-on",
text: this.supervisor.localize("dialog.uninstall_addon.text"), dismissText: "no",
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
dismissText: this.supervisor.localize("common.cancel"),
destructive: true,
}); });
if (!confirmed) { if (!confirmed) {

View File

@@ -119,7 +119,6 @@ export class HassioBackups extends LitElement {
(narrow: boolean): DataTableColumnContainer => ({ (narrow: boolean): DataTableColumnContainer => ({
name: { name: {
title: this.supervisor.localize("backup.name"), title: this.supervisor.localize("backup.name"),
main: true,
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,

View File

@@ -18,11 +18,9 @@ export const suggestAddonRestart = async (
addon: HassioAddonDetails addon: HassioAddonDetails
): Promise<void> => { ): Promise<void> => {
const confirmed = await showConfirmationDialog(element, { const confirmed = await showConfirmationDialog(element, {
title: supervisor.localize("dialog.restart_addon.title", { title: supervisor.localize("common.restart_name", "name", addon.name),
name: addon.name,
}),
text: supervisor.localize("dialog.restart_addon.text"), text: supervisor.localize("dialog.restart_addon.text"),
confirmText: supervisor.localize("dialog.restart_addon.restart"), confirmText: supervisor.localize("dialog.restart_addon.confirm_text"),
dismissText: supervisor.localize("common.cancel"), dismissText: supervisor.localize("common.cancel"),
}); });
if (confirmed) { if (confirmed) {
@@ -30,9 +28,11 @@ export const suggestAddonRestart = async (
await restartHassioAddon(hass, addon.slug); await restartHassioAddon(hass, addon.slug);
} catch (err: any) { } catch (err: any) {
showAlertDialog(element, { showAlertDialog(element, {
title: supervisor.localize("common.failed_to_restart_name", { title: supervisor.localize(
name: addon.name, "common.failed_to_restart_name",
}), "name",
addon.name
),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
} }

View File

@@ -23,7 +23,6 @@ import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box"; } from "../../../src/dialogs/generic/show-dialog-box";
import { showJoinBetaDialog } from "../../../src/panels/config/core/updates/show-dialog-join-beta";
import { import {
UNHEALTHY_REASON_URL, UNHEALTHY_REASON_URL,
UNSUPPORTED_REASON_URL, UNSUPPORTED_REASON_URL,
@@ -231,27 +230,36 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true; button.progress = true;
if (this.supervisor.supervisor.channel === "stable") { if (this.supervisor.supervisor.channel === "stable") {
showJoinBetaDialog(this, { const confirmed = await showConfirmationDialog(this, {
join: async () => { title: this.supervisor.localize("system.supervisor.warning"),
await this._setChannel("beta"); text: html`${this.supervisor.localize("system.supervisor.beta_warning")}
button.progress = false; <br />
}, <b> ${this.supervisor.localize("system.supervisor.beta_backup")} </b>
cancel: () => { <br /><br />
button.progress = false; ${this.supervisor.localize("system.supervisor.beta_release_items")}
}, <ul>
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
</ul>
<br />
${this.supervisor.localize("system.supervisor.beta_join_confirm")}`,
confirmText: this.supervisor.localize(
"system.supervisor.join_beta_action"
),
dismissText: this.supervisor.localize("common.cancel"),
}); });
} else {
await this._setChannel("stable");
button.progress = false;
}
}
private async _setChannel( if (!confirmed) {
channel: SupervisorOptions["channel"] button.progress = false;
): Promise<void> { return;
}
}
try { try {
const data: Partial<SupervisorOptions> = { const data: Partial<SupervisorOptions> = {
channel, channel:
this.supervisor.supervisor.channel === "stable" ? "beta" : "stable",
}; };
await setSupervisorOption(this.hass, data); await setSupervisorOption(this.hass, data);
await this._reloadSupervisor(); await this._reloadSupervisor();
@@ -262,6 +270,8 @@ class HassioSupervisorInfo extends LitElement {
), ),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
} finally {
button.progress = false;
} }
} }

View File

@@ -1,8 +1,11 @@
module.exports = { module.exports = {
"*.{js,ts}": ["prettier --write", "eslint --fix"], "*.{js,ts}": [
"prettier --write",
'eslint --ignore-pattern "**/build-scripts/**/*.js" --fix',
],
"!(/translations)*.{json,css,md,html}": "prettier --write", "!(/translations)*.{json,css,md,html}": "prettier --write",
"translations/*/*.json": (files) => "translations/*/*.json": (files) =>
'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." ' + '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." ' +
files.join(" ") + files.join(" ") +
" >&2 && exit 1", " >&2 && exit 1",
}; };

View File

@@ -43,6 +43,7 @@
"@formatjs/intl-numberformat": "^7.2.5", "@formatjs/intl-numberformat": "^7.2.5",
"@formatjs/intl-pluralrules": "^4.1.5", "@formatjs/intl-pluralrules": "^4.1.5",
"@formatjs/intl-relativetimeformat": "^9.3.2", "@formatjs/intl-relativetimeformat": "^9.3.2",
"@formatjs/intl-utils": "^3.8.4",
"@fullcalendar/common": "5.9.0", "@fullcalendar/common": "5.9.0",
"@fullcalendar/core": "5.9.0", "@fullcalendar/core": "5.9.0",
"@fullcalendar/daygrid": "5.9.0", "@fullcalendar/daygrid": "5.9.0",
@@ -92,8 +93,8 @@
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.4.1", "@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.5.4", "@thomasloven/round-slider": "0.5.4",
"@vaadin/combo-box": "^23.2.9", "@vaadin/combo-box": "^23.1.5",
"@vaadin/vaadin-themable-mixin": "^23.2.9", "@vaadin/vaadin-themable-mixin": "^23.1.5",
"@vibrant/color": "^3.2.1-alpha.1", "@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
@@ -110,9 +111,8 @@
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hammerjs": "^2.0.8", "hls.js": "^1.2.1",
"hls.js": "^1.2.5", "home-assistant-js-websocket": "^8.0.0",
"home-assistant-js-websocket": "^8.0.1",
"idb-keyval": "^5.1.3", "idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1", "intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
@@ -129,7 +129,6 @@
"regenerator-runtime": "^0.13.8", "regenerator-runtime": "^0.13.8",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"rrule": "^2.7.1",
"sortablejs": "^1.14.0", "sortablejs": "^1.14.0",
"superstruct": "^0.15.2", "superstruct": "^0.15.2",
"tinykeys": "^1.1.3", "tinykeys": "^1.1.3",
@@ -139,7 +138,6 @@
"vis-network": "^8.5.4", "vis-network": "^8.5.4",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1", "vue2-daterange-picker": "^0.5.1",
"weekstart": "^1.1.0",
"workbox-cacheable-response": "^6.4.2", "workbox-cacheable-response": "^6.4.2",
"workbox-core": "^6.4.2", "workbox-core": "^6.4.2",
"workbox-expiration": "^6.4.2", "workbox-expiration": "^6.4.2",
@@ -149,21 +147,19 @@
"xss": "^1.0.9" "xss": "^1.0.9"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.2", "@babel/core": "^7.15.5",
"@babel/plugin-external-helpers": "^7.18.6", "@babel/plugin-external-helpers": "^7.14.5",
"@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-decorators": "^7.20.2", "@babel/plugin-proposal-decorators": "^7.15.4",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
"@babel/plugin-proposal-object-rest-spread": "^7.20.2", "@babel/plugin-proposal-object-rest-spread": "^7.15.6",
"@babel/plugin-proposal-optional-chaining": "^7.18.9", "@babel/plugin-proposal-optional-chaining": "^7.14.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.15.6",
"@babel/preset-typescript": "^7.18.6", "@babel/preset-typescript": "^7.15.0",
"@koa/cors": "^3.1.0", "@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", "@open-wc/dev-server-hmr": "^0.0.2",
"@rollup/plugin-babel": "^5.2.1", "@rollup/plugin-babel": "^5.2.1",
"@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-commonjs": "^11.1.0",
@@ -173,7 +169,6 @@
"@types/chromecast-caf-receiver": "5.0.12", "@types/chromecast-caf-receiver": "5.0.12",
"@types/chromecast-caf-sender": "^1.0.3", "@types/chromecast-caf-sender": "^1.0.3",
"@types/glob": "^7", "@types/glob": "^7",
"@types/hammerjs": "^2.0.41",
"@types/js-yaml": "^4", "@types/js-yaml": "^4",
"@types/leaflet": "^1", "@types/leaflet": "^1",
"@types/leaflet-draw": "^1", "@types/leaflet-draw": "^1",
@@ -181,13 +176,12 @@
"@types/mocha": "^8", "@types/mocha": "^8",
"@types/qrcode": "^1.4.2", "@types/qrcode": "^1.4.2",
"@types/sortablejs": "^1", "@types/sortablejs": "^1",
"@types/tar": "^6",
"@types/webspeechapi": "^0.0.29", "@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^5.44.0", "@typescript-eslint/eslint-plugin": "^4.32.0",
"@typescript-eslint/parser": "^5.44.0", "@typescript-eslint/parser": "^4.32.0",
"@web/dev-server": "^0.0.24", "@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11", "@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^9.1.0", "babel-loader": "^8.2.2",
"chai": "^4.3.4", "chai": "^4.3.4",
"del": "^4.0.0", "del": "^4.0.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
@@ -213,7 +207,6 @@
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",
"husky": "^8.0.1", "husky": "^8.0.1",
"instant-mocha": "^1.3.1", "instant-mocha": "^1.3.1",
"jszip": "^3.10.1",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"lit-analyzer": "^1.2.1", "lit-analyzer": "^1.2.1",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
@@ -234,10 +227,9 @@
"sinon": "^11.0.0", "sinon": "^11.0.0",
"source-map-url": "^0.4.0", "source-map-url": "^0.4.0",
"systemjs": "^6.3.2", "systemjs": "^6.3.2",
"tar": "^6.1.11",
"terser-webpack-plugin": "^5.2.4", "terser-webpack-plugin": "^5.2.4",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^1.2.1",
"typescript": "^4.9.3", "typescript": "^4.4.3",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
"webpack": "^5.55.1", "webpack": "^5.55.1",
@@ -261,5 +253,5 @@
"trailingComma": "es5", "trailingComma": "es5",
"arrowParens": "always" "arrowParens": "always"
}, },
"packageManager": "yarn@3.2.3" "packageManager": "yarn@3.2.0"
} }

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20221201.1" version = "20220816.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@@ -7,4 +7,4 @@ set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
# Install node modules # Install node modules
yarn install yarn install

View File

@@ -46,14 +46,6 @@ frontend:
# development_repo: ${WD}" >> "${WD}/config/configuration.yaml" # development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
fi fi
if [ ! -z "${CODESPACES}" ]; then
echo "
http:
use_x_forwarded_for: true
trusted_proxies:
- 127.0.0.1
" >> "${WD}/config/configuration.yaml"
fi
fi fi
hass -c "${WD}/config" hass -c "${WD}/config"

View File

@@ -1,9 +0,0 @@
#!/bin/sh
# Setup translation fetching during development
# Stop on errors
set -e
cd "$(dirname "$0")/.."
./node_modules/.bin/gulp setup-and-fetch-nightly-translations

View File

@@ -20,28 +20,24 @@ fi
# Load token from file if not already in the environment # Load token from file if not already in the environment
[ -z "${LOKALISE_TOKEN-}" ] && LOKALISE_TOKEN="$(<.lokalise_token)" [ -z "${LOKALISE_TOKEN-}" ] && LOKALISE_TOKEN="$(<.lokalise_token)"
declare -A PROJECT_ID=( \ PROJECT_ID="3420425759f6d6d241f598.13594006"
[frontend]="3420425759f6d6d241f598.13594006" \ LOCAL_DIR="$(pwd)/translations/downloads"
[backend]="130246255a974bd3b5e8a1.51616605" \ FILE_FORMAT=json
)
for project in ${!PROJECT_ID[*]}; do mkdir -p ${LOCAL_DIR}
LOCAL_DIR=`pwd`/translations/${project}
rm -f ${LOCAL_DIR}/* || mkdir -p ${LOCAL_DIR} docker run \
docker run \ -v ${LOCAL_DIR}:/opt/dest/locale \
-v ${LOCAL_DIR}:/opt/dest/locale \ --rm \
--rm \ lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d lokalise2 \
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d \ --token ${LOKALISE_TOKEN} \
lokalise2 \ --project-id ${PROJECT_ID} \
--token ${LOKALISE_TOKEN} \ file download \
--project-id ${PROJECT_ID[${project}]} \ --export-empty-as skip \
file download \ --format json \
--export-empty-as skip \ --json-unescaped-slashes=true \
--format json \ --replace-breaks=false \
--json-unescaped-slashes=true \ --original-filenames=false \
--replace-breaks=false \ --unzip-to /opt/dest
--original-filenames=false \
--unzip-to /opt/dest
done
./node_modules/.bin/gulp check-downloaded-translations ./node_modules/.bin/gulp check-downloaded-translations

View File

@@ -15,7 +15,7 @@ import { computeInitialHaFormData } from "../components/ha-form/compute-initial-
import "../components/ha-form/ha-form"; import "../components/ha-form/ha-form";
import "../components/ha-formfield"; import "../components/ha-formfield";
import "../components/ha-markdown"; import "../components/ha-markdown";
import { AuthProvider, autocompleteLoginFields } from "../data/auth"; import { AuthProvider } from "../data/auth";
import { import {
DataEntryFlowStep, DataEntryFlowStep,
DataEntryFlowStepForm, DataEntryFlowStepForm,
@@ -204,7 +204,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
: html``} : html``}
<ha-form <ha-form
.data=${this._stepData} .data=${this._stepData}
.schema=${autocompleteLoginFields(step.data_schema)} .schema=${step.data_schema}
.error=${step.errors} .error=${step.errors}
.disabled=${this._submitting} .disabled=${this._submitting}
.computeLabel=${this._computeLabelCallback(step)} .computeLabel=${this._computeLabelCallback(step)}

View File

@@ -3,7 +3,6 @@ import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { HaFormSchema } from "../components/ha-form/types"; import type { HaFormSchema } from "../components/ha-form/types";
import { autocompleteLoginFields } from "../data/auth";
import type { DataEntryFlowStep } from "../data/data_entry_flow"; import type { DataEntryFlowStep } from "../data/data_entry_flow";
declare global { declare global {
@@ -70,9 +69,7 @@ export class HaPasswordManagerPolyfill extends LitElement {
aria-hidden="true" aria-hidden="true"
@submit=${this._handleSubmit} @submit=${this._handleSubmit}
> >
${autocompleteLoginFields(this.step.data_schema).map((input) => ${this.step.data_schema.map((input) => this.render_input(input))}
this.render_input(input)
)}
<input type="submit" /> <input type="submit" />
<style> <style>
${this.styles} ${this.styles}
@@ -92,10 +89,8 @@ export class HaPasswordManagerPolyfill extends LitElement {
<input <input
tabindex="-1" tabindex="-1"
.id=${schema.name} .id=${schema.name}
.name=${schema.name}
.type=${inputType} .type=${inputType}
.value=${this.stepData[schema.name] || ""} .value=${this.stepData[schema.name] || ""}
.autocomplete=${schema.autocomplete}
@input=${this._valueChanged} @input=${this._valueChanged}
/> />
`; `;

View File

@@ -1,42 +0,0 @@
import { hex2rgb } from "./convert-color";
export const THEME_COLORS = new Set([
"primary",
"accent",
"disabled",
"red",
"pink",
"purple",
"deep-purple",
"indigo",
"blue",
"light-blue",
"cyan",
"teal",
"green",
"light-green",
"lime",
"yellow",
"amber",
"orange",
"deep-orange",
"brown",
"grey",
"blue-grey",
"black",
"white",
]);
export function computeRgbColor(color: string): string {
if (THEME_COLORS.has(color)) {
return `var(--rgb-${color}-color)`;
}
if (color.startsWith("#")) {
try {
return hex2rgb(color).join(", ");
} catch (err) {
return "";
}
}
return color;
}

View File

@@ -6,14 +6,12 @@ import {
mdiAlert, mdiAlert,
mdiAngleAcute, mdiAngleAcute,
mdiAppleSafari, mdiAppleSafari,
mdiArrowLeftRight,
mdiBell, mdiBell,
mdiBookmark, mdiBookmark,
mdiBrightness5, mdiBrightness5,
mdiBullhorn, mdiBullhorn,
mdiCalendar, mdiCalendar,
mdiCalendarClock, mdiCalendarClock,
mdiCarCoolantLevel,
mdiCash, mdiCash,
mdiClock, mdiClock,
mdiCloudUpload, mdiCloudUpload,
@@ -27,6 +25,7 @@ import {
mdiFlower, mdiFlower,
mdiFormatListBulleted, mdiFormatListBulleted,
mdiFormTextbox, mdiFormTextbox,
mdiGasCylinder,
mdiGauge, mdiGauge,
mdiGestureTapButton, mdiGestureTapButton,
mdiGoogleAssistant, mdiGoogleAssistant,
@@ -38,31 +37,23 @@ import {
mdiLightningBolt, mdiLightningBolt,
mdiMailbox, mdiMailbox,
mdiMapMarkerRadius, mdiMapMarkerRadius,
mdiMeterGas,
mdiMicrophoneMessage,
mdiMolecule, mdiMolecule,
mdiMoleculeCo, mdiMoleculeCo,
mdiMoleculeCo2, mdiMoleculeCo2,
mdiPalette, mdiPalette,
mdiProgressClock,
mdiRayVertex, mdiRayVertex,
mdiRemote, mdiRemote,
mdiRobot, mdiRobot,
mdiRobotVacuum, mdiRobotVacuum,
mdiScriptText, mdiScriptText,
mdiSineWave, mdiSineWave,
mdiSpeedometer, mdiMicrophoneMessage,
mdiThermometer, mdiThermometer,
mdiThermostat, mdiThermostat,
mdiTimerOutline, mdiTimerOutline,
mdiVideo, mdiVideo,
mdiWater,
mdiWaterPercent, mdiWaterPercent,
mdiWeatherCloudy, mdiWeatherCloudy,
mdiWeatherPouring,
mdiWeatherRainy,
mdiWeatherWindy,
mdiWeight,
mdiWhiteBalanceSunny, mdiWhiteBalanceSunny,
mdiWifi, mdiWifi,
} from "@mdi/js"; } from "@mdi/js";
@@ -114,7 +105,6 @@ export const FIXED_DOMAIN_ICONS = {
siren: mdiBullhorn, siren: mdiBullhorn,
simple_alarm: mdiBell, simple_alarm: mdiBell,
sun: mdiWhiteBalanceSunny, sun: mdiWhiteBalanceSunny,
text: mdiFormTextbox,
timer: mdiTimerOutline, timer: mdiTimerOutline,
updater: mdiCloudUpload, updater: mdiCloudUpload,
vacuum: mdiRobotVacuum, vacuum: mdiRobotVacuum,
@@ -131,14 +121,11 @@ export const FIXED_DEVICE_CLASS_ICONS = {
carbon_monoxide: mdiMoleculeCo, carbon_monoxide: mdiMoleculeCo,
current: mdiCurrentAc, current: mdiCurrentAc,
date: mdiCalendar, date: mdiCalendar,
distance: mdiArrowLeftRight,
duration: mdiProgressClock,
energy: mdiLightningBolt, energy: mdiLightningBolt,
frequency: mdiSineWave, frequency: mdiSineWave,
gas: mdiMeterGas, gas: mdiGasCylinder,
humidity: mdiWaterPercent, humidity: mdiWaterPercent,
illuminance: mdiBrightness5, illuminance: mdiBrightness5,
moisture: mdiWaterPercent,
monetary: mdiCash, monetary: mdiCash,
nitrogen_dioxide: mdiMolecule, nitrogen_dioxide: mdiMolecule,
nitrogen_monoxide: mdiMolecule, nitrogen_monoxide: mdiMolecule,
@@ -149,21 +136,14 @@ export const FIXED_DEVICE_CLASS_ICONS = {
pm25: mdiMolecule, pm25: mdiMolecule,
power: mdiFlash, power: mdiFlash,
power_factor: mdiAngleAcute, power_factor: mdiAngleAcute,
precipitation: mdiWeatherRainy,
precipitation_intensity: mdiWeatherPouring,
pressure: mdiGauge, pressure: mdiGauge,
reactive_power: mdiFlash, reactive_power: mdiFlash,
signal_strength: mdiWifi, signal_strength: mdiWifi,
speed: mdiSpeedometer,
sulphur_dioxide: mdiMolecule, sulphur_dioxide: mdiMolecule,
temperature: mdiThermometer, temperature: mdiThermometer,
timestamp: mdiClock, timestamp: mdiClock,
volatile_organic_compounds: mdiMolecule, volatile_organic_compounds: mdiMolecule,
voltage: mdiSineWave, voltage: mdiSineWave,
volume: mdiCarCoolantLevel,
water: mdiWater,
weight: mdiWeight,
wind_speed: mdiWeatherWindy,
}; };
/** Domains that have a state card. */ /** Domains that have a state card. */
@@ -183,7 +163,6 @@ export const DOMAINS_WITH_CARD = [
"script", "script",
"select", "select",
"timer", "timer",
"text",
"vacuum", "vacuum",
"water_heater", "water_heater",
]; ];
@@ -216,7 +195,6 @@ export const DOMAINS_INPUT_ROW = [
"script", "script",
"select", "select",
"switch", "switch",
"text",
"vacuum", "vacuum",
]; ];

View File

@@ -1,33 +0,0 @@
import { getWeekStartByLocale } from "weekstart";
import { FrontendLocaleData, FirstWeekday } from "../../data/translation";
export const weekdays = [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
] as const;
type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
export const firstWeekdayIndex = (locale: FrontendLocaleData): WeekdayIndex => {
if (locale.first_weekday === FirstWeekday.language) {
// @ts-ignore
if ("weekInfo" in Intl.Locale.prototype) {
// @ts-ignore
return new Intl.Locale(locale.language).weekInfo.firstDay % 7;
}
return (getWeekStartByLocale(locale.language) % 7) as WeekdayIndex;
}
return weekdays.includes(locale.first_weekday)
? (weekdays.indexOf(locale.first_weekday) as WeekdayIndex)
: 1;
};
export const firstWeekday = (locale: FrontendLocaleData) => {
const index = firstWeekdayIndex(locale);
return weekdays[index];
};

View File

@@ -1,28 +0,0 @@
import { HaDurationData } from "../../components/ha-duration-input";
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
export const formatDuration = (duration: HaDurationData) => {
const d = duration.days || 0;
const h = duration.hours || 0;
const m = duration.minutes || 0;
const s = duration.seconds || 0;
const ms = duration.milliseconds || 0;
if (d > 0) {
return `${d} day${d === 1 ? "" : "s"} ${h}:${leftPad(m)}:${leftPad(s)}`;
}
if (h > 0) {
return `${h}:${leftPad(m)}:${leftPad(s)}`;
}
if (m > 0) {
return `${m}:${leftPad(s)}`;
}
if (s > 0) {
return `${s} second${s === 1 ? "" : "s"}`;
}
if (ms > 0) {
return `${ms} millisecond${ms === 1 ? "" : "s"}`;
}
return null;
};

View File

@@ -1,7 +1,7 @@
import { selectUnit } from "@formatjs/intl-utils";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import { polyfillsLoaded } from "../translations/localize"; import { polyfillsLoaded } from "../translations/localize";
import { selectUnit } from "../util/select-unit";
if (__BUILD__ === "latest" && polyfillsLoaded) { if (__BUILD__ === "latest" && polyfillsLoaded) {
await polyfillsLoaded; await polyfillsLoaded;
@@ -9,6 +9,7 @@ if (__BUILD__ === "latest" && polyfillsLoaded) {
const formatRelTimeMem = memoizeOne( const formatRelTimeMem = memoizeOne(
(locale: FrontendLocaleData) => (locale: FrontendLocaleData) =>
// @ts-expect-error
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" }) new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" })
); );
@@ -24,6 +25,7 @@ export const relativeTime = (
} }
return Intl.NumberFormat(locale.language, { return Intl.NumberFormat(locale.language, {
style: "unit", style: "unit",
// @ts-expect-error
unit: diff.unit, unit: diff.unit,
unitDisplay: "long", unitDisplay: "long",
}).format(Math.abs(diff.value)); }).format(Math.abs(diff.value));

View File

@@ -1,19 +0,0 @@
export const alarmControlPanelColor = (state?: string): string | undefined => {
switch (state) {
case "armed_away":
case "armed_vacation":
case "armed_home":
case "armed_night":
case "armed_custom_bypass":
return "alarm-armed";
case "pending":
return "alarm-pending";
case "arming":
case "disarming":
return "alarm-arming";
case "triggered":
return "alarm-triggered";
default:
return undefined;
}
};

View File

@@ -1,15 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
export const batteryStateColor = (stateObj: HassEntity) => {
const value = Number(stateObj.state);
if (isNaN(value)) {
return "sensor-battery-unknown";
}
if (value >= 70) {
return "sensor-battery-high";
}
if (value >= 30) {
return "sensor-battery-medium";
}
return "sensor-battery-low";
};

View File

@@ -1,21 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
const ALERTING_DEVICE_CLASSES = new Set([
"battery",
"carbon_monoxide",
"gas",
"heat",
"moisture",
"problem",
"safety",
"smoke",
"tamper",
]);
export const binarySensorColor = (stateObj: HassEntity): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
return deviceClass && ALERTING_DEVICE_CLASSES.has(deviceClass)
? "binary-sensor-alerting"
: "binary-sensor";
};

View File

@@ -1,18 +0,0 @@
export const climateColor = (state: string): string | undefined => {
switch (state) {
case "auto":
return "climate-auto";
case "cool":
return "climate-cool";
case "dry":
return "climate-dry";
case "fan_only":
return "climate-fan-only";
case "heat":
return "climate-heat";
case "heat_cool":
return "climate-heat-cool";
default:
return undefined;
}
};

View File

@@ -1,13 +0,0 @@
export const lockColor = (state?: string): string | undefined => {
switch (state) {
case "locked":
return "lock-locked";
case "jammed":
return "lock-jammed";
case "locking":
case "unlocking":
return "lock-pending";
default:
return undefined;
}
};

View File

@@ -1,12 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { batteryStateColor } from "./battery_color";
export const sensorColor = (stateObj: HassEntity): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
if (deviceClass === "battery") {
return batteryStateColor(stateObj);
}
return undefined;
};

View File

@@ -0,0 +1,17 @@
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;
};

View File

@@ -1,37 +1,28 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import { import {
updateIsInstallingFromAttributes,
UPDATE_SUPPORT_PROGRESS, UPDATE_SUPPORT_PROGRESS,
updateIsInstallingFromAttributes,
} from "../../data/update"; } from "../../data/update";
import { HomeAssistant } from "../../types";
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
import { import { formatNumber, isNumericFromAttributes } from "../number/format_number";
formatNumber,
getNumberFormatOptions,
isNumericFromAttributes,
} from "../number/format_number";
import { blankBeforePercent } from "../translations/blank_before_percent";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain";
import { supportsFeatureFromAttributes } from "./supports-feature"; import { supportsFeatureFromAttributes } from "./supports-feature";
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
import { computeDomain } from "./compute_domain";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
locale: FrontendLocaleData, locale: FrontendLocaleData,
entities: HomeAssistant["entities"],
state?: string state?: string
): string => ): string =>
computeStateDisplayFromEntityAttributes( computeStateDisplayFromEntityAttributes(
localize, localize,
locale, locale,
entities,
stateObj.entity_id, stateObj.entity_id,
stateObj.attributes, stateObj.attributes,
state !== undefined ? state : stateObj.state state !== undefined ? state : stateObj.state
@@ -40,7 +31,6 @@ export const computeStateDisplay = (
export const computeStateDisplayFromEntityAttributes = ( export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc, localize: LocalizeFunc,
locale: FrontendLocaleData, locale: FrontendLocaleData,
entities: HomeAssistant["entities"],
entityId: string, entityId: string,
attributes: any, attributes: any,
state: string state: string
@@ -77,13 +67,9 @@ export const computeStateDisplayFromEntityAttributes = (
const unit = !attributes.unit_of_measurement const unit = !attributes.unit_of_measurement
? "" ? ""
: attributes.unit_of_measurement === "%" : attributes.unit_of_measurement === "%"
? blankBeforePercent(locale) + "%" ? "%"
: ` ${attributes.unit_of_measurement}`; : ` ${attributes.unit_of_measurement}`;
return `${formatNumber( return `${formatNumber(state, locale)}${unit}`;
state,
locale,
getNumberFormatOptions({ state, attributes } as HassEntity)
)}${unit}`;
} }
const domain = computeDomain(entityId); const domain = computeDomain(entityId);
@@ -156,12 +142,7 @@ export const computeStateDisplayFromEntityAttributes = (
domain === "number" || domain === "number" ||
domain === "input_number" domain === "input_number"
) { ) {
// Format as an integer if the value and step are integers return formatNumber(state, locale);
return formatNumber(
state,
locale,
getNumberFormatOptions({ state, attributes } as HassEntity)
);
} }
// state of button is a timestamp // state of button is a timestamp
@@ -187,8 +168,7 @@ export const computeStateDisplayFromEntityAttributes = (
// When update is not available and there is no latest_version show "Unavailable" // When update is not available and there is no latest_version show "Unavailable"
return state === "on" return state === "on"
? updateIsInstallingFromAttributes(attributes) ? updateIsInstallingFromAttributes(attributes)
? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) && ? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS)
typeof attributes.in_progress === "number"
? localize("ui.card.update.installing_with_progress", { ? localize("ui.card.update.installing_with_progress", {
progress: attributes.in_progress, progress: attributes.in_progress,
}) })
@@ -199,13 +179,7 @@ export const computeStateDisplayFromEntityAttributes = (
: localize("ui.card.update.up_to_date"); : localize("ui.card.update.up_to_date");
} }
const entity = entities[entityId] as EntityRegistryEntry | undefined;
return ( return (
(entity?.translation_key &&
localize(
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
)) ||
// Return device class translation // Return device class translation
(attributes.device_class && (attributes.device_class &&
localize( localize(

View File

@@ -12,10 +12,8 @@ import {
mdiCircle, mdiCircle,
mdiWindowShutter, mdiWindowShutter,
mdiWindowShutterOpen, mdiWindowShutterOpen,
mdiBlindsHorizontal, mdiBlinds,
mdiBlindsHorizontalClosed, mdiBlindsOpen,
mdiRollerShade,
mdiRollerShadeClosed,
mdiWindowClosed, mdiWindowClosed,
mdiWindowOpen, mdiWindowOpen,
mdiArrowExpandHorizontal, mdiArrowExpandHorizontal,
@@ -81,16 +79,6 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
return mdiCurtains; return mdiCurtains;
} }
case "blind": case "blind":
switch (state) {
case "opening":
return mdiArrowUpBox;
case "closing":
return mdiArrowDownBox;
case "closed":
return mdiBlindsHorizontalClosed;
default:
return mdiBlindsHorizontal;
}
case "shade": case "shade":
switch (state) { switch (state) {
case "opening": case "opening":
@@ -98,9 +86,9 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
case "closing": case "closing":
return mdiArrowDownBox; return mdiArrowDownBox;
case "closed": case "closed":
return mdiRollerShadeClosed; return mdiBlinds;
default: default:
return mdiRollerShade; return mdiBlindsOpen;
} }
case "window": case "window":
switch (state) { switch (state) {

View File

@@ -25,8 +25,6 @@ import {
mdiPackageUp, mdiPackageUp,
mdiPowerPlug, mdiPowerPlug,
mdiPowerPlugOff, mdiPowerPlugOff,
mdiAudioVideo,
mdiAudioVideoOff,
mdiRestart, mdiRestart,
mdiSpeaker, mdiSpeaker,
mdiSpeakerOff, mdiSpeakerOff,
@@ -161,13 +159,6 @@ export const domainIconWithoutDefault = (
default: default:
return mdiTelevision; return mdiTelevision;
} }
case "receiver":
switch (compareState) {
case "off":
return mdiAudioVideoOff;
default:
return mdiAudioVideo;
}
default: default:
switch (compareState) { switch (compareState) {
case "playing": case "playing":

View File

@@ -1,14 +1,10 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { supportsFeature } from "./supports-feature"; import { supportsFeature } from "./supports-feature";
export type FeatureClassNames<T extends number = number> = Partial<
Record<T, string>
>;
// Expects classNames to be an object mapping feature-bit -> className // Expects classNames to be an object mapping feature-bit -> className
export const featureClassNames = ( export const featureClassNames = (
stateObj: HassEntity, stateObj: HassEntity,
classNames: FeatureClassNames classNames: { [feature: number]: string }
) => { ) => {
if (!stateObj || !stateObj.attributes.supported_features) { if (!stateObj || !stateObj.attributes.supported_features) {
return ""; return "";

View File

@@ -37,7 +37,6 @@ const FIXED_DOMAIN_STATES = {
siren: ["on", "off"], siren: ["on", "off"],
sun: ["above_horizon", "below_horizon"], sun: ["above_horizon", "below_horizon"],
switch: ["on", "off"], switch: ["on", "off"],
timer: ["active", "idle", "paused"],
update: ["on", "off"], update: ["on", "off"],
vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"], vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"],
weather: [ weather: [
@@ -240,13 +239,10 @@ export const getStates = (
} }
break; break;
case "light": case "light":
if (attribute === "effect" && state.attributes.effect_list) { if (attribute === "effect") {
result.push(...state.attributes.effect_list); result.push(...state.attributes.effect_list);
} else if ( } else if (attribute === "color_mode") {
attribute === "color_mode" && result.push(...state.attributes.color_modes);
state.attributes.supported_color_modes
) {
result.push(...state.attributes.supported_color_modes);
} }
break; break;
case "media_player": case "media_player":

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