Compare commits

..

1 Commits

Author SHA1 Message Date
Zack
026d027386 Update Struct for Buttons Footer 2022-05-25 23:13:27 -05:00
950 changed files with 214958 additions and 42285 deletions

View File

@@ -1,8 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
time: "06:00"
open-pull-requests-limit: 10

View File

@@ -11,18 +11,17 @@ on:
- master
env:
NODE_VERSION: 16
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=6144
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@@ -44,9 +43,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@@ -63,9 +62,9 @@ jobs:
needs: [lint, test]
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@@ -82,9 +81,9 @@ jobs:
needs: [lint, test]
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v1

View File

@@ -6,7 +6,7 @@ on:
- dev
env:
NODE_VERSION: 16
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=6144
jobs:
@@ -14,9 +14,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@@ -26,10 +26,10 @@ jobs:
CI: true
- name: Build Demo
run: ./node_modules/.bin/gulp build-demo
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to Netlify
run: npx netlify-cli deploy --dir=demo/dist --prod
uses: netlify/actions/cli@master
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
with:
args: deploy --dir=demo/dist --prod

View File

@@ -9,7 +9,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4.0.0
- uses: dessant/lock-threads@v2.0.1
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: "30"

View File

@@ -1,72 +0,0 @@
name: Nightly
on:
workflow_dispatch:
schedule:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.10"
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
permissions:
actions: none
jobs:
nightly:
name: Nightly
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download translations
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Bump version
run: script/version_bump.js nightly
- name: Build nightly Python wheels
run: |
pip install build
yarn install
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/build_frontend
rm -rf dist home_assistant_frontend.egg-info
python3 -m build
- name: Archive translations
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v3
with:
name: translations
path: translations.tar.gz
if-no-files-found: error

View File

@@ -6,8 +6,8 @@ on:
- published
env:
PYTHON_VERSION: "3.10"
NODE_VERSION: 16
PYTHON_VERSION: 3.8
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@@ -21,21 +21,21 @@ jobs:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@@ -52,11 +52,11 @@ jobs:
python3 -m pip install twine build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v0.1.15
uses: softprops/action-gh-release@v0.1.14
with:
files: |
dist/*.whl
@@ -74,11 +74,33 @@ jobs:
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2022.10.1
- name: Upload requirements.txt
uses: actions/upload-artifact@v2
with:
abi: cp310
tag: musllinux_1_2
arch: amd64
name: requirements
path: ./requirements.txt
build-wheels:
name: Build wheels for ${{ matrix.arch }}
needs: wheels-init
runs-on: ubuntu-latest
strategy:
matrix:
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
tag:
- "3.9-alpine3.14"
steps:
- name: Download requirements.txt
uses: actions/download-artifact@v2
with:
name: requirements
- name: Build wheels
uses: home-assistant/wheels@master
with:
tag: ${{ matrix.tag }}
arch: ${{ matrix.arch }}
wheels-host: ${{ secrets.WHEELS_HOST }}
wheels-key: ${{ secrets.WHEELS_KEY }}
wheels-user: wheels
requirements: "requirements.txt"

View File

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

View File

@@ -8,7 +8,7 @@ on:
- src/translations/en.json
env:
NODE_VERSION: 16
NODE_VERSION: 14
jobs:
upload:
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Upload Translations
run: |

17
.gitignore vendored
View File

@@ -2,20 +2,19 @@
.reify-cache
# build
build/
dist/
/hass_frontend/
/translations/
build
hass_frontend/*
dist
# yarn
.yarn/**
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
/node_modules/
node_modules/*
yarn-error.log
npm-debug.log
@@ -27,11 +26,11 @@ npm-debug.log
# venv stuff
pyvenv.cfg
pip-selfcheck.json
/venv/
venv/*
.venv
# vscode
.vscode/**
.vscode/*
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/tasks.json
@@ -46,4 +45,4 @@ src/cast/dev_const.ts
.tool-versions
# Home Assistant config
/config/
/config

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn run lint-staged --relative --shell "/bin/bash"

2
.nvmrc
View File

@@ -1 +1 @@
16
14

10
.vscode/tasks.json vendored
View File

@@ -181,7 +181,7 @@
{
"label": "Run HA Core for Supervisor in devcontainer",
"type": "shell",
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core",
"command": "HASSIO=${input:supervisorHost} HASSIO_TOKEN=${input:supervisorToken} script/core",
"isBackground": true,
"group": {
"kind": "build",
@@ -191,13 +191,7 @@
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Setup and fetch nightly translations",
"type": "gulp",
"task": "setup-and-fetch-nightly-translations",
"problemMatcher": []
}
}
],
"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
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",
"rules": {
"no-console": "off",
"import/no-extraneous-dependencies": "off",
"import/extensions": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"@typescript-eslint/no-var-requires": "off",
"prefer-arrow-callback": "off"
"import/no-extraneous-dependencies": 0,
"global-require": 0
}
}

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require("fs");
const path = require("path");
const paths = require("./paths.js");
@@ -26,7 +27,7 @@ module.exports = {
version() {
const version = fs
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8")
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/);
.match(/version\W+=\W"(\d{8}\.\d)"/);
if (!version) {
throw Error("Version not found");
}

View File

@@ -1,12 +1,17 @@
const del = require("del");
const gulp = require("gulp");
const fs = require("fs/promises");
const fs = require("fs");
const mapStream = require("map-stream");
const inDirFrontend = "translations/frontend";
const inDirBackend = "translations/backend";
const downloadDir = "translations/downloads";
const srcMeta = "src/translations/translationMetadata.json";
const encoding = "utf8";
const tasks = [];
function hasHtml(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
gulp.task("check-translations-html", function () {
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
let taskName = "clean-downloaded-translations";
gulp.task(taskName, function () {
return del([`${downloadDir}/**`]);
});
tasks.push(taskName);
gulp.task("check-all-files-exist", async function () {
const file = await fs.readFile(srcMeta, { encoding });
taskName = "check-translations-html";
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 writings = [];
Object.keys(meta).forEach((lang) => {
writings.push(
fs.writeFile(`${inDirFrontend}/${lang}.json`, JSON.stringify({}), {
flag: "wx",
}),
fs.writeFile(`${inDirBackend}/${lang}.json`, JSON.stringify({}), {
flag: "wx",
})
);
if (!fs.existsSync(`${inDirFrontend}/${lang}.json`)) {
fs.writeFileSync(`${inDirFrontend}/${lang}.json`, JSON.stringify({}));
}
if (!fs.existsSync(`${inDirBackend}/${lang}.json`)) {
fs.writeFileSync(`${inDirBackend}/${lang}.json`, JSON.stringify({}));
}
});
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(
"check-downloaded-translations",
gulp.series("check-translations-html", "check-all-files-exist")
taskName,
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
/* eslint-disable import/no-dynamic-require */
/* eslint-disable global-require */
const gulp = require("gulp");
const fs = require("fs-extra");
const path = require("path");
@@ -89,9 +91,7 @@ gulp.task("gen-pages-prod", (done) => {
});
gulp.task("gen-index-app-dev", (done) => {
let latestAppJS;
let latestCoreJS;
let latestCustomPanelJS;
let latestAppJS, latestCoreJS, latestCustomPanelJS;
if (env.useWDS()) {
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
const gulp = require("gulp");
const fs = require("fs");
@@ -40,7 +41,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
}
processed.add(pageId);
const [category] = pageId.split("/", 2);
const [category, name] = pageId.split("/", 2);
const demoFile = path.resolve(pageDir, `${pageId}.ts`);
const descriptionFile = path.resolve(pageDir, `${pageId}.markdown`);

View File

@@ -156,12 +156,3 @@ gulp.task("gen-icons-json", (done) => {
done();
});
gulp.task("gen-dummy-icons-json", (done) => {
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
fs.writeFileSync(path.resolve(OUTPUT_DIR, "iconList.json"), "[]");
done();
});

View File

@@ -9,7 +9,6 @@ require("./compress.js");
require("./rollup.js");
require("./gather-static.js");
require("./translations.js");
require("./gen-icons-json.js");
gulp.task(
"develop-hassio",
@@ -18,7 +17,6 @@ gulp.task(
process.env.NODE_ENV = "development";
},
"clean-hassio",
"gen-dummy-icons-json",
"gen-index-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
@@ -35,7 +33,6 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean-hassio",
"gen-dummy-icons-json",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
// Generate service worker.
// Based on manifest, create a file with the content as service_worker.js
/* eslint-disable import/no-dynamic-require */
/* eslint-disable global-require */
const gulp = require("gulp");
const path = require("path");
const fs = require("fs-extra");

View File

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

View File

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

View File

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

View File

@@ -1,30 +1 @@
[
{
"path": "M20,20H7A2,2 0 0,1 5,18V8.94L2.23,5.64C2.09,5.47 2,5.24 2,5A1,1 0 0,1 3,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20M8.5,7A0.5,0.5 0 0,0 8,7.5V8.5A0.5,0.5 0 0,0 8.5,9H18.5A0.5,0.5 0 0,0 19,8.5V7.5A0.5,0.5 0 0,0 18.5,7H8.5M8.5,11A0.5,0.5 0 0,0 8,11.5V12.5A0.5,0.5 0 0,0 8.5,13H18.5A0.5,0.5 0 0,0 19,12.5V11.5A0.5,0.5 0 0,0 18.5,11H8.5M8.5,15A0.5,0.5 0 0,0 8,15.5V16.5A0.5,0.5 0 0,0 8.5,17H13.5A0.5,0.5 0 0,0 14,16.5V15.5A0.5,0.5 0 0,0 13.5,15H8.5Z",
"name": "android-messages"
},
{
"path": "M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M20,12L17.5,10.5L15,12V4H20V12Z",
"name": "book-variant-multiple"
},
{
"path": "M21,14H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10L8,21V22H16V21L14,18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z",
"name": "desktop-mac"
},
{
"path": "M21,14V4H3V14H21M21,2A2,2 0 0,1 23,4V16A2,2 0 0,1 21,18H14L16,21V22H8V21L10,18H3C1.89,18 1,17.1 1,16V4C1,2.89 1.89,2 3,2H21M4,5H15V10H4V5M16,5H20V7H16V5M20,8V13H16V8H20M4,11H9V13H4V11M10,11H15V13H10V11Z",
"name": "desktop-mac-dashboard"
},
{
"path": "M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z",
"name": "discord"
},
{
"path": "M8.06,7.78C7.5,7.78 7.17,7.73 7.08,7.64L6.66,13.73C7.19,14.05 7.88,14.3 8.72,14.5C9.56,14.71 10.78,14.77 12.38,14.67C13.97,14.58 15.63,14.23 17.34,13.64L16.55,4.22C15.67,5.09 14.38,5.91 12.66,6.66C11.13,7.31 9.81,7.69 8.72,7.78H8.06M7.97,5.34C7.28,5.94 7,6.34 7.13,6.56C7.22,6.78 7.7,6.84 8.58,6.75C9.67,6.66 10.91,6.31 12.28,5.72C13.22,5.31 14.03,4.88 14.72,4.41C15.41,3.94 15.88,3.55 16.13,3.23C16.38,2.92 16.47,2.7 16.41,2.58C16.34,2.42 16.03,2.34 15.47,2.34C14.34,2.34 12.94,2.7 11.25,3.42C9.81,4.05 8.72,4.69 7.97,5.34M17.34,2.2C17.41,2.33 17.44,2.47 17.44,2.63L18.61,17C18.61,18.73 18,20.09 16.83,21.07C15.64,22.05 14.03,22.55 12,22.55C10,22.55 8.4,22.04 7.2,21C6,20 5.39,18.64 5.39,16.92L6.09,6.47C6.09,6.22 6.2,5.94 6.42,5.63C6.64,5.31 6.84,5.06 7.03,4.88L7.36,4.59C8.33,3.78 9.5,3.08 10.88,2.5C11.81,2.08 12.73,1.77 13.62,1.57C14.5,1.37 15.3,1.3 16,1.38C16.71,1.46 17.16,1.73 17.34,2.2Z",
"name": "google-home"
},
{
"path": "M19.25,19H4.75V3H19.25M14,22H10V21H14M18,0H6A3,3 0 0,0 3,3V21A3,3 0 0,0 6,24H18A3,3 0 0,0 21,21V3A3,3 0 0,0 18,0Z",
"name": "tablet-android"
}
]
[]

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
@@ -75,7 +76,7 @@ const createWebpackConfig = ({
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
},
plugins: [
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
new WebpackBar({ fancy: !isProdBuild }),
new WebpackManifestPlugin({
// Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
@@ -102,6 +103,7 @@ const createWebpackConfig = ({
? path.resolve(context, resource)
: require.resolve(resource);
} catch (err) {
// eslint-disable-next-line no-console
console.error(
"Error in Home Assistant ignore plugin",
resource,

View File

@@ -1,9 +0,0 @@
# These redirects are handled by Netlify
#
# Some custom cards are not prefixing the instance URL when fetching data
# and can end up fetching the data from the Cast domain instead of HA.
# This will make sure that some common ones are replaced with a placeholder.
/api/camera_proxy/* /images/google-nest-hub.png
/api/camera_proxy_stream/* /images/google-nest-hub.png
/api/media_player_proxy/* /images/google-nest-hub.png

View File

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

View File

@@ -88,7 +88,7 @@ class HcCast extends LitElement {
>
${(this.lovelaceConfig
? this.lovelaceConfig.views
: [generateDefaultViewConfig({}, {}, {}, {}, () => "")]
: [generateDefaultViewConfig([], [], [], {}, () => "")]
).map(
(view, idx) => html`
<paper-icon-item
@@ -181,7 +181,7 @@ class HcCast extends LitElement {
private async _handlePickView(ev: Event) {
const path = (ev.currentTarget as any).getAttribute("data-path");
await ensureConnectedCastSession(this.castManager!, this.auth!);
castSendShowLovelaceView(this.castManager, path, this.auth.data.hassUrl);
castSendShowLovelaceView(this.castManager, path);
}
private async _handleLogout() {

View File

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

View File

@@ -33,6 +33,7 @@ import { castContext } from "../cast_context";
import "./hc-launch-screen";
let resourcesLoaded = false;
@customElement("hc-main")
export class HcMain extends HassElement {
@state() private _showDemo = false;
@@ -45,8 +46,6 @@ export class HcMain extends HassElement {
@state() private _urlPath?: string | null;
private _hassUUID?: string;
private _unsubLovelace?: UnsubscribeFunc;
public processIncomingMessage(msg: HassMessage) {
@@ -126,7 +125,6 @@ export class HcMain extends HassElement {
if (this.hass) {
status.hassUrl = this.hass.auth.data.hassUrl;
status.hassUUID = this._hassUUID;
status.lovelacePath = this._lovelacePath;
status.urlPath = this._urlPath;
}
@@ -165,18 +163,6 @@ export class HcMain extends HassElement {
};
private async _handleGetStatusMessage(msg: GetStatusMessage) {
if (
(this.hass && msg.hassUUID && msg.hassUUID !== this._hassUUID) ||
(this.hass && msg.hassUrl && msg.hassUrl !== this.hass.auth.data.hassUrl)
) {
this._error = "Not connected to the same Home Assistant instance.";
this._sendError(
ReceiverErrorCode.WRONG_INSTANCE,
this._error,
msg.senderId!
);
}
this._sendStatus(msg.senderId!);
}
@@ -193,7 +179,6 @@ export class HcMain extends HassElement {
expires_in: 0,
}),
});
this._hassUUID = msg.hassUUID;
} catch (err: any) {
const errorMessage = this._getErrorMessage(err);
this._error = errorMessage;
@@ -224,29 +209,9 @@ export class HcMain extends HassElement {
if (!this.hass) {
this._sendStatus(msg.senderId!);
this._error = "Cannot show Lovelace because we're not connected.";
this._sendError(
ReceiverErrorCode.NOT_CONNECTED,
this._error,
msg.senderId!
);
this._sendError(ReceiverErrorCode.NOT_CONNECTED, this._error);
return;
}
if (
(msg.hassUUID && msg.hassUUID !== this._hassUUID) ||
(msg.hassUrl && msg.hassUrl !== this.hass.auth.data.hassUrl)
) {
this._sendStatus(msg.senderId!);
this._error =
"Cannot show Lovelace because we're not connected to the same Home Assistant instance.";
this._sendError(
ReceiverErrorCode.WRONG_INSTANCE,
this._error,
msg.senderId!
);
return;
}
this._error = undefined;
if (msg.urlPath === "lovelace") {
msg.urlPath = null;

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: {
hidden: true,
radius: 50,
friendly_name: "School",
friendly_name: "Skolan",
icon: "mdi:school",
},
},
@@ -137,7 +137,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
state: "73",
attributes: {
unit_of_measurement: "%",
friendly_name: "Oskar battery",
friendly_name: "oskar batteri",
device_class: "battery",
},
},
@@ -146,7 +146,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
state: "88",
attributes: {
unit_of_measurement: "%",
friendly_name: "Bella battery",
friendly_name: "bella batteri",
device_class: "battery",
},
},
@@ -154,7 +154,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
entity_id: "binary_sensor.unifi_camera",
state: "off",
attributes: {
friendly_name: "Motion sensor camera",
friendly_name: "R\u00f6relsesensor kamera",
icon: "mdi:walk",
},
},
@@ -707,7 +707,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
},
],
cloudiness: 25,
friendly_name: "Weather",
friendly_name: "V\u00e4der",
},
},
"binary_sensor.ubiquiti_switch": {
@@ -731,7 +731,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
round_trip_time_max: "0.626",
round_trip_time_mdev: "",
round_trip_time_min: "0.358",
friendly_name: "Entrance camera",
friendly_name: "Entr\u00e9 kamera",
device_class: "connectivity",
icon: "mdi:cctv",
},
@@ -797,7 +797,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: {
battery_level: 34,
on: true,
friendly_name: "Porch motion sensor",
friendly_name: "altan_motion_sensor",
device_class: "motion",
},
},
@@ -807,7 +807,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: {
battery_level: 88,
on: true,
friendly_name: "Back door sensor",
friendly_name: "Altand\u00f6rren sensor",
device_class: "opening",
icon: "mdi:door",
},
@@ -818,7 +818,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: {
battery_level: 74,
on: true,
friendly_name: "Bathroom motion sensor",
friendly_name: "badrumssensor",
device_class: "motion",
},
},
@@ -829,7 +829,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 47,
on: true,
dark: true,
friendly_name: "Basement motion sensor",
friendly_name: "R\u00f6relsesensor k\u00e4llaren 1",
device_class: "motion",
icon: "mdi:walk",
},
@@ -841,7 +841,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 60,
on: true,
dark: true,
friendly_name: "Laundy room motion sensor",
friendly_name: "R\u00f6relsesensor tv\u00e4ttstugan",
device_class: "motion",
icon: "mdi:walk",
},
@@ -863,7 +863,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
attributes: {
battery_level: 60,
on: true,
friendly_name: "Pantry motion sensor",
friendly_name: "R\u00f6relsesensor skafferiet",
device_class: "motion",
icon: "mdi:walk",
},
@@ -875,7 +875,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 60,
on: true,
dark: true,
friendly_name: "Stair motion sensor",
friendly_name: "R\u00f6relsesensor k\u00e4llaren 2",
device_class: "motion",
icon: "mdi:walk",
},
@@ -887,7 +887,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
battery_level: 47,
on: true,
dark: true,
friendly_name: "Bench sensor",
friendly_name: "B\u00e4nksensor",
device_class: "motion",
},
},

View File

@@ -277,7 +277,7 @@ export const demoLovelaceKernehed: DemoConfig["lovelace"] = () => ({
],
show_header_toggle: false,
type: "entities",
title: "Bandwidth",
title: "Bandbredd",
},
// {
// title: "Updater",

View File

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

View File

@@ -1,4 +1,5 @@
// Compat needs to be first import
import "../../src/resources/compatibility";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate";
import {
@@ -6,25 +7,22 @@ import {
provideHass,
} from "../../src/fake_data/provide_hass";
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import "../../src/resources/compatibility";
import { HomeAssistant } from "../../src/types";
import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries";
import { mockEnergy } from "./stubs/energy";
import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFrontend } from "./stubs/frontend";
import { mockHistory } from "./stubs/history";
import { mockLovelace } from "./stubs/lovelace";
import { mockMediaPlayer } from "./stubs/media_player";
import { mockPersistentNotification } from "./stubs/persistent_notification";
import { mockRecorder } from "./stubs/recorder";
import { mockShoppingList } from "./stubs/shopping_list";
import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations";
import { mockEnergy } from "./stubs/energy";
import { mockConfig } from "./stubs/config";
import { energyEntities } from "./stubs/entities";
class HaDemo extends HomeAssistantAppEl {
protected async _initializeHass() {
@@ -46,7 +44,6 @@ class HaDemo extends HomeAssistantAppEl {
mockAuth(hass);
mockTranslations(hass);
mockHistory(hass);
mockRecorder(hass);
mockShoppingList(hass);
mockSystemLog(hass);
mockTemplate(hass);
@@ -54,40 +51,8 @@ class HaDemo extends HomeAssistantAppEl {
mockMediaPlayer(hass);
mockFrontend(hass);
mockEnergy(hass);
mockConfig(hass);
mockPersistentNotification(hass);
mockConfigEntries(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.co2_intensity",
id: "sensor.co2_intensity",
name: null,
icon: null,
platform: "co2signal",
hidden_by: null,
entity_category: null,
has_entity_name: false,
unique_id: "co2_intensity",
},
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.grid_fossil_fuel_percentage",
id: "sensor.co2_intensity",
name: null,
icon: null,
platform: "co2signal",
hidden_by: null,
entity_category: null,
has_entity_name: false,
unique_id: "grid_fossil_fuel_percentage",
},
]);
hass.addEntities(energyEntities());
@@ -122,9 +87,3 @@ class HaDemo extends HomeAssistantAppEl {
}
customElements.define("ha-demo", HaDemo);
declare global {
interface HTMLElementTagNameMap {
"ha-demo": HaDemo;
}
}

41
demo/src/stubs/config.ts Normal file
View File

@@ -0,0 +1,41 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockAPI("config/config_entries/entry", () => [
{
entry_id: "co2signal",
domain: "co2signal",
title: "CO2 Signal",
source: "user",
state: "loaded",
supports_options: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
},
]);
hass.mockWS("config/entity_registry/list", () => [
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.co2_intensity",
name: null,
icon: null,
platform: "co2signal",
},
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.grid_fossil_fuel_percentage",
name: null,
icon: null,
platform: "co2signal",
},
]);
};

View File

@@ -1,20 +0,0 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfigEntries = (hass: MockHomeAssistant) => {
hass.mockWS("config_entries/get", () => [
{
entry_id: "co2signal",
domain: "co2signal",
title: "CO2 Signal",
source: "user",
state: "loaded",
supports_options: false,
supports_remove_device: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
},
]);
};

View File

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

View File

@@ -4,6 +4,4 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEntityRegistry = (
hass: MockHomeAssistant,
data: EntityRegistryEntry[] = []
) => {
hass.mockWS("config/entity_registry/list", () => data);
};
) => hass.mockWS("config/entity_registry/list", () => data);

View File

@@ -1,4 +1,12 @@
import {
addDays,
addHours,
addMonths,
differenceInHours,
endOfDay,
} from "date-fns/esm";
import { HassEntity } from "home-assistant-js-websocket";
import { StatisticValue } from "../../../src/data/history";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
interface HistoryQueryParams {
@@ -64,6 +72,331 @@ const generateHistory = (state, deltas) => {
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) => {
mockHass.mockAPI(
new RegExp("history/period/.+"),
@@ -133,4 +466,42 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
return results;
}
);
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.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -8,7 +8,7 @@ module.exports = [
{
category: "lovelace",
// Label for in the sidebar
header: "Dashboards",
header: "Lovelace",
// Specify order of pages. Any pages in the category folder but not listed here will
// automatically be added after the pages listed here.
pages: ["introduction"],
@@ -34,7 +34,7 @@ module.exports = [
},
{
category: "misc",
header: "Miscellaneous",
header: "Miscelaneous",
},
{
category: "brand",

View File

@@ -5,7 +5,7 @@ import { html, css, LitElement, PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../src/components/ha-icon-button";
import "../../src/managers/notification-manager";
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
import "../../src/components/ha-expansion-panel";
import { haStyle } from "../../src/resources/styles";
import { PAGES, SIDEBAR } from "../build/import-pages";
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
@@ -174,10 +174,9 @@ class HaGallery extends LitElement {
const menuItem = this.shadowRoot!.querySelector(
`a[href="#${this._page}"]`
)!;
// Make sure section is expanded
if (menuItem.parentElement instanceof HaExpansionPanel) {
menuItem.parentElement.expanded = true;
if (menuItem.parentElement instanceof HTMLDetailsElement) {
menuItem.parentElement.open = true;
}
}

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

@@ -1,9 +1,7 @@
import { dump } from "js-yaml";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import { Action } from "../../../../src/data/script";
import { describeAction } from "../../../../src/data/script_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
@@ -90,15 +88,6 @@ const ACTIONS = [
then: [{ delay: "00:00:01" }],
else: [{ delay: "00:00:05" }],
},
{
if: [{ condition: "state" }],
then: [{ delay: "00:00:01" }],
},
{
if: [{ condition: "state" }, { condition: "state" }],
then: [{ delay: "00:00:01" }],
else: [{ delay: "00:00:05" }],
},
{
choose: [
{
@@ -114,38 +103,16 @@ const ACTIONS = [
},
];
const initialAction: Action = {
service: "light.turn_on",
target: {
entity_id: "light.kitchen",
},
};
@customElement("demo-automation-describe-action")
export class DemoAutomationDescribeAction extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
@state() _action = initialAction;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-card header="Actions">
<div class="action">
<span>
${this._action
? describeAction(this.hass, this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
label="Action Config"
.defaultValue=${initialAction}
@value-changed=${this._dataChanged}
></ha-yaml-editor>
</div>
${ACTIONS.map(
(conf) => html`
<div class="action">
@@ -165,11 +132,6 @@ export class DemoAutomationDescribeAction extends LitElement {
hass.addEntities(ENTITIES);
}
private _dataChanged(ev: CustomEvent): void {
ev.stopPropagation();
this._action = ev.detail.isValid ? ev.detail.value : undefined;
}
static get styles() {
return css`
ha-card {
@@ -185,9 +147,6 @@ export class DemoAutomationDescribeAction extends LitElement {
span {
margin-right: 16px;
}
ha-yaml-editor {
width: 50%;
}
`;
}
}

View File

@@ -1,82 +1,31 @@
import { dump } from "js-yaml";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import { Condition } from "../../../../src/data/automation";
import { describeCondition } from "../../../../src/data/automation_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
const ENTITIES = [
getEntity("light", "kitchen", "on", {
friendly_name: "Kitchen Light",
}),
getEntity("device_tracker", "person", "home", {
friendly_name: "Person",
}),
getEntity("zone", "home", "", {
friendly_name: "Home",
}),
];
const conditions = [
{ condition: "and" },
{ condition: "not" },
{ condition: "or" },
{ condition: "state", entity_id: "light.kitchen", state: "on" },
{
condition: "numeric_state",
entity_id: "light.kitchen",
attribute: "brightness",
below: 80,
above: 20,
},
{ condition: "state" },
{ condition: "numeric_state" },
{ condition: "sun", after: "sunset" },
{ condition: "sun", after: "sunrise", offset: "-01:00" },
{ condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" },
{ condition: "trigger", id: "motion" },
{ condition: "sun", after: "sunrise" },
{ condition: "zone" },
{ condition: "time" },
{ condition: "template" },
];
const initialCondition: Condition = {
condition: "state",
entity_id: "light.kitchen",
state: "on",
};
@customElement("demo-automation-describe-condition")
export class DemoAutomationDescribeCondition extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
@state() _condition = initialCondition;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-card header="Conditions">
<div class="condition">
<span>
${this._condition
? describeCondition(this._condition, this.hass)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
label="Condition Config"
.defaultValue=${initialCondition}
@value-changed=${this._dataChanged}
></ha-yaml-editor>
</div>
${conditions.map(
(conf) => html`
<div class="condition">
<span>${describeCondition(conf as any, this.hass)}</span>
<span>${describeCondition(conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`
@@ -85,18 +34,6 @@ export class DemoAutomationDescribeCondition extends LitElement {
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
private _dataChanged(ev: CustomEvent): void {
ev.stopPropagation();
this._condition = ev.detail.isValid ? ev.detail.value : undefined;
}
static get styles() {
return css`
ha-card {
@@ -112,9 +49,6 @@ export class DemoAutomationDescribeCondition extends LitElement {
span {
margin-right: 16px;
}
ha-yaml-editor {
width: 50%;
}
`;
}
}

View File

@@ -1,92 +1,34 @@
import { dump } from "js-yaml";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import { Trigger } from "../../../../src/data/automation";
import { describeTrigger } from "../../../../src/data/automation_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
const ENTITIES = [
getEntity("light", "kitchen", "on", {
friendly_name: "Kitchen Light",
}),
getEntity("person", "person", "", {
friendly_name: "Person",
}),
getEntity("zone", "home", "", {
friendly_name: "Home",
}),
];
const triggers = [
{ platform: "state", entity_id: "light.kitchen", from: "off", to: "on" },
{ platform: "state" },
{ platform: "mqtt" },
{
platform: "geo_location",
source: "test_source",
zone: "zone.home",
event: "enter",
},
{ platform: "homeassistant", event: "start" },
{
platform: "numeric_state",
entity_id: "light.kitchen",
attribute: "brightness",
below: 80,
above: 20,
},
{ platform: "sun", event: "sunset" },
{ platform: "geo_location" },
{ platform: "homeassistant" },
{ platform: "numeric_state" },
{ platform: "sun" },
{ platform: "time_pattern" },
{ platform: "webhook" },
{
platform: "zone",
entity_id: "person.person",
zone: "zone.home",
event: "enter",
},
{ platform: "zone" },
{ platform: "tag" },
{ platform: "time", at: "15:32" },
{ platform: "time" },
{ platform: "template" },
{ platform: "event", event_type: "homeassistant_started" },
{ platform: "event" },
];
const initialTrigger: Trigger = {
platform: "state",
entity_id: "light.kitchen",
};
@customElement("demo-automation-describe-trigger")
export class DemoAutomationDescribeTrigger extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
@state() _trigger = initialTrigger;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-card header="Triggers">
<div class="trigger">
<span>
${this._trigger
? describeTrigger(this._trigger, this.hass)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
label="Trigger Config"
.defaultValue=${initialTrigger}
@value-changed=${this._dataChanged}
></ha-yaml-editor>
</div>
${triggers.map(
(conf) => html`
<div class="trigger">
<span>${describeTrigger(conf as any, this.hass)}</span>
<span>${describeTrigger(conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`
@@ -95,18 +37,6 @@ export class DemoAutomationDescribeTrigger extends LitElement {
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
private _dataChanged(ev: CustomEvent): void {
ev.stopPropagation();
this._trigger = ev.detail.isValid ? ev.detail.value : undefined;
}
static get styles() {
return css`
ha-card {
@@ -122,9 +52,6 @@ export class DemoAutomationDescribeTrigger extends LitElement {
span {
margin-right: 16px;
}
ha-yaml-editor {
width: 50%;
}
`;
}
}

View File

@@ -1,5 +1,5 @@
/* 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 { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
@@ -47,8 +47,6 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
class DemoHaAutomationEditorAction extends LitElement {
@state() private hass!: HomeAssistant;
@state() private _disabled = false;
private data: any = SCHEMAS.map((info) => info.actions);
constructor() {
@@ -69,15 +67,6 @@ class DemoHaAutomationEditorAction extends LitElement {
this.requestUpdate();
};
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(
(info, sampleIdx) => html`
<demo-black-white-row
@@ -92,7 +81,6 @@ class DemoHaAutomationEditorAction extends LitElement {
.hass=${this.hass}
.actions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${valueChanged}
></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 {

View File

@@ -1,5 +1,5 @@
/* 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 { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
@@ -83,8 +83,6 @@ const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
class DemoHaAutomationEditorCondition extends LitElement {
@state() private hass!: HomeAssistant;
@state() private _disabled = false;
private data: any = SCHEMAS.map((info) => info.conditions);
constructor() {
@@ -105,15 +103,6 @@ class DemoHaAutomationEditorCondition extends LitElement {
this.requestUpdate();
};
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(
(info, sampleIdx) => html`
<demo-black-white-row
@@ -128,7 +117,6 @@ class DemoHaAutomationEditorCondition extends LitElement {
.hass=${this.hass}
.conditions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${valueChanged}
></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 {

View File

@@ -1,5 +1,5 @@
/* 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 { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
@@ -107,8 +107,6 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
class DemoHaAutomationEditorTrigger extends LitElement {
@state() private hass!: HomeAssistant;
@state() private _disabled = false;
private data: any = SCHEMAS.map((info) => info.triggers);
constructor() {
@@ -129,15 +127,6 @@ class DemoHaAutomationEditorTrigger extends LitElement {
this.requestUpdate();
};
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(
(info, sampleIdx) => html`
<demo-black-white-row
@@ -152,7 +141,6 @@ class DemoHaAutomationEditorTrigger extends LitElement {
.hass=${this.hass}
.triggers=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${valueChanged}
></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 {

View File

@@ -2,6 +2,8 @@
title: "Logo"
---
![Using our logo](/images/using-our-logo.png)
# 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.

View File

@@ -1,15 +1,8 @@
---
title: Alert
title: Alerts
subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
---
<style>
ha-alert {
display: block;
margin: 4px 0;
}
</style>
# Alert `<ha-alert>`
The alert offers four severity levels that set a distinctive icon and color.

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,4 +1,3 @@
---
title: Bar
subtitle: Can be used to communicate progress of a task.
title: Progress Bars
---

View File

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

View File

@@ -1,32 +0,0 @@
---
title: Dialog
subtitle: Dialogs provide important prompts in a user flow.
---
# Material Design 3
Our dialogs are based on the latest version of Material Design. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
# Highlighted guidelines
## Content
* A best practice is to always use a title, even if it is optional by Material guidelines.
* People mainly read the title and a button. Put the most important information in those two.
* Try to avoid user generated content in the title, this could make the title unreadable long.
* If users become unsure, they read the description. Make sure this explains what will happen.
* Strive for minimalism.
## Buttons and X-icon
* Keep the labels short, for example `Save`, `Delete`, `Enable`.
* Dialog with actions must always have a discard button. On desktop a `Cancel` button and X-icon, on mobile only the X-icon.
* Destructive actions should be a red warning button.
* Alert or confirmation dialogs only have buttons and no X-icon.
* Try to avoid three buttons in one dialog. Especially when you leave the dialog task unfinished.
## Example
### Confirmation dialog
> **Delete dashboard?**
>
> Dashboard [dashboard name] will be permanently deleted from Home Assistant.
>
> Cancel / Delete

View File

@@ -1,5 +0,0 @@
---
title: Expansion Panel
---
Expansion panel following all the ARIA guidelines.

View File

@@ -1,157 +0,0 @@
import { mdiPacMan } from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-markdown";
import "../../components/demo-black-white-row";
import { LONG_TEXT } from "../../data/text";
const SHORT_TEXT = LONG_TEXT.substring(0, 113);
const SAMPLES: {
template: (slot: string, leftChevron: boolean) => TemplateResult;
}[] = [
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
header="Attr header"
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
header="Attr header"
secondary="Attr secondary"
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
.header=${"Prop header"}
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
.header=${"Prop header"}
.secondary=${"Prop secondary"}
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
.header=${"Prop header"}
>
<span slot="secondary">Slot Secondary</span>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel slot=${slot} .leftChevron=${leftChevron}>
<span slot="header">Slot header</span>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel slot=${slot} .leftChevron=${leftChevron}>
<span slot="header">Slot header with actions</span>
<ha-icon-button
slot="icons"
label="Some Action"
.path=${mdiPacMan}
></ha-icon-button>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
header="Attr Header with actions"
>
<ha-icon-button
slot="icons"
label="Some Action"
.path=${mdiPacMan}
></ha-icon-button>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
];
@customElement("demo-components-ha-expansion-panel")
export class DemoHaExpansionPanel extends LitElement {
protected render(): TemplateResult {
return html`
${SAMPLES.map(
(sample) => html`
<demo-black-white-row>
${["light", "dark"].map((slot) =>
sample.template(slot, slot === "dark")
)}
</demo-black-white-row>
`
)}
`;
}
static get styles() {
return css`
ha-expansion-panel {
margin: -16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-expansion-panel": DemoHaExpansionPanel;
}
}

View File

@@ -3,7 +3,6 @@ import "@material/mwc-button";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
@@ -21,22 +20,16 @@ const ENTITIES = [
}),
getEntity("media_player", "livingroom", "playing", {
friendly_name: "Livingroom",
media_content_type: "music",
device_class: "tv",
}),
getEntity("media_player", "lounge", "idle", {
friendly_name: "Lounge",
supported_features: 444983,
device_class: "speaker",
}),
getEntity("light", "bedroom", "on", {
friendly_name: "Bedroom",
effect: "colorloop",
effect_list: ["colorloop", "random"],
}),
getEntity("switch", "coffee", "off", {
friendly_name: "Coffee",
device_class: "switch",
}),
];
@@ -148,13 +141,7 @@ const SCHEMAS: {
selector: { attribute: { entity_id: "" } },
context: { filter_entity: "entity" },
},
{
name: "State",
selector: { state: { entity_id: "" } },
context: { filter_entity: "entity", filter_attribute: "Attribute" },
},
{ name: "Device", selector: { device: {} } },
{ name: "Config entry", selector: { config_entry: {} } },
{ name: "Duration", selector: { duration: {} } },
{ name: "area", selector: { area: {} } },
{ name: "target", selector: { target: {} } },
@@ -436,7 +423,6 @@ class DemoHaForm extends LitElement {
hass.addEntities(ENTITIES);
mockEntityRegistry(hass);
mockDeviceRegistry(hass, DEVICES);
mockConfigEntries(hass);
mockAreaRegistry(hass, AREAS);
mockHassioSupervisor(hass);
}

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

@@ -1,5 +1,5 @@
---
title: Selector
title: Selectors
---
See the website for [list of available selectors](https://www.home-assistant.io/docs/blueprint/selectors/).

View File

@@ -3,7 +3,6 @@ import "@material/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
@@ -116,19 +115,11 @@ const SCHEMAS: {
name: "One of each",
input: {
entity: { name: "Entity", selector: { entity: {} } },
state: {
name: "State",
selector: { state: { entity_id: "alarm_control_panel.alarm" } },
},
attribute: {
name: "Attribute",
selector: { attribute: { entity_id: "" } },
},
device: { name: "Device", selector: { device: {} } },
config_entry: {
name: "Integration",
selector: { config_entry: {} },
},
duration: { name: "Duration", selector: { duration: {} } },
addon: { name: "Addon", selector: { addon: {} } },
area: { name: "Area", selector: { area: {} } },
@@ -195,48 +186,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: {
name: "Select (Custom)",
selector: {
@@ -327,7 +276,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
hass.addEntities(ENTITIES);
mockEntityRegistry(hass);
mockDeviceRegistry(hass, DEVICES);
mockConfigEntries(hass);
mockAreaRegistry(hass, AREAS);
mockHassioSupervisor(hass);
hass.mockWS("auth/sign_path", (params) => params);

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

@@ -31,7 +31,7 @@ const ENTITIES = [
friendly_name: "Office Light",
}),
getEntity("fan", "kitchen", "on", {
friendly_name: "Kitchen Fan",
friendly_name: "Second Office Fan",
}),
getEntity("binary_sensor", "kitchen_door", "on", {
friendly_name: "Office Door",
@@ -102,7 +102,7 @@ class DemoArea extends LitElement {
picture: "/images/office.jpg",
},
{
name: "Kitchen",
name: "Second Office",
area_id: "kitchen",
picture: "/images/kitchen.png",
},

View File

@@ -75,10 +75,6 @@ const ENTITIES = [
timestamp: 1641801600,
friendly_name: "Date and Time",
}),
getEntity("sensor", "humidity", "23.2", {
friendly_name: "Humidity",
unit_of_measurement: "%",
}),
getEntity("input_select", "dropdown", "Soda", {
friendly_name: "Dropdown",
options: ["Soda", "Beer", "Wine"],
@@ -98,9 +94,6 @@ const ENTITIES = [
minimum: 0,
maximum: 10,
}),
getEntity("text", "message", "Hello!", {
friendly_name: "Message",
}),
getEntity("light", "unavailable", "unavailable", {
friendly_name: "Bed Light",
@@ -132,9 +125,6 @@ const ENTITIES = [
friendly_name: "Who cooks",
icon: "mdi:cheff",
}),
getEntity("text", "unavailable", "unavailable", {
friendly_name: "Message",
}),
];
const CONFIGS = [
@@ -152,27 +142,6 @@ const CONFIGS = [
- light.non_existing
- climate.ecobee
- input_number.number
- sensor.humidity
- text.message
`,
},
{
heading: "With enabled state color",
config: `
- type: entities
state_color: true
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
- cover.kitchen_window
- group.kitchen
- lock.kitchen_door
- light.bed_light
- light.non_existing
- climate.ecobee
- input_number.number
- sensor.humidity
- text.message
`,
},
{
@@ -245,7 +214,6 @@ const CONFIGS = [
- climate.unavailable
- input_number.unavailable
- input_select.unavailable
- text.unavailable
`,
},
{

View File

@@ -35,11 +35,11 @@ const CONFIGS = [
`,
},
{
heading: "With State",
heading: "Without State",
config: `
- type: button
entity: light.bed_light
show_state: true
show_state: false
`,
},
{

View File

@@ -23,12 +23,13 @@ const CONFIGS = [
heading: "Basic example",
config: `
- type: gauge
title: Humidity
entity: sensor.outside_humidity
name: Outside Humidity
`,
},
{
heading: "Custom unit of measurement",
heading: "Custom Unit of Measurement",
config: `
- type: gauge
entity: sensor.outside_temperature
@@ -37,16 +38,7 @@ const CONFIGS = [
`,
},
{
heading: "Rendering needle",
config: `
- type: gauge
entity: sensor.outside_humidity
name: Outside Humidity
needle: true
`,
},
{
heading: "Setting severity levels",
heading: "Setting Severity Levels",
config: `
- type: gauge
entity: sensor.brightness
@@ -58,7 +50,7 @@ const CONFIGS = [
`,
},
{
heading: "Setting severity levels",
heading: "Setting Severity Levels",
config: `
- type: gauge
entity: sensor.brightness_medium
@@ -70,7 +62,7 @@ const CONFIGS = [
`,
},
{
heading: "Setting severity levels",
heading: "Setting Severity Levels",
config: `
- type: gauge
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: `
- type: gauge
entity: sensor.brightness
@@ -92,14 +84,14 @@ const CONFIGS = [
`,
},
{
heading: "Invalid entity",
heading: "Invalid Entity",
config: `
- type: gauge
entity: sensor.invalid_entity
`,
},
{
heading: "Non-numeric value",
heading: "Non-Numeric Value",
config: `
- type: gauge
entity: plant.bonsai

View File

@@ -62,21 +62,6 @@ const CONFIGS = [
heading: "Basic example",
config: `
- type: glance
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
},
{
heading: "No state colors",
config: `
- type: glance
state_color: false
entities:
- device_tracker.demo_paulus
- media_player.living_room

View File

@@ -1,3 +1,3 @@
---
title: Grid and Stack Card
title: Grid And Stack Card
---

View File

@@ -1,11 +1,11 @@
---
title: Introduction
---
Dashboards have many different cards. Each card allows the user to tell
Lovelace has many different cards. Each card allows the user to tell
a different story about what is going on in their house. These cards
are very customizable, as no household is the same.
This gallery helps our developers and designers to see all the
different states that each card can be in.
Check [the Dashboards documentation](https://www.home-assistant.io/dashboards/) for instructions on how to get started with Dashboards.
Check [the Lovelace documentation](https://www.home-assistant.io/lovelace) for instructions on how to get started with Lovelace.

View File

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

View File

@@ -1,394 +0,0 @@
import {
HassEntity,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeDomain } from "../../../../src/common/entity/compute_domain";
import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display";
import "../../../../src/components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table";
import "../../../../src/components/entity/state-badge";
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"),
// Alert
createEntity("alert.off", "off"),
createEntity("alert.on", "on"),
// Automation
createEntity("automation.off", "off"),
createEntity("automation.on", "on"),
// 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.off", "off"),
createEntity("calendar.on", "on"),
// Camera
createEntity("camera.idle", "idle"),
createEntity("camera.streaming", "streaming"),
// 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.closing", "closing"),
createEntity("cover.closed", "closed"),
createEntity("cover.opening", "opening"),
createEntity("cover.open", "open"),
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.not_home", "not_home"),
createEntity("device_tracker.home", "home"),
createEntity("device_tracker.work", "work"),
createEntity("person.home", "home"),
createEntity("person.not_home", "not_home"),
createEntity("person.work", "work"),
// Fan
createEntity("fan.off", "off"),
createEntity("fan.on", "on"),
// Camera
createEntity("group.off", "off"),
createEntity("group.on", "on"),
// Humidifier
createEntity("humidifier.off", "off"),
createEntity("humidifier.on", "on"),
// Helpers
createEntity("input_boolean.off", "off"),
createEntity("input_boolean.on", "on"),
// Light
createEntity("light.off", "off"),
createEntity("light.on", "on"),
// Locks
createEntity("lock.unlocked", "unlocked"),
createEntity("lock.locked", "locked"),
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"),
// Remote
createEntity("remote.off", "off"),
createEntity("remote.on", "on"),
// Script
createEntity("script.off", "off"),
createEntity("script.on", "on"),
// 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"),
// Timer
createEntity("timer.idle", "idle"),
createEntity("timer.active", "active"),
createEntity("timer.paused", "paused"),
// Vacuum
createEntity("vacuum.docked", "docked"),
createEntity("vacuum.cleaning", "cleaning"),
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) => html`
<state-badge
.stateObj=${entry.stateObj}
.stateColor=${true}
></state-badge>
`,
},
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,9 @@ const createEntityRegistryEntries = (
hidden_by: null,
entity_category: null,
entity_id: "binary_sensor.updater",
id: "binary_sensor.updater",
name: null,
icon: null,
platform: "updater",
has_entity_name: false,
unique_id: "updater",
},
];
@@ -283,7 +280,7 @@ export class DemoIntegrationCard extends LitElement {
.deviceRegistryEntries=${createDeviceRegistryEntries(
info.items[0]
)}
?entryDisabled=${info.disabled}
?disabled=${info.disabled}
.selectedConfigEntryId=${info.highlight}
></ha-integration-card>
`

View File

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

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