mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-05 03:06:40 +00:00
Compare commits
2 Commits
delay-init
...
dashboard-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f3f2059634 | ||
![]() |
497484d419 |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
|
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Build resources
|
- name: Build resources
|
||||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||||
- name: Setup lint cache
|
- name: Setup lint cache
|
||||||
uses: actions/cache@v3.3.2
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
node_modules/.cache/prettier
|
node_modules/.cache/prettier
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v3.8.1
|
||||||
with:
|
with:
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v3.8.1
|
||||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v3.8.1
|
||||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Verify version
|
- name: Verify version
|
||||||
uses: home-assistant/actions/helpers/verify-version@master
|
uses: home-assistant/actions/helpers/verify-version@master
|
||||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -8,4 +8,4 @@ plugins:
|
|||||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||||
spec: "@yarnpkg/plugin-interactive-tools"
|
spec: "@yarnpkg/plugin-interactive-tools"
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-3.6.3.cjs
|
yarnPath: .yarn/releases/yarn-3.6.2.cjs
|
||||||
|
@@ -100,7 +100,6 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
|||||||
useBuiltIns: latestBuild ? false : "entry",
|
useBuiltIns: latestBuild ? false : "entry",
|
||||||
corejs: latestBuild ? false : { version: "3.32", proposals: true },
|
corejs: latestBuild ? false : { version: "3.32", proposals: true },
|
||||||
bugfixes: true,
|
bugfixes: true,
|
||||||
shippedProposals: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@babel/preset-typescript",
|
"@babel/preset-typescript",
|
||||||
|
@@ -1,14 +1,10 @@
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import gulp from "gulp";
|
import gulp from "gulp";
|
||||||
import path from "path";
|
|
||||||
import mapStream from "map-stream";
|
import mapStream from "map-stream";
|
||||||
import transform from "gulp-json-transform";
|
import transform from "gulp-json-transform";
|
||||||
import { LokaliseApi } from "@lokalise/node-api";
|
|
||||||
import JSZip from "jszip";
|
|
||||||
|
|
||||||
const inDir = "translations";
|
const inDirFrontend = "translations/frontend";
|
||||||
const inDirFrontend = `${inDir}/frontend`;
|
const inDirBackend = "translations/backend";
|
||||||
const inDirBackend = `${inDir}/backend`;
|
|
||||||
const srcMeta = "src/translations/translationMetadata.json";
|
const srcMeta = "src/translations/translationMetadata.json";
|
||||||
const encoding = "utf8";
|
const encoding = "utf8";
|
||||||
|
|
||||||
@@ -72,9 +68,8 @@ gulp.task("convert-backend-translations", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("check-translations-html", function () {
|
gulp.task("check-translations-html", function () {
|
||||||
return gulp
|
// We exclude backend translations because they are not compliant with the HTML rule for now
|
||||||
.src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
|
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
|
||||||
.pipe(checkHtml());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("check-all-files-exist", async function () {
|
gulp.task("check-all-files-exist", async function () {
|
||||||
@@ -94,83 +89,7 @@ gulp.task("check-all-files-exist", async function () {
|
|||||||
await Promise.allSettled(writings);
|
await Promise.allSettled(writings);
|
||||||
});
|
});
|
||||||
|
|
||||||
const lokaliseProjects = {
|
|
||||||
backend: "130246255a974bd3b5e8a1.51616605",
|
|
||||||
frontend: "3420425759f6d6d241f598.13594006",
|
|
||||||
};
|
|
||||||
|
|
||||||
gulp.task("fetch-lokalise", async function () {
|
|
||||||
let apiKey;
|
|
||||||
try {
|
|
||||||
apiKey =
|
|
||||||
process.env.LOKALISE_TOKEN ||
|
|
||||||
(await fs.readFile(".lokalise_token", { encoding }));
|
|
||||||
} catch {
|
|
||||||
throw new Error(
|
|
||||||
"An Administrator Lokalise API token is required to download the latest set of translations. Place your token in a new file `.lokalise_token` in the repo root directory."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const lokaliseApi = new LokaliseApi({ apiKey });
|
|
||||||
|
|
||||||
const mkdirPromise = Promise.all([
|
|
||||||
fs.mkdir(inDirFrontend, { recursive: true }),
|
|
||||||
fs.mkdir(inDirBackend, { recursive: true }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Object.entries(lokaliseProjects).map(([project, projectId]) =>
|
|
||||||
lokaliseApi
|
|
||||||
.files()
|
|
||||||
.download(projectId, {
|
|
||||||
format: "json",
|
|
||||||
original_filenames: false,
|
|
||||||
replace_breaks: false,
|
|
||||||
json_unescaped_slashes: true,
|
|
||||||
export_empty_as: "skip",
|
|
||||||
})
|
|
||||||
.then((download) => fetch(download.bundle_url))
|
|
||||||
.then((response) => {
|
|
||||||
if (response.status === 200 || response.status === 0) {
|
|
||||||
return response.arrayBuffer();
|
|
||||||
}
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
})
|
|
||||||
.then(JSZip.loadAsync)
|
|
||||||
.then(async (contents) => {
|
|
||||||
await mkdirPromise;
|
|
||||||
return Promise.all(
|
|
||||||
Object.keys(contents.files).map(async (filename) => {
|
|
||||||
const file = contents.file(filename);
|
|
||||||
if (!file) {
|
|
||||||
// no file, probably a directory
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return file
|
|
||||||
.async("nodebuffer")
|
|
||||||
.then((content) =>
|
|
||||||
fs.writeFile(
|
|
||||||
path.join(
|
|
||||||
inDir,
|
|
||||||
project,
|
|
||||||
filename.split("/").splice(-1)[0]
|
|
||||||
),
|
|
||||||
content,
|
|
||||||
{ flag: "w", encoding }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"download-translations",
|
"check-downloaded-translations",
|
||||||
gulp.series(
|
gulp.series("check-translations-html", "check-all-files-exist")
|
||||||
"fetch-lokalise",
|
|
||||||
"convert-backend-translations",
|
|
||||||
"check-translations-html",
|
|
||||||
"check-all-files-exist"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
@@ -6,8 +6,6 @@ import presetEnv from "@babel/preset-env";
|
|||||||
import compilationTargets from "@babel/helper-compilation-targets";
|
import compilationTargets from "@babel/helper-compilation-targets";
|
||||||
import coreJSCompat from "core-js-compat";
|
import coreJSCompat from "core-js-compat";
|
||||||
import { logPlugin } from "@babel/preset-env/lib/debug.js";
|
import { logPlugin } from "@babel/preset-env/lib/debug.js";
|
||||||
// eslint-disable-next-line import/no-relative-packages
|
|
||||||
import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js";
|
|
||||||
import { babelOptions } from "./bundle.cjs";
|
import { babelOptions } from "./bundle.cjs";
|
||||||
|
|
||||||
const detailsOpen = (heading) =>
|
const detailsOpen = (heading) =>
|
||||||
@@ -28,22 +26,6 @@ const dummyAPI = {
|
|||||||
targets: () => ({}),
|
targets: () => ({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate filter function based on proposal/method inputs
|
|
||||||
// Copied and adapted from babel-plugin-polyfill-corejs3/esm/index.mjs
|
|
||||||
const polyfillFilter = (method, proposals, shippedProposals) => (name) => {
|
|
||||||
if (proposals || method === "entry-global") return true;
|
|
||||||
if (shippedProposals && shippedPolyfills.default.has(name)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (name.startsWith("esnext.")) {
|
|
||||||
const esName = `es.${name.slice(7)}`;
|
|
||||||
// If its imaginative esName is not in latest compat data, it means the proposal is not stage 4
|
|
||||||
return esName in coreJSCompat.data;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Log the plugins and polyfills for each build environment
|
|
||||||
for (const buildType of ["Modern", "Legacy"]) {
|
for (const buildType of ["Modern", "Legacy"]) {
|
||||||
const browserslistEnv = buildType.toLowerCase();
|
const browserslistEnv = buildType.toLowerCase();
|
||||||
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
|
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
|
||||||
@@ -64,13 +46,7 @@ for (const buildType of ["Modern", "Legacy"]) {
|
|||||||
const targets = compilationTargets.default(babelOpts?.targets, {
|
const targets = compilationTargets.default(babelOpts?.targets, {
|
||||||
browserslistEnv,
|
browserslistEnv,
|
||||||
});
|
});
|
||||||
const polyfillList = coreJSCompat({ targets }).list.filter(
|
const polyfillList = coreJSCompat({ targets }).list;
|
||||||
polyfillFilter(
|
|
||||||
`${presetEnvOpts.useBuiltIns}-global`,
|
|
||||||
presetEnvOpts?.corejs?.proposals,
|
|
||||||
presetEnvOpts?.shippedProposals
|
|
||||||
)
|
|
||||||
);
|
|
||||||
console.log(
|
console.log(
|
||||||
"The following %i polyfills may be injected by Babel:\n",
|
"The following %i polyfills may be injected by Babel:\n",
|
||||||
polyfillList.length
|
polyfillList.length
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -1,4 +0,0 @@
|
|||||||
# Note!
|
|
||||||
|
|
||||||
Note, the assets in this folder, are not part of the CC license this repository is shipped in.
|
|
||||||
All rights reserved.
|
|
Binary file not shown.
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
BIN
gallery/public/images/logo.png
Normal file
BIN
gallery/public/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
@@ -2,86 +2,30 @@
|
|||||||
title: "Logo"
|
title: "Logo"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Our logo
|
# Using our logo
|
||||||
|
|
||||||
As a community, we are proud of our logo. Follow these guidelines to ensure it always represents the identity of the Home Assistant project and community the best way possible.
|
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.
|
||||||
|
|
||||||
[Download Logo](https://github.com/home-assistant/assets/tree/master/logo)
|
[Download Logo](https://github.com/home-assistant/assets/tree/master/logo)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Please note that this logo is not released under the CC license. All rights reserved.
|
## Using the icon
|
||||||
|
|
||||||
# Design
|
Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon.
|
||||||
|
|
||||||
At the core of the Home Assistant logomark is the Blue House with Antenna, the three most recognizable and distinct features of the previous logo throughout the past decade.
|

|
||||||
|
|
||||||
### Blue
|
## Using the right variant
|
||||||
|
|
||||||
Blue feels stable and essential. A bright sky blue is joyful, clear, and free of clouds.
|
The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography.
|
||||||
|
|
||||||
### House
|
When needed you can use our logo without a shadow, as seen as the second variant.
|
||||||
|
|
||||||
Of all possible combinations of shapes, a home is best abstracted in the shape of a structure with a pitched roof. With the vast amount of logos based on this shape, the best we can do is to make it more iconic. The house is further simplified - there is no gable and there is no chimney - to an orthogonal shape with an elegant and deliberate proportion.
|
The outlined logo should only be used on packaging.
|
||||||
|
|
||||||
### Antenna
|
## Exclusion zone
|
||||||
|
|
||||||
Call it a tree, a set of nodes, a PCB, or an antenna. The antenna is the most recognizable and memorable part of the previous Home Assistant logo, and is an easily understandable symbol that conveys technologies that are smart, connected, and growing evergreen.
|
The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon.
|
||||||
|
|
||||||
# Usage
|

|
||||||
|
|
||||||
The default variation is the static colored wordmark in horizontal layout and dark text on a light background.
|
|
||||||
|
|
||||||
## Layout variations
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
The default layout is the wordmark in horizontal layout. It provides the clearest context to the brand identity of Home Assistant.
|
|
||||||
|
|
||||||
Use the logomark variant when the context is clear that the logo is about Home Assistant. For example, inside the Home Assistant app where users are already aware of where they are at, the logomark variant without the wordmark can be used. The logomark can exist without the wordmark, however, the wordmark should never exist without the icon.
|
|
||||||
|
|
||||||
Use the wordmark in vertical layout when the space available has an aspect ratio less than 4:3. For example, in a square space on a t-shirt where a logo is needed, since there is no established context of Home Assistant, the wordmark in vertical layout should be used.
|
|
||||||
|
|
||||||
Lastly, use the wordmark in vertical layout with small logomark when Home Assistant is displayed in context of other Home Assistant-related projects. For example, in a flowchart showing the voice pipeline, use this layout for Home Assistant and its other related projects.
|
|
||||||
|
|
||||||
## Color variations, backgrounds, and placement
|
|
||||||
|
|
||||||
The default color is the colored version on light background with dark text.
|
|
||||||
|
|
||||||
For backgrounds that are dark, for example, when it is used on a page in a dark theme, use the colored version on dark background with light text.
|
|
||||||
|
|
||||||
In printed materials where color is unavailable, use the monochrome color variations.
|
|
||||||
|
|
||||||
On background that are dark or photographic, use the light monochrome color on dark background variation.
|
|
||||||
|
|
||||||
On backgrounds that are light or photographic, use the colored version. Do not use the monochrome variations.
|
|
||||||
|
|
||||||
Do not enclose the logmark in a square or color or any confined backgrounds, except in specific situations enforced by another company's marketplace guidelines, for example, an iOS app icon.
|
|
||||||
|
|
||||||
Do not add drop shadow to the logomark or the wordmark. If legibility is compromised due to the background, change the background to provide more contrast, or in last resort, add a heavily blurred drop shadaow.
|
|
||||||
|
|
||||||
It should only be used with black, white, and non-duotone photography.
|
|
||||||
|
|
||||||
Unlike the previous version of our logo, no outlined variants are available. Use the monochrome variants in those spaces.
|
|
||||||
|
|
||||||
### Exclusion zone
|
|
||||||
|
|
||||||
The logo needs some personal space. Its exclusion zone is equal to a quarter the height of the icon.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Animation
|
|
||||||
|
|
||||||
The default is the static variant.
|
|
||||||
|
|
||||||
Use the animated variant only for introductory purposes, for example, in the beginning of a video or on a loading screen.
|
|
||||||
|
|
||||||
Use the animated with sound variant only when sound is warranted in the user's context. For example, use it in the beginning of a video since sounds are expected in a video, but do not use it on a loading screen since sounds are not expected in a user interface.
|
|
||||||
|
|
||||||
Do not repeat the logo animation.
|
|
||||||
|
|
||||||
## Sizes and app icon variants
|
|
||||||
|
|
||||||
Special variants are created for specific contexts.
|
|
||||||
|
|
||||||
Use the tiny variants when the logomark is used in a very small space (16x16 dp), for example, the favicon of the Home Assistant website, a notification on Android, or the menubar of macOS.
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
title: Control Number Buttons
|
|
||||||
---
|
|
@@ -1,100 +0,0 @@
|
|||||||
import { LitElement, TemplateResult, css, html } from "lit";
|
|
||||||
import { customElement, state } from "lit/decorators";
|
|
||||||
import "../../../../src/components/ha-card";
|
|
||||||
import "../../../../src/components/ha-control-number-buttons";
|
|
||||||
import { repeat } from "lit/directives/repeat";
|
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
|
||||||
|
|
||||||
const buttons: {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
step?: number;
|
|
||||||
class?: string;
|
|
||||||
}[] = [
|
|
||||||
{
|
|
||||||
id: "basic",
|
|
||||||
label: "Basic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "min_max_step",
|
|
||||||
label: "With min/max and step",
|
|
||||||
min: 5,
|
|
||||||
max: 25,
|
|
||||||
step: 0.5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "custom",
|
|
||||||
label: "Custom",
|
|
||||||
class: "custom",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
@customElement("demo-components-ha-control-number-buttons")
|
|
||||||
export class DemoHarControlNumberButtons extends LitElement {
|
|
||||||
@state() value = 5;
|
|
||||||
|
|
||||||
private _valueChanged(ev) {
|
|
||||||
this.value = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
${repeat(buttons, (button) => {
|
|
||||||
const { id, label, ...config } = button;
|
|
||||||
return html`
|
|
||||||
<ha-card>
|
|
||||||
<div class="card-content">
|
|
||||||
<label id=${id}>${label}</label>
|
|
||||||
<pre>Config: ${JSON.stringify(config)}</pre>
|
|
||||||
<ha-control-number-buttons
|
|
||||||
.value=${this.value}
|
|
||||||
.min=${config.min}
|
|
||||||
.max=${config.max}
|
|
||||||
.step=${config.step}
|
|
||||||
class=${ifDefined(config.class)}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
.label=${label}
|
|
||||||
>
|
|
||||||
</ha-control-number-buttons>
|
|
||||||
</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 {
|
|
||||||
color: #2196f3;
|
|
||||||
--control-number-buttons-color: #2196f3;
|
|
||||||
--control-number-buttons-background-color: #2196f3;
|
|
||||||
--control-number-buttons-background-opacity: 0.1;
|
|
||||||
--control-number-buttons-thickness: 100px;
|
|
||||||
--control-number-buttons-border-radius: 24px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"demo-components-ha-control-number-buttons": DemoHarControlNumberButtons;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Temp Color Picker
|
||||||
|
---
|
117
gallery/src/pages/components/ha-temp-color-picker.ts
Normal file
117
gallery/src/pages/components/ha-temp-color-picker.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import "../../../../src/components/ha-temp-color-picker";
|
||||||
|
|
||||||
|
import { css, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-slider";
|
||||||
|
|
||||||
|
@customElement("demo-components-ha-temp-color-picker")
|
||||||
|
export class DemoHaTempColorPicker extends LitElement {
|
||||||
|
@state()
|
||||||
|
min = 3000;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
max = 7000;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
value = 4000;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
liveValue?: number;
|
||||||
|
|
||||||
|
private _minChanged(ev) {
|
||||||
|
this.min = Number(ev.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _maxChanged(ev) {
|
||||||
|
this.max = Number(ev.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev) {
|
||||||
|
this.value = Number(ev.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _tempColorCursor(ev) {
|
||||||
|
this.liveValue = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _tempColorChanged(ev) {
|
||||||
|
this.value = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="value">${this.liveValue ?? this.value} K</p>
|
||||||
|
<ha-temp-color-picker
|
||||||
|
.min=${this.min}
|
||||||
|
.max=${this.max}
|
||||||
|
.value=${this.value}
|
||||||
|
@value-changed=${this._tempColorChanged}
|
||||||
|
@cursor-moved=${this._tempColorCursor}
|
||||||
|
></ha-temp-color-picker>
|
||||||
|
<p>Min temp : ${this.min} K</p>
|
||||||
|
<ha-slider
|
||||||
|
step="1"
|
||||||
|
pin
|
||||||
|
min="2000"
|
||||||
|
max="10000"
|
||||||
|
.value=${this.min}
|
||||||
|
@change=${this._minChanged}
|
||||||
|
>
|
||||||
|
</ha-slider>
|
||||||
|
<p>Max temp : ${this.max} K</p>
|
||||||
|
<ha-slider
|
||||||
|
step="1"
|
||||||
|
pin
|
||||||
|
min="2000"
|
||||||
|
max="10000"
|
||||||
|
.value=${this.max}
|
||||||
|
@change=${this._maxChanged}
|
||||||
|
>
|
||||||
|
</ha-slider>
|
||||||
|
<p>Value : ${this.value} K</p>
|
||||||
|
<ha-slider
|
||||||
|
step="1"
|
||||||
|
pin
|
||||||
|
min=${this.min}
|
||||||
|
max=${this.max}
|
||||||
|
.value=${this.value}
|
||||||
|
@change=${this._valueChanged}
|
||||||
|
>
|
||||||
|
</ha-slider>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
ha-temp-color-picker {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-components-ha-temp-color-picker": DemoHaTempColorPicker;
|
||||||
|
}
|
||||||
|
}
|
@@ -343,7 +343,7 @@ export class DemoEntityState extends LitElement {
|
|||||||
const columns: DataTableColumnContainer<EntityRowData> = {
|
const columns: DataTableColumnContainer<EntityRowData> = {
|
||||||
icon: {
|
icon: {
|
||||||
title: "Icon",
|
title: "Icon",
|
||||||
template: (entry) => html`
|
template: (_, entry) => html`
|
||||||
<state-badge
|
<state-badge
|
||||||
.stateObj=${entry.stateObj}
|
.stateObj=${entry.stateObj}
|
||||||
.stateColor=${true}
|
.stateColor=${true}
|
||||||
@@ -360,7 +360,7 @@ export class DemoEntityState extends LitElement {
|
|||||||
title: "State",
|
title: "State",
|
||||||
width: "20%",
|
width: "20%",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
template: (entry) =>
|
template: (_, entry) =>
|
||||||
html`${computeStateDisplay(
|
html`${computeStateDisplay(
|
||||||
hass.localize,
|
hass.localize,
|
||||||
entry.stateObj,
|
entry.stateObj,
|
||||||
@@ -371,14 +371,14 @@ export class DemoEntityState extends LitElement {
|
|||||||
},
|
},
|
||||||
device_class: {
|
device_class: {
|
||||||
title: "Device class",
|
title: "Device class",
|
||||||
template: (entry) => html`${entry.device_class ?? "-"}`,
|
template: (dc) => html`${dc ?? "-"}`,
|
||||||
width: "20%",
|
width: "20%",
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
domain: {
|
domain: {
|
||||||
title: "Domain",
|
title: "Domain",
|
||||||
template: (entry) => html`${computeDomain(entry.entity_id)}`,
|
template: (_, entry) => html`${computeDomain(entry.entity_id)}`,
|
||||||
width: "20%",
|
width: "20%",
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
@@ -49,10 +49,6 @@ import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hass
|
|||||||
import { supervisorTabs } from "../hassio-tabs";
|
import { supervisorTabs } from "../hassio-tabs";
|
||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
|
||||||
type BackupItem = HassioBackup & {
|
|
||||||
secondary: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("hassio-backups")
|
@customElement("hassio-backups")
|
||||||
export class HassioBackups extends LitElement {
|
export class HassioBackups extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -121,15 +117,15 @@ export class HassioBackups extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _columns = memoizeOne(
|
private _columns = memoizeOne(
|
||||||
(narrow: boolean): DataTableColumnContainer<BackupItem> => ({
|
(narrow: boolean): DataTableColumnContainer => ({
|
||||||
name: {
|
name: {
|
||||||
title: this.supervisor.localize("backup.name"),
|
title: this.supervisor.localize("backup.name"),
|
||||||
main: true,
|
main: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
grows: true,
|
grows: true,
|
||||||
template: (backup) =>
|
template: (entry: string, backup: any) =>
|
||||||
html`${backup.name || backup.slug}
|
html`${entry || backup.slug}
|
||||||
<div class="secondary">${backup.secondary}</div>`,
|
<div class="secondary">${backup.secondary}</div>`,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
@@ -138,7 +134,7 @@ export class HassioBackups extends LitElement {
|
|||||||
hidden: narrow,
|
hidden: narrow,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB",
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
title: this.supervisor.localize("backup.location"),
|
title: this.supervisor.localize("backup.location"),
|
||||||
@@ -146,8 +142,8 @@ export class HassioBackups extends LitElement {
|
|||||||
hidden: narrow,
|
hidden: narrow,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
template: (backup) =>
|
template: (entry: string | null) =>
|
||||||
backup.location || this.supervisor.localize("backup.data_disk"),
|
entry || this.supervisor.localize("backup.data_disk"),
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
title: this.supervisor.localize("backup.created"),
|
title: this.supervisor.localize("backup.created"),
|
||||||
@@ -156,8 +152,8 @@ export class HassioBackups extends LitElement {
|
|||||||
hidden: narrow,
|
hidden: narrow,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
template: (backup) =>
|
template: (entry: string) =>
|
||||||
relativeTime(new Date(backup.date), this.hass.locale),
|
relativeTime(new Date(entry), this.hass.locale),
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
title: "",
|
title: "",
|
||||||
@@ -167,7 +163,7 @@ export class HassioBackups extends LitElement {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] =>
|
private _backupData = memoizeOne((backups: HassioBackup[]) =>
|
||||||
backups.map((backup) => ({
|
backups.map((backup) => ({
|
||||||
...backup,
|
...backup,
|
||||||
secondary: this._computeBackupContent(backup),
|
secondary: this._computeBackupContent(backup),
|
||||||
|
@@ -31,8 +31,8 @@ export class HassioUploadBackup extends LitElement {
|
|||||||
.icon=${mdiFolderUpload}
|
.icon=${mdiFolderUpload}
|
||||||
accept="application/x-tar"
|
accept="application/x-tar"
|
||||||
label="Upload backup"
|
label="Upload backup"
|
||||||
supports="Supports .TAR files"
|
|
||||||
@file-picked=${this._uploadFile}
|
@file-picked=${this._uploadFile}
|
||||||
|
auto-open-file-dialog
|
||||||
></ha-file-upload>
|
></ha-file-upload>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -173,7 +173,6 @@ class HassioBackupDialog
|
|||||||
private async _restoreClicked() {
|
private async _restoreClicked() {
|
||||||
const backupDetails = this._backupContent.backupDetails();
|
const backupDetails = this._backupContent.backupDetails();
|
||||||
this._restoringBackup = true;
|
this._restoringBackup = true;
|
||||||
this._dialogParams?.onRestoring?.();
|
|
||||||
if (this._backupContent.backupType === "full") {
|
if (this._backupContent.backupType === "full") {
|
||||||
await this._fullRestoreClicked(backupDetails);
|
await this._fullRestoreClicked(backupDetails);
|
||||||
} else {
|
} else {
|
||||||
@@ -220,7 +219,7 @@ class HassioBackupDialog
|
|||||||
this._error = error.body.message;
|
this._error = error.body.message;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._dialogParams?.onRestoring?.();
|
fireEvent(this, "restoring");
|
||||||
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(backupDetails),
|
body: JSON.stringify(backupDetails),
|
||||||
@@ -269,7 +268,7 @@ class HassioBackupDialog
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this._dialogParams?.onRestoring?.();
|
fireEvent(this, "restoring");
|
||||||
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
|
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(backupDetails),
|
body: JSON.stringify(backupDetails),
|
||||||
|
@@ -5,7 +5,6 @@ import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
|||||||
export interface HassioBackupDialogParams {
|
export interface HassioBackupDialogParams {
|
||||||
slug: string;
|
slug: string;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onRestoring?: () => void;
|
|
||||||
onboarding?: boolean;
|
onboarding?: boolean;
|
||||||
supervisor?: Supervisor;
|
supervisor?: Supervisor;
|
||||||
localize?: LocalizeFunc;
|
localize?: LocalizeFunc;
|
||||||
|
116
package.json
116
package.json
@@ -25,24 +25,24 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.22.15",
|
"@babel/runtime": "7.22.10",
|
||||||
"@braintree/sanitize-url": "6.0.4",
|
"@braintree/sanitize-url": "6.0.4",
|
||||||
"@codemirror/autocomplete": "6.9.1",
|
"@codemirror/autocomplete": "6.9.0",
|
||||||
"@codemirror/commands": "6.2.5",
|
"@codemirror/commands": "6.2.4",
|
||||||
"@codemirror/language": "6.9.0",
|
"@codemirror/language": "6.9.0",
|
||||||
"@codemirror/legacy-modes": "6.3.3",
|
"@codemirror/legacy-modes": "6.3.3",
|
||||||
"@codemirror/search": "6.5.3",
|
"@codemirror/search": "6.5.1",
|
||||||
"@codemirror/state": "6.2.1",
|
"@codemirror/state": "6.2.1",
|
||||||
"@codemirror/view": "6.19.0",
|
"@codemirror/view": "6.16.0",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.10.2",
|
"@formatjs/intl-datetimeformat": "6.10.0",
|
||||||
"@formatjs/intl-displaynames": "6.5.2",
|
"@formatjs/intl-displaynames": "6.5.0",
|
||||||
"@formatjs/intl-getcanonicallocales": "2.2.1",
|
"@formatjs/intl-getcanonicallocales": "2.2.1",
|
||||||
"@formatjs/intl-listformat": "7.4.2",
|
"@formatjs/intl-listformat": "7.4.0",
|
||||||
"@formatjs/intl-locale": "3.3.4",
|
"@formatjs/intl-locale": "3.3.2",
|
||||||
"@formatjs/intl-numberformat": "8.7.2",
|
"@formatjs/intl-numberformat": "8.7.0",
|
||||||
"@formatjs/intl-pluralrules": "5.2.6",
|
"@formatjs/intl-pluralrules": "5.2.4",
|
||||||
"@formatjs/intl-relativetimeformat": "11.2.6",
|
"@formatjs/intl-relativetimeformat": "11.2.4",
|
||||||
"@fullcalendar/core": "6.1.8",
|
"@fullcalendar/core": "6.1.8",
|
||||||
"@fullcalendar/daygrid": "6.1.8",
|
"@fullcalendar/daygrid": "6.1.8",
|
||||||
"@fullcalendar/interaction": "6.1.8",
|
"@fullcalendar/interaction": "6.1.8",
|
||||||
@@ -50,10 +50,10 @@
|
|||||||
"@fullcalendar/luxon3": "6.1.8",
|
"@fullcalendar/luxon3": "6.1.8",
|
||||||
"@fullcalendar/timegrid": "6.1.8",
|
"@fullcalendar/timegrid": "6.1.8",
|
||||||
"@lezer/highlight": "1.1.6",
|
"@lezer/highlight": "1.1.6",
|
||||||
"@lit-labs/context": "0.4.1",
|
"@lit-labs/context": "0.4.0",
|
||||||
"@lit-labs/motion": "1.0.4",
|
"@lit-labs/motion": "1.0.4",
|
||||||
"@lit-labs/virtualizer": "2.0.7",
|
"@lit-labs/virtualizer": "2.0.5",
|
||||||
"@lrnwebcomponents/simple-tooltip": "7.0.18",
|
"@lrnwebcomponents/simple-tooltip": "7.0.16",
|
||||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/mwc-button": "0.27.0",
|
"@material/mwc-button": "0.27.0",
|
||||||
@@ -79,9 +79,10 @@
|
|||||||
"@material/mwc-top-app-bar": "0.27.0",
|
"@material/mwc-top-app-bar": "0.27.0",
|
||||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/web": "=1.0.0-pre.17",
|
"@material/web": "=1.0.0-pre.15",
|
||||||
"@mdi/js": "7.2.96",
|
"@mdi/js": "7.2.96",
|
||||||
"@mdi/svg": "7.2.96",
|
"@mdi/svg": "7.2.96",
|
||||||
|
"@polymer/app-layout": "3.1.0",
|
||||||
"@polymer/iron-flex-layout": "3.0.1",
|
"@polymer/iron-flex-layout": "3.0.1",
|
||||||
"@polymer/iron-input": "3.0.1",
|
"@polymer/iron-input": "3.0.1",
|
||||||
"@polymer/iron-resizable-behavior": "3.0.1",
|
"@polymer/iron-resizable-behavior": "3.0.1",
|
||||||
@@ -93,8 +94,8 @@
|
|||||||
"@polymer/paper-toast": "3.0.1",
|
"@polymer/paper-toast": "3.0.1",
|
||||||
"@polymer/polymer": "3.5.1",
|
"@polymer/polymer": "3.5.1",
|
||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@vaadin/combo-box": "24.1.7",
|
"@vaadin/combo-box": "24.1.5",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.1.7",
|
"@vaadin/vaadin-themable-mixin": "24.1.5",
|
||||||
"@vibrant/color": "3.2.1-alpha.1",
|
"@vibrant/color": "3.2.1-alpha.1",
|
||||||
"@vibrant/core": "3.2.1-alpha.1",
|
"@vibrant/core": "3.2.1-alpha.1",
|
||||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||||
@@ -102,26 +103,26 @@
|
|||||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||||
"app-datepicker": "5.1.1",
|
"app-datepicker": "5.1.1",
|
||||||
"chart.js": "4.3.3",
|
"chart.js": "3.3.2",
|
||||||
"comlink": "4.4.1",
|
"comlink": "4.4.1",
|
||||||
"core-js": "3.32.2",
|
"core-js": "3.32.1",
|
||||||
"cropperjs": "1.6.0",
|
"cropperjs": "1.5.13",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"date-fns-tz": "2.0.0",
|
"date-fns-tz": "2.0.0",
|
||||||
"deep-clone-simple": "1.1.1",
|
"deep-clone-simple": "1.1.1",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"fuse.js": "6.6.2",
|
"fuse.js": "6.6.2",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
"hls.js": "1.4.12",
|
"hls.js": "1.4.10",
|
||||||
"home-assistant-js-websocket": "8.2.0",
|
"home-assistant-js-websocket": "8.2.0",
|
||||||
"idb-keyval": "6.2.1",
|
"idb-keyval": "6.2.1",
|
||||||
"intl-messageformat": "10.5.2",
|
"intl-messageformat": "10.5.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "1.0.4",
|
"leaflet-draw": "1.0.4",
|
||||||
"lit": "2.8.0",
|
"lit": "2.8.0",
|
||||||
"luxon": "3.4.3",
|
"luxon": "3.4.0",
|
||||||
"marked": "7.0.5",
|
"marked": "7.0.4",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "3.2.1-alpha.1",
|
"node-vibrant": "3.2.1-alpha.1",
|
||||||
"proxy-polyfill": "0.3.2",
|
"proxy-polyfill": "0.3.2",
|
||||||
@@ -137,9 +138,9 @@
|
|||||||
"tinykeys": "2.1.0",
|
"tinykeys": "2.1.0",
|
||||||
"tsparticles-engine": "2.12.0",
|
"tsparticles-engine": "2.12.0",
|
||||||
"tsparticles-preset-links": "2.12.0",
|
"tsparticles-preset-links": "2.12.0",
|
||||||
"ua-parser-js": "1.0.36",
|
"ua-parser-js": "1.0.35",
|
||||||
"unfetch": "5.0.0",
|
"unfetch": "5.0.0",
|
||||||
"vis-data": "7.1.7",
|
"vis-data": "7.1.6",
|
||||||
"vis-network": "9.1.6",
|
"vis-network": "9.1.6",
|
||||||
"vue": "2.7.14",
|
"vue": "2.7.14",
|
||||||
"vue2-daterange-picker": "0.6.8",
|
"vue2-daterange-picker": "0.6.8",
|
||||||
@@ -153,13 +154,12 @@
|
|||||||
"xss": "1.0.14"
|
"xss": "1.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.22.20",
|
"@babel/core": "7.22.10",
|
||||||
"@babel/plugin-proposal-decorators": "7.22.15",
|
"@babel/plugin-proposal-decorators": "7.22.10",
|
||||||
"@babel/plugin-transform-runtime": "7.22.15",
|
"@babel/plugin-transform-runtime": "7.22.10",
|
||||||
"@babel/preset-env": "7.22.20",
|
"@babel/preset-env": "7.22.10",
|
||||||
"@babel/preset-typescript": "7.22.15",
|
"@babel/preset-typescript": "7.22.5",
|
||||||
"@koa/cors": "4.0.0",
|
"@koa/cors": "4.0.0",
|
||||||
"@lokalise/node-api": "11.0.1",
|
|
||||||
"@octokit/auth-oauth-device": "6.0.0",
|
"@octokit/auth-oauth-device": "6.0.0",
|
||||||
"@octokit/plugin-retry": "6.0.0",
|
"@octokit/plugin-retry": "6.0.0",
|
||||||
"@octokit/rest": "20.0.1",
|
"@octokit/rest": "20.0.1",
|
||||||
@@ -167,34 +167,34 @@
|
|||||||
"@rollup/plugin-babel": "6.0.3",
|
"@rollup/plugin-babel": "6.0.3",
|
||||||
"@rollup/plugin-commonjs": "25.0.4",
|
"@rollup/plugin-commonjs": "25.0.4",
|
||||||
"@rollup/plugin-json": "6.0.0",
|
"@rollup/plugin-json": "6.0.0",
|
||||||
"@rollup/plugin-node-resolve": "15.2.1",
|
"@rollup/plugin-node-resolve": "15.2.0",
|
||||||
"@rollup/plugin-replace": "5.0.2",
|
"@rollup/plugin-replace": "5.0.2",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.3",
|
"@types/babel__plugin-transform-runtime": "7.9.2",
|
||||||
"@types/chromecast-caf-receiver": "6.0.10",
|
"@types/chromecast-caf-receiver": "6.0.9",
|
||||||
"@types/chromecast-caf-sender": "1.0.6",
|
"@types/chromecast-caf-sender": "1.0.5",
|
||||||
"@types/esprima": "4.0.3",
|
"@types/esprima": "4.0.3",
|
||||||
"@types/glob": "8.1.0",
|
"@types/glob": "8.1.0",
|
||||||
"@types/html-minifier-terser": "7.0.0",
|
"@types/html-minifier-terser": "7.0.0",
|
||||||
"@types/js-yaml": "4.0.6",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/leaflet": "1.9.4",
|
"@types/leaflet": "1.9.3",
|
||||||
"@types/leaflet-draw": "1.0.8",
|
"@types/leaflet-draw": "1.0.7",
|
||||||
"@types/luxon": "3.3.2",
|
"@types/luxon": "3.3.1",
|
||||||
"@types/mocha": "10.0.1",
|
"@types/mocha": "10.0.1",
|
||||||
"@types/qrcode": "1.5.2",
|
"@types/qrcode": "1.5.1",
|
||||||
"@types/serve-handler": "6.1.1",
|
"@types/serve-handler": "6.1.1",
|
||||||
"@types/sortablejs": "1.15.2",
|
"@types/sortablejs": "1.15.1",
|
||||||
"@types/tar": "6.1.6",
|
"@types/tar": "6.1.5",
|
||||||
"@types/ua-parser-js": "0.7.37",
|
"@types/ua-parser-js": "0.7.36",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "6.7.0",
|
"@typescript-eslint/eslint-plugin": "6.4.0",
|
||||||
"@typescript-eslint/parser": "6.7.0",
|
"@typescript-eslint/parser": "6.4.0",
|
||||||
"@web/dev-server": "0.1.38",
|
"@web/dev-server": "0.1.38",
|
||||||
"@web/dev-server-rollup": "0.4.1",
|
"@web/dev-server-rollup": "0.4.1",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"chai": "4.3.8",
|
"chai": "4.3.7",
|
||||||
"del": "7.1.0",
|
"del": "7.0.0",
|
||||||
"eslint": "8.49.0",
|
"eslint": "8.47.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "17.1.0",
|
"eslint-config-airbnb-typescript": "17.1.0",
|
||||||
"eslint-config-prettier": "9.0.0",
|
"eslint-config-prettier": "9.0.0",
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
"esprima": "4.0.1",
|
"esprima": "4.0.1",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.1.1",
|
"fs-extra": "11.1.1",
|
||||||
"glob": "10.3.4",
|
"glob": "10.3.3",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"gulp-flatmap": "1.0.2",
|
"gulp-flatmap": "1.0.2",
|
||||||
"gulp-json-transform": "0.4.8",
|
"gulp-json-transform": "0.4.8",
|
||||||
@@ -219,28 +219,28 @@
|
|||||||
"husky": "8.0.3",
|
"husky": "8.0.3",
|
||||||
"instant-mocha": "1.5.2",
|
"instant-mocha": "1.5.2",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "14.0.1",
|
"lint-staged": "14.0.0",
|
||||||
"lit-analyzer": "2.0.0-pre.3",
|
"lit-analyzer": "2.0.0-pre.3",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"magic-string": "0.30.3",
|
"magic-string": "0.30.2",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"mocha": "10.2.0",
|
"mocha": "10.2.0",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
"open": "9.1.0",
|
"open": "9.1.0",
|
||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.0.2",
|
||||||
"rollup": "2.79.1",
|
"rollup": "2.79.1",
|
||||||
"rollup-plugin-string": "3.0.0",
|
"rollup-plugin-string": "3.0.0",
|
||||||
"rollup-plugin-terser": "7.0.2",
|
"rollup-plugin-terser": "7.0.2",
|
||||||
"rollup-plugin-visualizer": "5.9.2",
|
"rollup-plugin-visualizer": "5.9.2",
|
||||||
"serve-handler": "6.1.5",
|
"serve-handler": "6.1.5",
|
||||||
"sinon": "16.0.0",
|
"sinon": "15.2.0",
|
||||||
"source-map-url": "0.4.1",
|
"source-map-url": "0.4.1",
|
||||||
"systemjs": "6.14.2",
|
"systemjs": "6.14.2",
|
||||||
"tar": "6.2.0",
|
"tar": "6.1.15",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.9",
|
||||||
"ts-lit-plugin": "2.0.0-pre.1",
|
"ts-lit-plugin": "2.0.0-pre.1",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.1.6",
|
||||||
"vinyl-buffer": "1.0.1",
|
"vinyl-buffer": "1.0.1",
|
||||||
"vinyl-source-stream": "2.0.0",
|
"vinyl-source-stream": "2.0.0",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.88.2",
|
||||||
@@ -257,5 +257,5 @@
|
|||||||
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
|
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
|
||||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.6.3"
|
"packageManager": "yarn@3.6.2"
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20230911.0"
|
version = "20230802.0"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@@ -8,4 +8,40 @@ set -eu -o pipefail
|
|||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
./node_modules/.bin/gulp download-translations
|
if [ -z "${LOKALISE_TOKEN-}" ] && [ ! -f .lokalise_token ] ; then
|
||||||
|
echo "Lokalise API token is required to download the latest set of" \
|
||||||
|
"translations. Please create an account by using the following link:" \
|
||||||
|
"https://lokalise.co/signup/3420425759f6d6d241f598.13594006/all/" \
|
||||||
|
"Place your token in a new file \".lokalise_token\" in the repo" \
|
||||||
|
"root directory."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load token from file if not already in the environment
|
||||||
|
[ -z "${LOKALISE_TOKEN-}" ] && LOKALISE_TOKEN="$(<.lokalise_token)"
|
||||||
|
|
||||||
|
declare -A PROJECT_ID=( \
|
||||||
|
[frontend]="3420425759f6d6d241f598.13594006" \
|
||||||
|
[backend]="130246255a974bd3b5e8a1.51616605" \
|
||||||
|
)
|
||||||
|
|
||||||
|
for project in ${!PROJECT_ID[*]}; do
|
||||||
|
LOCAL_DIR=`pwd`/translations/${project}
|
||||||
|
rm -f ${LOCAL_DIR}/* || mkdir -p ${LOCAL_DIR}
|
||||||
|
docker run \
|
||||||
|
-v ${LOCAL_DIR}:/opt/dest/locale \
|
||||||
|
--rm \
|
||||||
|
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d \
|
||||||
|
lokalise2 \
|
||||||
|
--token ${LOKALISE_TOKEN} \
|
||||||
|
--project-id ${PROJECT_ID[${project}]} \
|
||||||
|
file download \
|
||||||
|
--export-empty-as skip \
|
||||||
|
--format json \
|
||||||
|
--json-unescaped-slashes=true \
|
||||||
|
--replace-breaks=false \
|
||||||
|
--original-filenames=false \
|
||||||
|
--unzip-to /opt/dest
|
||||||
|
done
|
||||||
|
|
||||||
|
./node_modules/.bin/gulp check-downloaded-translations
|
@@ -35,8 +35,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
|
|
||||||
@property() public oauth2State?: string;
|
@property() public oauth2State?: string;
|
||||||
|
|
||||||
@property() public translationFragment = "page-authorize";
|
|
||||||
|
|
||||||
@state() private _authProvider?: AuthProvider;
|
@state() private _authProvider?: AuthProvider;
|
||||||
|
|
||||||
@state() private _authProviders?: AuthProvider[];
|
@state() private _authProviders?: AuthProvider[];
|
||||||
@@ -47,6 +45,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
this.translationFragment = "page-authorize";
|
||||||
const query = extractSearchParamsObject() as AuthUrlSearchParams;
|
const query = extractSearchParamsObject() as AuthUrlSearchParams;
|
||||||
if (query.client_id) {
|
if (query.client_id) {
|
||||||
this.clientId = query.client_id;
|
this.clientId = query.client_id;
|
||||||
@@ -103,6 +102,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
<ha-auth-flow
|
<ha-auth-flow
|
||||||
|
.resources=${this.resources}
|
||||||
.clientId=${this.clientId}
|
.clientId=${this.clientId}
|
||||||
.redirectUri=${this.redirectUri}
|
.redirectUri=${this.redirectUri}
|
||||||
.oauth2State=${this.oauth2State}
|
.oauth2State=${this.oauth2State}
|
||||||
|
@@ -35,47 +35,20 @@ export class HaPasswordManagerPolyfill extends LitElement {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._styleElement = document.createElement("style");
|
this._styleElement = document.createElement("style");
|
||||||
this._styleElement.textContent = css`
|
this._styleElement.textContent = css`
|
||||||
/* Polyfill form is sized and vertically aligned with true form, then positioned offscreen
|
|
||||||
rather than hiding so it does not create a new stacking context */
|
|
||||||
.password-manager-polyfill {
|
.password-manager-polyfill {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
box-sizing: border-box;
|
opacity: 0;
|
||||||
}
|
|
||||||
/* Excluding our wrapper, move any children back on screen, including anything injected that might not already be positioned */
|
|
||||||
.password-manager-polyfill > *:not(.wrapper),
|
|
||||||
.password-manager-polyfill > .wrapper > * {
|
|
||||||
position: relative;
|
|
||||||
left: 10000px;
|
|
||||||
}
|
|
||||||
/* Size and hide our polyfill fields */
|
|
||||||
.password-manager-polyfill .underneath {
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 16px;
|
|
||||||
border: 0;
|
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
height: 21px;
|
|
||||||
/* Transparency is only needed to hide during paint or in case of misalignment,
|
|
||||||
but LastPass will fail if it's 0, so we use 1% */
|
|
||||||
opacity: 0.01;
|
|
||||||
}
|
}
|
||||||
.password-manager-polyfill input.underneath {
|
.password-manager-polyfill input {
|
||||||
height: 28px;
|
width: 100%;
|
||||||
margin-bottom: 30.5px;
|
height: 62px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
}
|
}
|
||||||
/* Button position is not important, but size should not be zero */
|
.password-manager-polyfill input[type="submit"] {
|
||||||
.password-manager-polyfill > input.underneath[type="submit"] {
|
width: 0;
|
||||||
width: 1px;
|
height: 0;
|
||||||
height: 1px;
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
/* Ensure injected elements will be on top */
|
|
||||||
.password-manager-polyfill > *:not(.underneath, .wrapper),
|
|
||||||
.password-manager-polyfill > .wrapper > *:not(.underneath) {
|
|
||||||
isolation: isolate;
|
|
||||||
z-index: auto;
|
|
||||||
}
|
}
|
||||||
`.toString();
|
`.toString();
|
||||||
document.head.append(this._styleElement);
|
document.head.append(this._styleElement);
|
||||||
@@ -104,25 +77,16 @@ export class HaPasswordManagerPolyfill extends LitElement {
|
|||||||
class="password-manager-polyfill"
|
class="password-manager-polyfill"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
top: `${this.boundingRect?.y || 148}px`,
|
top: `${this.boundingRect?.y || 148}px`,
|
||||||
left: `calc(50% - ${
|
left: `calc(50% - ${(this.boundingRect?.width || 360) / 2}px)`,
|
||||||
(this.boundingRect?.width || 360) / 2
|
|
||||||
}px - 10000px)`,
|
|
||||||
width: `${this.boundingRect?.width || 360}px`,
|
width: `${this.boundingRect?.width || 360}px`,
|
||||||
})}
|
})}
|
||||||
action="/auth"
|
aria-hidden="true"
|
||||||
method="post"
|
|
||||||
@submit=${this._handleSubmit}
|
@submit=${this._handleSubmit}
|
||||||
>
|
>
|
||||||
${autocompleteLoginFields(this.step.data_schema).map((input) =>
|
${autocompleteLoginFields(this.step.data_schema).map((input) =>
|
||||||
this.render_input(input)
|
this.render_input(input)
|
||||||
)}
|
)}
|
||||||
<input
|
<input type="submit" />
|
||||||
type="submit"
|
|
||||||
value="Login"
|
|
||||||
class="underneath"
|
|
||||||
tabindex="-2"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -135,35 +99,25 @@ export class HaPasswordManagerPolyfill extends LitElement {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<!-- Label is a sibling so it can be stacked underneath without affecting injections adjacent to input (e.g. LastPass) -->
|
<input
|
||||||
<label for=${schema.name} class="underneath" aria-hidden="true">
|
tabindex="-1"
|
||||||
${schema.name}
|
.id=${schema.name}
|
||||||
</label>
|
.name=${schema.name}
|
||||||
<!-- LastPass fails if the input is hidden directly, so we trick it and hide a wrapper instead -->
|
.type=${inputType}
|
||||||
<div class="wrapper" aria-hidden="true">
|
.value=${this.stepData[schema.name] || ""}
|
||||||
<!-- LastPass fails with tabindex of -1, so we trick with -2 -->
|
.autocomplete=${schema.autocomplete}
|
||||||
<input
|
@input=${this._valueChanged}
|
||||||
class="underneath"
|
/>
|
||||||
tabindex="-2"
|
|
||||||
.id=${schema.name}
|
|
||||||
.name=${schema.name}
|
|
||||||
.type=${inputType}
|
|
||||||
.value=${this.stepData[schema.name] || ""}
|
|
||||||
.autocomplete=${schema.autocomplete}
|
|
||||||
@input=${this._valueChanged}
|
|
||||||
@change=${this._valueChanged}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleSubmit(ev: SubmitEvent) {
|
private _handleSubmit(ev: Event) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
fireEvent(this, "form-submitted");
|
fireEvent(this, "form-submitted");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: Event) {
|
private _valueChanged(ev: Event) {
|
||||||
const target = ev.target as HTMLInputElement;
|
const target = ev.target! as HTMLInputElement;
|
||||||
this.stepData = { ...this.stepData, [target.id]: target.value };
|
this.stepData = { ...this.stepData, [target.id]: target.value };
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: this.stepData,
|
value: this.stepData,
|
||||||
|
@@ -4,7 +4,7 @@ export const clamp = (value: number, min: number, max: number) =>
|
|||||||
// Variant that only applies the clamping to a border if the border is defined
|
// Variant that only applies the clamping to a border if the border is defined
|
||||||
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
||||||
let result: number;
|
let result: number;
|
||||||
result = min != null ? Math.max(value, min) : value;
|
result = min ? Math.max(value, min) : value;
|
||||||
result = max != null ? Math.min(result, max) : result;
|
result = max ? Math.min(result, max) : result;
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
@@ -108,7 +108,7 @@ export const formatNumber = (
|
|||||||
* @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined`
|
* @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined`
|
||||||
*/
|
*/
|
||||||
export const getNumberFormatOptions = (
|
export const getNumberFormatOptions = (
|
||||||
entityState?: HassEntity,
|
entityState: HassEntity,
|
||||||
entity?: EntityRegistryDisplayEntry
|
entity?: EntityRegistryDisplayEntry
|
||||||
): Intl.NumberFormatOptions | undefined => {
|
): Intl.NumberFormatOptions | undefined => {
|
||||||
const precision = entity?.display_precision;
|
const precision = entity?.display_precision;
|
||||||
@@ -119,8 +119,8 @@ export const getNumberFormatOptions = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
Number.isInteger(Number(entityState?.attributes?.step)) &&
|
Number.isInteger(Number(entityState.attributes?.step)) &&
|
||||||
Number.isInteger(Number(entityState?.state))
|
Number.isInteger(Number(entityState.state))
|
||||||
) {
|
) {
|
||||||
return { maximumFractionDigits: 0 };
|
return { maximumFractionDigits: 0 };
|
||||||
}
|
}
|
||||||
|
@@ -22,7 +22,14 @@ export type LocalizeKeys =
|
|||||||
| `ui.dialogs.unhealthy.reason.${string}`
|
| `ui.dialogs.unhealthy.reason.${string}`
|
||||||
| `ui.dialogs.unsupported.reason.${string}`
|
| `ui.dialogs.unsupported.reason.${string}`
|
||||||
| `ui.panel.config.${string}.${"caption" | "description"}`
|
| `ui.panel.config.${string}.${"caption" | "description"}`
|
||||||
|
| `ui.panel.config.automation.${string}`
|
||||||
| `ui.panel.config.dashboard.${string}`
|
| `ui.panel.config.dashboard.${string}`
|
||||||
|
| `ui.panel.config.devices.${string}`
|
||||||
|
| `ui.panel.config.energy.${string}`
|
||||||
|
| `ui.panel.config.info.${string}`
|
||||||
|
| `ui.panel.config.lovelace.${string}`
|
||||||
|
| `ui.panel.config.network.${string}`
|
||||||
|
| `ui.panel.config.scene.${string}`
|
||||||
| `ui.panel.config.zha.${string}`
|
| `ui.panel.config.zha.${string}`
|
||||||
| `ui.panel.config.zwave_js.${string}`
|
| `ui.panel.config.zwave_js.${string}`
|
||||||
| `ui.panel.lovelace.card.${string}`
|
| `ui.panel.lovelace.card.${string}`
|
||||||
|
@@ -15,20 +15,13 @@ import { HomeAssistant } from "../../types";
|
|||||||
|
|
||||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||||
|
|
||||||
export interface ChartResizeOptions {
|
interface Tooltip extends TooltipModel<any> {
|
||||||
aspectRatio?: number;
|
|
||||||
height?: number;
|
|
||||||
width?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Tooltip
|
|
||||||
extends Omit<TooltipModel<any>, "tooltipPosition" | "hasValue" | "getProps"> {
|
|
||||||
top: string;
|
top: string;
|
||||||
left: string;
|
left: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ha-chart-base")
|
@customElement("ha-chart-base")
|
||||||
export class HaChartBase extends LitElement {
|
export default class HaChartBase extends LitElement {
|
||||||
public chart?: Chart;
|
public chart?: Chart;
|
||||||
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -52,6 +45,14 @@ export class HaChartBase extends LitElement {
|
|||||||
|
|
||||||
@state() private _hiddenDatasets: Set<number> = new Set();
|
@state() private _hiddenDatasets: Set<number> = new Set();
|
||||||
|
|
||||||
|
private _releaseCanvas() {
|
||||||
|
// release the canvas memory to prevent
|
||||||
|
// safari from running out of memory.
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
this._releaseCanvas();
|
this._releaseCanvas();
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
@@ -64,36 +65,6 @@ export class HaChartBase extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateChart = (
|
|
||||||
mode:
|
|
||||||
| "resize"
|
|
||||||
| "reset"
|
|
||||||
| "none"
|
|
||||||
| "hide"
|
|
||||||
| "show"
|
|
||||||
| "default"
|
|
||||||
| "active"
|
|
||||||
| undefined
|
|
||||||
): void => {
|
|
||||||
this.chart?.update(mode);
|
|
||||||
};
|
|
||||||
|
|
||||||
public resize = (options?: ChartResizeOptions): void => {
|
|
||||||
if (options?.aspectRatio && !options.height) {
|
|
||||||
options.height = Math.round(
|
|
||||||
(options.width ?? this.clientWidth) / options.aspectRatio
|
|
||||||
);
|
|
||||||
} else if (options?.aspectRatio && !options.width) {
|
|
||||||
options.width = Math.round(
|
|
||||||
(options.height ?? this.clientHeight) * options.aspectRatio
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.chart?.resize(
|
|
||||||
options?.width ?? this.clientWidth,
|
|
||||||
options?.height ?? this.clientHeight
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
this._setupChart();
|
this._setupChart();
|
||||||
this.data.datasets.forEach((dataset, index) => {
|
this.data.datasets.forEach((dataset, index) => {
|
||||||
@@ -109,11 +80,14 @@ export class HaChartBase extends LitElement {
|
|||||||
if (!this.hasUpdated || !this.chart) {
|
if (!this.hasUpdated || !this.chart) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (changedProps.has("plugins") || changedProps.has("chartType")) {
|
if (changedProps.has("plugins")) {
|
||||||
this.chart.destroy();
|
this.chart.destroy();
|
||||||
this._setupChart();
|
this._setupChart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (changedProps.has("chartType")) {
|
||||||
|
this.chart.config.type = this.chartType;
|
||||||
|
}
|
||||||
if (changedProps.has("data")) {
|
if (changedProps.has("data")) {
|
||||||
if (this._hiddenDatasets.size) {
|
if (this._hiddenDatasets.size) {
|
||||||
this.data.datasets.forEach((dataset, index) => {
|
this.data.datasets.forEach((dataset, index) => {
|
||||||
@@ -157,70 +131,55 @@ export class HaChartBase extends LitElement {
|
|||||||
</div>`
|
</div>`
|
||||||
: ""}
|
: ""}
|
||||||
<div
|
<div
|
||||||
class="animationContainer"
|
class="chartContainer"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
height: `${this.height || this._chartHeight || 0}px`,
|
height: `${this.height ?? this._chartHeight}px`,
|
||||||
overflow: this._chartHeight ? "initial" : "hidden",
|
overflow: this._chartHeight ? "initial" : "hidden",
|
||||||
|
"padding-left": `${computeRTL(this.hass) ? 0 : this.paddingYAxis}px`,
|
||||||
|
"padding-right": `${computeRTL(this.hass) ? this.paddingYAxis : 0}px`,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<canvas></canvas>
|
||||||
class="chartContainer"
|
${this._tooltip
|
||||||
style=${styleMap({
|
? html`<div
|
||||||
height: `${
|
class="chartTooltip ${classMap({ [this._tooltip.yAlign]: true })}"
|
||||||
this.height ?? this._chartHeight ?? this.clientWidth / 2
|
style=${styleMap({
|
||||||
}px`,
|
top: this._tooltip.top,
|
||||||
"padding-left": `${
|
left: this._tooltip.left,
|
||||||
computeRTL(this.hass) ? 0 : this.paddingYAxis
|
})}
|
||||||
}px`,
|
>
|
||||||
"padding-right": `${
|
<div class="title">${this._tooltip.title}</div>
|
||||||
computeRTL(this.hass) ? this.paddingYAxis : 0
|
${this._tooltip.beforeBody
|
||||||
}px`,
|
? html`<div class="beforeBody">
|
||||||
})}
|
${this._tooltip.beforeBody}
|
||||||
>
|
</div>`
|
||||||
<canvas></canvas>
|
: ""}
|
||||||
${this._tooltip
|
<div>
|
||||||
? html`<div
|
<ul>
|
||||||
class="chartTooltip ${classMap({
|
${this._tooltip.body.map(
|
||||||
[this._tooltip.yAlign]: true,
|
(item, i) =>
|
||||||
})}"
|
html`<li>
|
||||||
style=${styleMap({
|
<div
|
||||||
top: this._tooltip.top,
|
class="bullet"
|
||||||
left: this._tooltip.left,
|
style=${styleMap({
|
||||||
})}
|
backgroundColor: this._tooltip!.labelColors[i]
|
||||||
>
|
.backgroundColor as string,
|
||||||
<div class="title">${this._tooltip.title}</div>
|
borderColor: this._tooltip!.labelColors[i]
|
||||||
${this._tooltip.beforeBody
|
.borderColor as string,
|
||||||
? html`<div class="beforeBody">
|
})}
|
||||||
${this._tooltip.beforeBody}
|
></div>
|
||||||
</div>`
|
${item.lines.join("\n")}
|
||||||
: ""}
|
</li>`
|
||||||
<div>
|
)}
|
||||||
<ul>
|
</ul>
|
||||||
${this._tooltip.body.map(
|
</div>
|
||||||
(item, i) =>
|
${this._tooltip.footer.length
|
||||||
html`<li>
|
? html`<div class="footer">
|
||||||
<div
|
${this._tooltip.footer.map((item) => html`${item}<br />`)}
|
||||||
class="bullet"
|
</div>`
|
||||||
style=${styleMap({
|
: ""}
|
||||||
backgroundColor: this._tooltip!.labelColors[i]
|
</div>`
|
||||||
.backgroundColor as string,
|
: ""}
|
||||||
borderColor: this._tooltip!.labelColors[i]
|
|
||||||
.borderColor as string,
|
|
||||||
})}
|
|
||||||
></div>
|
|
||||||
${item.lines.join("\n")}
|
|
||||||
</li>`
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
${this._tooltip.footer.length
|
|
||||||
? html`<div class="footer">
|
|
||||||
${this._tooltip.footer.map((item) => html`${item}<br />`)}
|
|
||||||
</div>`
|
|
||||||
: ""}
|
|
||||||
</div>`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -254,7 +213,6 @@ export class HaChartBase extends LitElement {
|
|||||||
|
|
||||||
private _createOptions() {
|
private _createOptions() {
|
||||||
return {
|
return {
|
||||||
maintainAspectRatio: false,
|
|
||||||
...this.options,
|
...this.options,
|
||||||
plugins: {
|
plugins: {
|
||||||
...this.options?.plugins,
|
...this.options?.plugins,
|
||||||
@@ -275,10 +233,10 @@ export class HaChartBase extends LitElement {
|
|||||||
return [
|
return [
|
||||||
...(this.plugins || []),
|
...(this.plugins || []),
|
||||||
{
|
{
|
||||||
id: "resizeHook",
|
id: "afterRenderHook",
|
||||||
resize: (chart) => {
|
afterRender: (chart) => {
|
||||||
const change = chart.height - (this._chartHeight ?? 0);
|
const change = chart.height - (this._chartHeight ?? 0);
|
||||||
if (!this._chartHeight || change > 12 || change < -12) {
|
if (!this._chartHeight || change > 0 || change < -12) {
|
||||||
// hysteresis to prevent infinite render loops
|
// hysteresis to prevent infinite render loops
|
||||||
this._chartHeight = chart.height;
|
this._chartHeight = chart.height;
|
||||||
}
|
}
|
||||||
@@ -330,13 +288,21 @@ export class HaChartBase extends LitElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _releaseCanvas() {
|
public updateChart = (
|
||||||
// release the canvas memory to prevent
|
mode:
|
||||||
// safari from running out of memory.
|
| "resize"
|
||||||
|
| "reset"
|
||||||
|
| "none"
|
||||||
|
| "hide"
|
||||||
|
| "show"
|
||||||
|
| "normal"
|
||||||
|
| "active"
|
||||||
|
| undefined
|
||||||
|
): void => {
|
||||||
if (this.chart) {
|
if (this.chart) {
|
||||||
this.chart.destroy();
|
this.chart.update(mode);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
@@ -344,7 +310,7 @@ export class HaChartBase extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
position: var(--chart-base-position, relative);
|
position: var(--chart-base-position, relative);
|
||||||
}
|
}
|
||||||
.animationContainer {
|
.chartContainer {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 0;
|
height: 0;
|
||||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||||
import { html, LitElement, PropertyValues } from "lit";
|
import { html, LitElement, PropertyValues } from "lit";
|
||||||
import { property, query, state } from "lit/decorators";
|
import { property, state } from "lit/decorators";
|
||||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
@@ -11,18 +11,14 @@ import {
|
|||||||
} from "../../common/number/format_number";
|
} from "../../common/number/format_number";
|
||||||
import { LineChartEntity, LineChartState } from "../../data/history";
|
import { LineChartEntity, LineChartState } from "../../data/history";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import {
|
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||||
ChartResizeOptions,
|
|
||||||
HaChartBase,
|
|
||||||
MIN_TIME_BETWEEN_UPDATES,
|
|
||||||
} from "./ha-chart-base";
|
|
||||||
|
|
||||||
const safeParseFloat = (value) => {
|
const safeParseFloat = (value) => {
|
||||||
const parsed = parseFloat(value);
|
const parsed = parseFloat(value);
|
||||||
return isFinite(parsed) ? parsed : null;
|
return isFinite(parsed) ? parsed : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class StateHistoryChartLine extends LitElement {
|
class StateHistoryChartLine extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public data: LineChartEntity[] = [];
|
@property({ attribute: false }) public data: LineChartEntity[] = [];
|
||||||
@@ -51,12 +47,6 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
|
|
||||||
private _chartTime: Date = new Date();
|
private _chartTime: Date = new Date();
|
||||||
|
|
||||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
|
||||||
|
|
||||||
public resize = (options?: ChartResizeOptions): void => {
|
|
||||||
this._chart?.resize(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-chart-base
|
<ha-chart-base
|
||||||
@@ -137,16 +127,12 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
`${context.dataset.label}: ${formatNumber(
|
`${context.dataset.label}: ${formatNumber(
|
||||||
context.parsed.y,
|
context.parsed.y,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
this.data[context.datasetIndex]?.entity_id
|
getNumberFormatOptions(
|
||||||
? getNumberFormatOptions(
|
this.hass.states[this.data[context.datasetIndex].entity_id],
|
||||||
this.hass.states[
|
this.hass.entities[
|
||||||
this.data[context.datasetIndex].entity_id
|
this.data[context.datasetIndex].entity_id
|
||||||
],
|
]
|
||||||
this.hass.entities[
|
)
|
||||||
this.data[context.datasetIndex].entity_id
|
|
||||||
]
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
)} ${this.unit}`,
|
)} ${this.unit}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||||
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
@@ -8,11 +8,7 @@ import { numberFormatToLocale } from "../../common/number/format_number";
|
|||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import { TimelineEntity } from "../../data/history";
|
import { TimelineEntity } from "../../data/history";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import {
|
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||||
ChartResizeOptions,
|
|
||||||
HaChartBase,
|
|
||||||
MIN_TIME_BETWEEN_UPDATES,
|
|
||||||
} from "./ha-chart-base";
|
|
||||||
import type { TimeLineData } from "./timeline-chart/const";
|
import type { TimeLineData } from "./timeline-chart/const";
|
||||||
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
||||||
|
|
||||||
@@ -50,12 +46,6 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
|
|
||||||
private _chartTime: Date = new Date();
|
private _chartTime: Date = new Date();
|
||||||
|
|
||||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
|
||||||
|
|
||||||
public resize = (options?: ChartResizeOptions): void => {
|
|
||||||
this._chart?.resize(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-chart-base
|
<ha-chart-base
|
||||||
|
@@ -6,13 +6,7 @@ import {
|
|||||||
nothing,
|
nothing,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import {
|
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||||
customElement,
|
|
||||||
eventOptions,
|
|
||||||
property,
|
|
||||||
queryAll,
|
|
||||||
state,
|
|
||||||
} from "lit/decorators";
|
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||||
import {
|
import {
|
||||||
@@ -24,9 +18,6 @@ import { loadVirtualizer } from "../../resources/virtualizer";
|
|||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "./state-history-chart-line";
|
import "./state-history-chart-line";
|
||||||
import "./state-history-chart-timeline";
|
import "./state-history-chart-timeline";
|
||||||
import type { StateHistoryChartLine } from "./state-history-chart-line";
|
|
||||||
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
|
|
||||||
import { ChartResizeOptions } from "./ha-chart-base";
|
|
||||||
|
|
||||||
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
|
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
|
||||||
|
|
||||||
@@ -84,16 +75,6 @@ export class StateHistoryCharts extends LitElement {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||||
|
|
||||||
@queryAll("state-history-chart-line")
|
|
||||||
private _charts?: StateHistoryChartLine[];
|
|
||||||
|
|
||||||
public resize = (options?: ChartResizeOptions): void => {
|
|
||||||
this._charts?.forEach(
|
|
||||||
(chart: StateHistoryChartLine | StateHistoryChartTimeline) =>
|
|
||||||
chart.resize(options)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!isComponentLoaded(this.hass, "history")) {
|
if (!isComponentLoaded(this.hass, "history")) {
|
||||||
return html`<div class="info">
|
return html`<div class="info">
|
||||||
|
@@ -12,7 +12,7 @@ import {
|
|||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state, query } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
} from "../../data/recorder";
|
} from "../../data/recorder";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "./ha-chart-base";
|
import "./ha-chart-base";
|
||||||
import type { ChartResizeOptions, HaChartBase } from "./ha-chart-base";
|
|
||||||
|
|
||||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||||
mean: "mean",
|
mean: "mean",
|
||||||
@@ -43,7 +42,7 @@ export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@customElement("statistics-chart")
|
@customElement("statistics-chart")
|
||||||
export class StatisticsChart extends LitElement {
|
class StatisticsChart extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public statisticsData?: Statistics;
|
@property({ attribute: false }) public statisticsData?: Statistics;
|
||||||
@@ -76,14 +75,8 @@ export class StatisticsChart extends LitElement {
|
|||||||
|
|
||||||
@state() private _chartOptions?: ChartOptions;
|
@state() private _chartOptions?: ChartOptions;
|
||||||
|
|
||||||
@query("ha-chart-base") private _chart?: HaChartBase;
|
|
||||||
|
|
||||||
private _computedStyle?: CSSStyleDeclaration;
|
private _computedStyle?: CSSStyleDeclaration;
|
||||||
|
|
||||||
public resize = (options?: ChartResizeOptions): void => {
|
|
||||||
this._chart?.resize(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
return changedProps.size > 1 || !changedProps.has("hass");
|
return changedProps.size > 1 || !changedProps.has("hass");
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,3 @@
|
|||||||
import type {
|
|
||||||
BarControllerChartOptions,
|
|
||||||
BarControllerDatasetOptions,
|
|
||||||
} from "chart.js";
|
|
||||||
|
|
||||||
export interface TimeLineData {
|
export interface TimeLineData {
|
||||||
start: Date;
|
start: Date;
|
||||||
end: Date;
|
end: Date;
|
||||||
|
@@ -16,7 +16,7 @@ export interface TextBaroptions extends BarOptions {
|
|||||||
export class TextBarElement extends BarElement {
|
export class TextBarElement extends BarElement {
|
||||||
static id = "textbar";
|
static id = "textbar";
|
||||||
|
|
||||||
draw(ctx: CanvasRenderingContext2D) {
|
draw(ctx) {
|
||||||
super.draw(ctx);
|
super.draw(ctx);
|
||||||
const options = this.options as TextBaroptions;
|
const options = this.options as TextBaroptions;
|
||||||
const { x, y, base, width, text } = (
|
const { x, y, base, width, text } = (
|
||||||
|
@@ -2,95 +2,6 @@ import { BarController, BarElement } from "chart.js";
|
|||||||
import { TimeLineData } from "./const";
|
import { TimeLineData } from "./const";
|
||||||
import { TextBarProps } from "./textbar-element";
|
import { TextBarProps } from "./textbar-element";
|
||||||
|
|
||||||
function borderProps(properties) {
|
|
||||||
let reverse;
|
|
||||||
let start;
|
|
||||||
let end;
|
|
||||||
let top;
|
|
||||||
let bottom;
|
|
||||||
if (properties.horizontal) {
|
|
||||||
reverse = properties.base > properties.x;
|
|
||||||
start = "left";
|
|
||||||
end = "right";
|
|
||||||
} else {
|
|
||||||
reverse = properties.base < properties.y;
|
|
||||||
start = "bottom";
|
|
||||||
end = "top";
|
|
||||||
}
|
|
||||||
if (reverse) {
|
|
||||||
top = "end";
|
|
||||||
bottom = "start";
|
|
||||||
} else {
|
|
||||||
top = "start";
|
|
||||||
bottom = "end";
|
|
||||||
}
|
|
||||||
return { start, end, reverse, top, bottom };
|
|
||||||
}
|
|
||||||
|
|
||||||
function setBorderSkipped(properties, options, stack, index) {
|
|
||||||
let edge = options.borderSkipped;
|
|
||||||
const res = {};
|
|
||||||
|
|
||||||
if (!edge) {
|
|
||||||
properties.borderSkipped = res;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (edge === true) {
|
|
||||||
properties.borderSkipped = {
|
|
||||||
top: true,
|
|
||||||
right: true,
|
|
||||||
bottom: true,
|
|
||||||
left: true,
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { start, end, reverse, top, bottom } = borderProps(properties);
|
|
||||||
|
|
||||||
if (edge === "middle" && stack) {
|
|
||||||
properties.enableBorderRadius = true;
|
|
||||||
if ((stack._top || 0) === index) {
|
|
||||||
edge = top;
|
|
||||||
} else if ((stack._bottom || 0) === index) {
|
|
||||||
edge = bottom;
|
|
||||||
} else {
|
|
||||||
res[parseEdge(bottom, start, end, reverse)] = true;
|
|
||||||
edge = top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res[parseEdge(edge, start, end, reverse)] = true;
|
|
||||||
properties.borderSkipped = res;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseEdge(edge, a, b, reverse) {
|
|
||||||
if (reverse) {
|
|
||||||
edge = swap(edge, a, b);
|
|
||||||
edge = startEnd(edge, b, a);
|
|
||||||
} else {
|
|
||||||
edge = startEnd(edge, a, b);
|
|
||||||
}
|
|
||||||
return edge;
|
|
||||||
}
|
|
||||||
|
|
||||||
function swap(orig, v1, v2) {
|
|
||||||
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startEnd(v, start, end) {
|
|
||||||
return v === "start" ? start : v === "end" ? end : v;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setInflateAmount(
|
|
||||||
properties,
|
|
||||||
{ inflateAmount }: { inflateAmount?: string | number },
|
|
||||||
ratio
|
|
||||||
) {
|
|
||||||
properties.inflateAmount =
|
|
||||||
inflateAmount === "auto" ? (ratio === 1 ? 0.33 : 0) : inflateAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseValue(entry, item, vScale, i) {
|
function parseValue(entry, item, vScale, i) {
|
||||||
const startValue = vScale.parse(entry.start, i);
|
const startValue = vScale.parse(entry.start, i);
|
||||||
const endValue = vScale.parse(entry.end, i);
|
const endValue = vScale.parse(entry.end, i);
|
||||||
@@ -186,7 +97,7 @@ export class TimelineController extends BarController {
|
|||||||
bars: BarElement[],
|
bars: BarElement[],
|
||||||
start: number,
|
start: number,
|
||||||
count: number,
|
count: number,
|
||||||
mode: "reset" | "resize" | "none" | "hide" | "show" | "default" | "active"
|
mode: "reset" | "resize" | "none" | "hide" | "show" | "normal" | "active"
|
||||||
) {
|
) {
|
||||||
const vScale = this._cachedMeta.vScale!;
|
const vScale = this._cachedMeta.vScale!;
|
||||||
const iScale = this._cachedMeta.iScale!;
|
const iScale = this._cachedMeta.iScale!;
|
||||||
@@ -203,15 +114,15 @@ export class TimelineController extends BarController {
|
|||||||
for (let index = start; index < start + count; index++) {
|
for (let index = start; index < start + count; index++) {
|
||||||
const data = dataset.data[index] as TimeLineData;
|
const data = dataset.data[index] as TimeLineData;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const y = vScale.getPixelForValue(this.index);
|
const y = vScale.getPixelForValue(this.index);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const xStart = iScale.getPixelForValue(data.start.getTime());
|
const xStart = iScale.getPixelForValue(data.start.getTime());
|
||||||
|
// @ts-ignore
|
||||||
const xEnd = iScale.getPixelForValue(data.end.getTime());
|
const xEnd = iScale.getPixelForValue(data.end.getTime());
|
||||||
const width = xEnd - xStart;
|
const width = xEnd - xStart;
|
||||||
|
|
||||||
const parsed = this.getParsed(index);
|
|
||||||
const stack = (parsed._stacks || {})[vScale.axis];
|
|
||||||
|
|
||||||
const height = 10;
|
const height = 10;
|
||||||
|
|
||||||
const properties: TextBarProps = {
|
const properties: TextBarProps = {
|
||||||
@@ -234,10 +145,7 @@ export class TimelineController extends BarController {
|
|||||||
backgroundColor: data.color,
|
backgroundColor: data.color,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const options = properties.options || bars[index].options;
|
|
||||||
|
|
||||||
setBorderSkipped(properties, options, stack, index);
|
|
||||||
setInflateAmount(properties, options, 1);
|
|
||||||
this.updateElement(bars[index], index, properties as any, mode);
|
this.updateElement(bars[index], index, properties as any, mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -74,7 +74,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
|||||||
title: TemplateResult | string;
|
title: TemplateResult | string;
|
||||||
label?: TemplateResult | string;
|
label?: TemplateResult | string;
|
||||||
type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
|
type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
|
||||||
template?: (row: T) => TemplateResult | string | typeof nothing;
|
template?: (data: any, row: T) => TemplateResult | string | typeof nothing;
|
||||||
width?: string;
|
width?: string;
|
||||||
maxWidth?: string;
|
maxWidth?: string;
|
||||||
grows?: boolean;
|
grows?: boolean;
|
||||||
@@ -431,7 +431,7 @@ export class HaDataTable extends LitElement {
|
|||||||
})
|
})
|
||||||
: ""}
|
: ""}
|
||||||
>
|
>
|
||||||
${column.template ? column.template(row) : row[key]}
|
${column.template ? column.template(row[key], row) : row[key]}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
@@ -458,8 +458,7 @@ export class HaDataTable extends LitElement {
|
|||||||
filteredData,
|
filteredData,
|
||||||
this._sortColumns[this._sortColumn],
|
this._sortColumns[this._sortColumn],
|
||||||
this._sortDirection,
|
this._sortDirection,
|
||||||
this._sortColumn,
|
this._sortColumn
|
||||||
this.hass.locale.language
|
|
||||||
)
|
)
|
||||||
: filteredData;
|
: filteredData;
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
// To use comlink under ES5
|
// To use comlink under ES5
|
||||||
import { expose } from "comlink";
|
|
||||||
import "proxy-polyfill";
|
import "proxy-polyfill";
|
||||||
import { stringCompare } from "../../common/string/compare";
|
import { expose } from "comlink";
|
||||||
import type {
|
import type {
|
||||||
ClonedDataTableColumnData,
|
ClonedDataTableColumnData,
|
||||||
DataTableRowData,
|
DataTableRowData,
|
||||||
@@ -40,8 +39,7 @@ const sortData = (
|
|||||||
data: DataTableRowData[],
|
data: DataTableRowData[],
|
||||||
column: ClonedDataTableColumnData,
|
column: ClonedDataTableColumnData,
|
||||||
direction: SortingDirection,
|
direction: SortingDirection,
|
||||||
sortColumn: string,
|
sortColumn: string
|
||||||
language?: string
|
|
||||||
) =>
|
) =>
|
||||||
data.sort((a, b) => {
|
data.sort((a, b) => {
|
||||||
let sort = 1;
|
let sort = 1;
|
||||||
@@ -60,8 +58,13 @@ const sortData = (
|
|||||||
if (column.type === "numeric") {
|
if (column.type === "numeric") {
|
||||||
valA = isNaN(valA) ? undefined : Number(valA);
|
valA = isNaN(valA) ? undefined : Number(valA);
|
||||||
valB = isNaN(valB) ? undefined : Number(valB);
|
valB = isNaN(valB) ? undefined : Number(valB);
|
||||||
} else if (typeof valA === "string" && typeof valB === "string") {
|
} else {
|
||||||
return sort * stringCompare(valA, valB, language);
|
if (typeof valA === "string") {
|
||||||
|
valA = valA.toUpperCase();
|
||||||
|
}
|
||||||
|
if (typeof valB === "string") {
|
||||||
|
valB = valB.toUpperCase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure "undefined" and "null" are always sorted to the bottom
|
// Ensure "undefined" and "null" are always sorted to the bottom
|
||||||
|
@@ -27,12 +27,10 @@ export const filterData = (
|
|||||||
filter: FilterDataParamTypes[2]
|
filter: FilterDataParamTypes[2]
|
||||||
): Promise<ReturnType<FilterDataType>> =>
|
): Promise<ReturnType<FilterDataType>> =>
|
||||||
getWorker().filterData(data, columns, filter);
|
getWorker().filterData(data, columns, filter);
|
||||||
|
|
||||||
export const sortData = (
|
export const sortData = (
|
||||||
data: SortDataParamTypes[0],
|
data: SortDataParamTypes[0],
|
||||||
columns: SortDataParamTypes[1],
|
columns: SortDataParamTypes[1],
|
||||||
direction: SortDataParamTypes[2],
|
direction: SortDataParamTypes[2],
|
||||||
sortColumn: SortDataParamTypes[3],
|
sortColumn: SortDataParamTypes[3]
|
||||||
language?: SortDataParamTypes[4]
|
|
||||||
): Promise<ReturnType<SortDataType>> =>
|
): Promise<ReturnType<SortDataType>> =>
|
||||||
getWorker().sortData(data, columns, direction, sortColumn, language);
|
getWorker().sortData(data, columns, direction, sortColumn);
|
||||||
|
@@ -324,7 +324,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
.renderer=${rowRenderer}
|
.renderer=${rowRenderer}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
item-id-path="id"
|
|
||||||
item-value-path="id"
|
item-value-path="id"
|
||||||
item-label-path="name"
|
item-label-path="name"
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { LitElement, PropertyValues, html, nothing } from "lit";
|
import { html, LitElement, PropertyValues, nothing } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { computeStateDisplay } from "../../common/entity/compute_state_display";
|
||||||
import { getStates } from "../../common/entity/get_states";
|
import { getStates } from "../../common/entity/get_states";
|
||||||
import { HomeAssistant, ValueChangedEvent } from "../../types";
|
import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display";
|
||||||
|
import { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||||
import "../ha-combo-box";
|
import "../ha-combo-box";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
|
|
||||||
@@ -56,9 +58,20 @@ class HaEntityStatePicker extends LitElement {
|
|||||||
? getStates(state, this.attribute).map((key) => ({
|
? getStates(state, this.attribute).map((key) => ({
|
||||||
value: key,
|
value: key,
|
||||||
label: !this.attribute
|
label: !this.attribute
|
||||||
? this.hass.formatEntityState(state, key)
|
? computeStateDisplay(
|
||||||
: this.hass.formatEntityAttributeValue(
|
this.hass.localize,
|
||||||
state,
|
state,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
: computeAttributeValueDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
state,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities,
|
||||||
this.attribute,
|
this.attribute,
|
||||||
key
|
key
|
||||||
),
|
),
|
||||||
|
@@ -12,6 +12,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
|
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
|
||||||
import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||||
|
import { computeStateDisplay } from "../../common/entity/compute_state_display";
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||||
@@ -191,7 +192,13 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
this.hass!.locale,
|
this.hass!.locale,
|
||||||
getNumberFormatOptions(entityState, entry)
|
getNumberFormatOptions(entityState, entry)
|
||||||
)
|
)
|
||||||
: this.hass!.formatEntityState(entityState);
|
: computeStateDisplay(
|
||||||
|
this.hass!.localize,
|
||||||
|
entityState,
|
||||||
|
this.hass!.locale,
|
||||||
|
this.hass!.config,
|
||||||
|
this.hass!.entities
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,19 +1,12 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import {
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
nothing,
|
|
||||||
PropertyValues,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||||
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
|
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
|
||||||
import { haStyle } from "../resources/styles";
|
import { haStyle } from "../resources/styles";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-attribute-value";
|
|
||||||
import "./ha-expansion-panel";
|
import "./ha-expansion-panel";
|
||||||
|
import "./ha-attribute-value";
|
||||||
|
|
||||||
@customElement("ha-attributes")
|
@customElement("ha-attributes")
|
||||||
class HaAttributes extends LitElement {
|
class HaAttributes extends LitElement {
|
||||||
@@ -25,30 +18,16 @@ class HaAttributes extends LitElement {
|
|||||||
|
|
||||||
@state() private _expanded = false;
|
@state() private _expanded = false;
|
||||||
|
|
||||||
private get _filteredAttributes() {
|
|
||||||
return this.computeDisplayAttributes(
|
|
||||||
STATE_ATTRIBUTES.concat(
|
|
||||||
this.extraFilters ? this.extraFilters.split(",") : []
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
|
||||||
if (
|
|
||||||
changedProperties.has("extraFilters") ||
|
|
||||||
changedProperties.has("stateObj")
|
|
||||||
) {
|
|
||||||
this.toggleAttribute("empty", this._filteredAttributes.length === 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.stateObj) {
|
if (!this.stateObj) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributes = this._filteredAttributes;
|
const attributes = this.computeDisplayAttributes(
|
||||||
|
STATE_ATTRIBUTES.concat(
|
||||||
|
this.extraFilters ? this.extraFilters.split(",") : []
|
||||||
|
)
|
||||||
|
);
|
||||||
if (attributes.length === 0) {
|
if (attributes.length === 0) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,9 @@
|
|||||||
import {
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
nothing,
|
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
||||||
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { CLIMATE_PRESET_NONE, ClimateEntity } from "../data/climate";
|
import { CLIMATE_PRESET_NONE, ClimateEntity } from "../data/climate";
|
||||||
import { isUnavailableState, OFF } from "../data/entity";
|
import { isUnavailableState } from "../data/entity";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
@customElement("ha-climate-state")
|
@customElement("ha-climate-state")
|
||||||
@@ -27,24 +22,26 @@ class HaClimateState extends LitElement {
|
|||||||
${this.stateObj.attributes.preset_mode &&
|
${this.stateObj.attributes.preset_mode &&
|
||||||
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||||
? html`-
|
? html`-
|
||||||
${this.hass.formatEntityAttributeValue(
|
${computeAttributeValueDisplay(
|
||||||
|
this.hass.localize,
|
||||||
this.stateObj,
|
this.stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities,
|
||||||
"preset_mode"
|
"preset_mode"
|
||||||
)}`
|
)}`
|
||||||
: nothing}
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
<div class="unit">${this._computeTarget()}</div>`
|
<div class="unit">${this._computeTarget()}</div>`
|
||||||
: this._localizeState()}
|
: this._localizeState()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${currentStatus && !isUnavailableState(this.stateObj.state)
|
${currentStatus && !isUnavailableState(this.stateObj.state)
|
||||||
? html`
|
? html`<div class="current">
|
||||||
<div class="current">
|
${this.hass.localize("ui.card.climate.currently")}:
|
||||||
${this.hass.localize("ui.card.climate.currently")}:
|
<div class="unit">${currentStatus}</div>
|
||||||
<div class="unit">${currentStatus}</div>
|
</div>`
|
||||||
</div>
|
: ""}`;
|
||||||
`
|
|
||||||
: nothing}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeCurrentStatus(): string | undefined {
|
private _computeCurrentStatus(): string | undefined {
|
||||||
@@ -128,17 +125,24 @@ class HaClimateState extends LitElement {
|
|||||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateString = this.hass.formatEntityState(this.stateObj);
|
const stateString = computeStateDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities
|
||||||
|
);
|
||||||
|
|
||||||
if (this.stateObj.attributes.hvac_action && this.stateObj.state !== OFF) {
|
return this.stateObj.attributes.hvac_action
|
||||||
const actionString = this.hass.formatEntityAttributeValue(
|
? `${computeAttributeValueDisplay(
|
||||||
this.stateObj,
|
this.hass.localize,
|
||||||
"hvac_action"
|
this.stateObj,
|
||||||
);
|
this.hass.locale,
|
||||||
return `${actionString} (${stateString})`;
|
this.hass.config,
|
||||||
}
|
this.hass.entities,
|
||||||
|
"hvac_action"
|
||||||
return stateString;
|
)} (${stateString})`
|
||||||
|
: stateString;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@@ -244,6 +244,7 @@ export class HaComboBox extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (overlay) {
|
if (overlay) {
|
||||||
|
overlay.setAttribute("required-vertical-space", "0");
|
||||||
this._removeInert(overlay);
|
this._removeInert(overlay);
|
||||||
}
|
}
|
||||||
this._observeBody();
|
this._observeBody();
|
||||||
@@ -311,10 +312,6 @@ export class HaComboBox extends LitElement {
|
|||||||
|
|
||||||
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
|
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (!this.allowCustomValue) {
|
|
||||||
// @ts-ignore
|
|
||||||
this._comboBox._closeOnBlurIsPrevented = true;
|
|
||||||
}
|
|
||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
if (newValue !== this.value) {
|
if (newValue !== this.value) {
|
||||||
@@ -330,7 +327,7 @@ export class HaComboBox extends LitElement {
|
|||||||
}
|
}
|
||||||
vaadin-combo-box-light {
|
vaadin-combo-box-light {
|
||||||
position: relative;
|
position: relative;
|
||||||
--vaadin-combo-box-overlay-max-height: calc(45vh - 56px);
|
--vaadin-combo-box-overlay-max-height: calc(45vh);
|
||||||
}
|
}
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@@ -1,260 +0,0 @@
|
|||||||
import { mdiMinus, mdiPlus } from "@mdi/js";
|
|
||||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
|
||||||
import { customElement, property, query } from "lit/decorators";
|
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
|
||||||
import { conditionalClamp } from "../common/number/clamp";
|
|
||||||
import { formatNumber } from "../common/number/format_number";
|
|
||||||
import { FrontendLocaleData } from "../data/translation";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
|
|
||||||
const A11Y_KEY_CODES = new Set([
|
|
||||||
"ArrowRight",
|
|
||||||
"ArrowUp",
|
|
||||||
"ArrowLeft",
|
|
||||||
"ArrowDown",
|
|
||||||
"PageUp",
|
|
||||||
"PageDown",
|
|
||||||
"Home",
|
|
||||||
"End",
|
|
||||||
]);
|
|
||||||
|
|
||||||
@customElement("ha-control-number-buttons")
|
|
||||||
export class HaControlNumberButton extends LitElement {
|
|
||||||
@property({ attribute: false }) public locale?: FrontendLocaleData;
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
||||||
|
|
||||||
@property() public label?: string;
|
|
||||||
|
|
||||||
@property({ type: Number }) public step?: number;
|
|
||||||
|
|
||||||
@property({ type: Number }) public value?: number;
|
|
||||||
|
|
||||||
@property({ type: Number }) public min?: number;
|
|
||||||
|
|
||||||
@property({ type: Number }) public max?: number;
|
|
||||||
|
|
||||||
@property({ attribute: "false" })
|
|
||||||
public formatOptions: Intl.NumberFormatOptions = {};
|
|
||||||
|
|
||||||
@query("#input") _input!: HTMLDivElement;
|
|
||||||
|
|
||||||
private boundedValue(value: number) {
|
|
||||||
const clamped = conditionalClamp(value, this.min, this.max);
|
|
||||||
return Math.round(clamped / this._step) * this._step;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _step() {
|
|
||||||
return this.step ?? 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _value() {
|
|
||||||
return this.value ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _tenPercentStep() {
|
|
||||||
if (this.max == null || this.min == null) return this._step;
|
|
||||||
const range = this.max - this.min / 10;
|
|
||||||
|
|
||||||
if (range <= this._step) return this._step;
|
|
||||||
return Math.max(range / 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handlePlusButton() {
|
|
||||||
this._increment();
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
|
||||||
this._input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleMinusButton() {
|
|
||||||
this._decrement();
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
|
||||||
this._input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _increment() {
|
|
||||||
this.value = this.boundedValue(this._value + this._step);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _decrement() {
|
|
||||||
this.value = this.boundedValue(this._value - this._step);
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleKeyDown(e: KeyboardEvent) {
|
|
||||||
if (this.disabled) return;
|
|
||||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
|
||||||
e.preventDefault();
|
|
||||||
switch (e.code) {
|
|
||||||
case "ArrowRight":
|
|
||||||
case "ArrowUp":
|
|
||||||
this._increment();
|
|
||||||
break;
|
|
||||||
case "ArrowLeft":
|
|
||||||
case "ArrowDown":
|
|
||||||
this._decrement();
|
|
||||||
break;
|
|
||||||
case "PageUp":
|
|
||||||
this.value = this.boundedValue(this._value + this._tenPercentStep);
|
|
||||||
break;
|
|
||||||
case "PageDown":
|
|
||||||
this.value = this.boundedValue(this._value - this._tenPercentStep);
|
|
||||||
break;
|
|
||||||
case "Home":
|
|
||||||
if (this.min != null) {
|
|
||||||
this.value = this.min;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "End":
|
|
||||||
if (this.max != null) {
|
|
||||||
this.value = this.max;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
const displayedValue =
|
|
||||||
this.value != null
|
|
||||||
? formatNumber(this.value, this.locale, this.formatOptions)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="container">
|
|
||||||
<div
|
|
||||||
id="input"
|
|
||||||
class="value"
|
|
||||||
role="number-button"
|
|
||||||
.tabIndex=${this.disabled ? "-1" : "0"}
|
|
||||||
aria-valuenow=${this.value}
|
|
||||||
aria-valuemin=${this.min}
|
|
||||||
aria-valuemax=${this.max}
|
|
||||||
aria-label=${ifDefined(this.label)}
|
|
||||||
?disabled=${this.disabled}
|
|
||||||
@keydown=${this._handleKeyDown}
|
|
||||||
>
|
|
||||||
${displayedValue}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="button minus"
|
|
||||||
type="button"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-label="decrement"
|
|
||||||
@click=${this._handleMinusButton}
|
|
||||||
.disabled=${this.disabled ||
|
|
||||||
(this.min != null && this._value <= this.min)}
|
|
||||||
>
|
|
||||||
<ha-svg-icon aria-hidden .path=${mdiMinus}></ha-svg-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="button plus"
|
|
||||||
type="button"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-label="increment"
|
|
||||||
@click=${this._handlePlusButton}
|
|
||||||
.disabled=${this.disabled ||
|
|
||||||
(this.max != null && this._value >= this.max)}
|
|
||||||
>
|
|
||||||
<ha-svg-icon aria-hidden .path=${mdiPlus}></ha-svg-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
--control-number-buttons-focus-color: var(--primary-color);
|
|
||||||
--control-number-buttons-background-color: var(--disabled-color);
|
|
||||||
--control-number-buttons-background-opacity: 0.2;
|
|
||||||
--control-number-buttons-border-radius: 10px;
|
|
||||||
--mdc-icon-size: 16px;
|
|
||||||
height: 40px;
|
|
||||||
width: 200px;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: color 180ms ease-in-out;
|
|
||||||
}
|
|
||||||
:host([disabled]) {
|
|
||||||
color: var(--disabled-color);
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.value {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 44px;
|
|
||||||
border-radius: var(--control-number-buttons-border-radius);
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
line-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
/* For safari border-radius overflow */
|
|
||||||
z-index: 0;
|
|
||||||
font-size: inherit;
|
|
||||||
color: inherit;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.value::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--control-number-buttons-background-color);
|
|
||||||
transition:
|
|
||||||
background-color 180ms ease-in-out,
|
|
||||||
opacity 180ms ease-in-out;
|
|
||||||
opacity: var(--control-number-buttons-background-opacity);
|
|
||||||
}
|
|
||||||
.value:focus-visible {
|
|
||||||
box-shadow: 0 0 0 2px var(--control-number-buttons-focus-color);
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
color: inherit;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 35px;
|
|
||||||
height: 40px;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.button[disabled] {
|
|
||||||
opacity: 0.4;
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.button.minus {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.button.plus {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-control-number-buttons": HaControlNumberButton;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,12 +1,10 @@
|
|||||||
import { Ripple } from "@material/mwc-ripple";
|
import { Ripple } from "@material/mwc-ripple";
|
||||||
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||||
import { mdiMenuDown } from "@mdi/js";
|
|
||||||
import { css, html, nothing } from "lit";
|
import { css, html, nothing } from "lit";
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
eventOptions,
|
eventOptions,
|
||||||
property,
|
|
||||||
query,
|
query,
|
||||||
queryAsync,
|
queryAsync,
|
||||||
state,
|
state,
|
||||||
@@ -26,12 +24,6 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
|
|
||||||
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
|
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "show-arrow" })
|
|
||||||
public showArrow?: boolean;
|
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "hide-label" })
|
|
||||||
public hideLabel?: boolean;
|
|
||||||
|
|
||||||
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||||
|
|
||||||
@state() private _shouldRenderRipple = false;
|
@state() private _shouldRenderRipple = false;
|
||||||
@@ -44,9 +36,7 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
"select-no-value": !this.selectedText,
|
"select-no-value": !this.selectedText,
|
||||||
};
|
};
|
||||||
|
|
||||||
const labelledby = this.label && !this.hideLabel ? "label" : undefined;
|
const labelledby = this.label ? "label" : undefined;
|
||||||
const labelAttribute =
|
|
||||||
this.label && this.hideLabel ? this.label : undefined;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="select ${classMap(classes)}">
|
<div class="select ${classMap(classes)}">
|
||||||
@@ -67,7 +57,6 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
aria-invalid=${!this.isUiValid}
|
aria-invalid=${!this.isUiValid}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-labelledby=${ifDefined(labelledby)}
|
aria-labelledby=${ifDefined(labelledby)}
|
||||||
aria-label=${ifDefined(labelAttribute)}
|
|
||||||
aria-required=${this.required}
|
aria-required=${this.required}
|
||||||
@click=${this.onClick}
|
@click=${this.onClick}
|
||||||
@focus=${this.onFocus}
|
@focus=${this.onFocus}
|
||||||
@@ -83,14 +72,11 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
>
|
>
|
||||||
${this.renderIcon()}
|
${this.renderIcon()}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${this.hideLabel
|
<p id="label" class="label">${this.label}</p>
|
||||||
? nothing
|
|
||||||
: html`<p id="label" class="label">${this.label}</p>`}
|
|
||||||
${this.selectedText
|
${this.selectedText
|
||||||
? html`<p class="value">${this.selectedText}</p>`
|
? html`<p class="value">${this.selectedText}</p>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this.renderArrow()}
|
|
||||||
${this._shouldRenderRipple && !this.disabled
|
${this._shouldRenderRipple && !this.disabled
|
||||||
? html` <mwc-ripple></mwc-ripple> `
|
? html` <mwc-ripple></mwc-ripple> `
|
||||||
: nothing}
|
: nothing}
|
||||||
@@ -100,29 +86,13 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderArrow() {
|
|
||||||
if (!this.showArrow) return nothing;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="icon">
|
|
||||||
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderIcon() {
|
private renderIcon() {
|
||||||
const index = this.mdcFoundation?.getSelectedIndex();
|
const index = this.mdcFoundation?.getSelectedIndex();
|
||||||
const items = this.menuElement?.items ?? [];
|
const items = this.menuElement?.items ?? [];
|
||||||
const item = index != null ? items[index] : undefined;
|
const item = index != null ? items[index] : undefined;
|
||||||
const defaultIcon = this.querySelector("[slot='icon']");
|
const icon =
|
||||||
const icon = (item?.querySelector("[slot='graphic']") ?? null) as
|
item?.querySelector("[slot='graphic']") ??
|
||||||
| HaSvgIcon
|
(null as HaSvgIcon | HaIcon | null);
|
||||||
| HaIcon
|
|
||||||
| null;
|
|
||||||
|
|
||||||
if (!defaultIcon && !icon) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
@@ -201,18 +171,17 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
--control-select-menu-background-color: var(--disabled-color);
|
--control-select-menu-background-color: var(--disabled-color);
|
||||||
--control-select-menu-background-opacity: 0.2;
|
--control-select-menu-background-opacity: 0.2;
|
||||||
--control-select-menu-border-radius: 14px;
|
--control-select-menu-border-radius: 14px;
|
||||||
--control-select-menu-height: 48px;
|
--control-select-menu-min-width: 120px;
|
||||||
--control-select-menu-padding: 6px 10px;
|
--control-select-menu-max-width: 200px;
|
||||||
|
--control-select-menu-width: 100%;
|
||||||
--mdc-icon-size: 20px;
|
--mdc-icon-size: 20px;
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
width: auto;
|
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
.select-anchor {
|
.select-anchor {
|
||||||
height: var(--control-select-menu-height);
|
color: var(--control-select-menu-text-color);
|
||||||
padding: var(--control-select-menu-padding);
|
height: 48px;
|
||||||
|
padding: 6px 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -227,12 +196,18 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
--mdc-ripple-color: var(--control-select-menu-background-color);
|
--mdc-ripple-color: var(--control-select-menu-background-color);
|
||||||
/* For safari border-radius overflow */
|
/* For safari border-radius overflow */
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
font-size: inherit;
|
||||||
transition: color 180ms ease-in-out;
|
transition: color 180ms ease-in-out;
|
||||||
|
color: var(--control-text-icon-color);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
width: 100%;
|
min-width: var(--control-select-menu-min-width);
|
||||||
|
max-width: var(--control-select-menu-max-width);
|
||||||
|
width: var(--control-select-menu-width);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
font-size: 14px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
letter-spacing: 0.25px;
|
letter-spacing: 0.25px;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
@@ -254,7 +229,8 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 0.85em;
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -217,7 +217,6 @@ export class HaControlSelect extends LitElement {
|
|||||||
transition: box-shadow 180ms ease-in-out;
|
transition: box-shadow 180ms ease-in-out;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--primary-text-color);
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
@@ -268,6 +267,7 @@ export class HaControlSelect extends LitElement {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: var(--control-select-button-border-radius);
|
border-radius: var(--control-select-button-border-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: var(--primary-text-color);
|
||||||
/* For safari border-radius overflow */
|
/* For safari border-radius overflow */
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
@@ -331,7 +331,6 @@ export class HaControlSelect extends LitElement {
|
|||||||
:host([disabled]) {
|
:host([disabled]) {
|
||||||
--control-select-color: var(--disabled-color);
|
--control-select-color: var(--disabled-color);
|
||||||
--control-select-focused-opacity: 0;
|
--control-select-focused-opacity: 0;
|
||||||
color: var(--disabled-color);
|
|
||||||
}
|
}
|
||||||
:host([disabled]) .option {
|
:host([disabled]) .option {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
@@ -155,12 +155,11 @@ export class HaConversationAgentPicker extends LitElement {
|
|||||||
if (!this._configEntry) {
|
if (!this._configEntry) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showOptionsFlowDialog(this, this._configEntry, {
|
showOptionsFlowDialog(
|
||||||
manifest: await fetchIntegrationManifest(
|
this,
|
||||||
this.hass,
|
this._configEntry,
|
||||||
this._configEntry.domain
|
await fetchIntegrationManifest(this.hass, this._configEntry.domain)
|
||||||
),
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@@ -10,12 +10,12 @@ import "./ha-icon-button";
|
|||||||
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
|
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
|
||||||
|
|
||||||
export const createCloseHeading = (
|
export const createCloseHeading = (
|
||||||
hass: HomeAssistant | undefined,
|
hass: HomeAssistant,
|
||||||
title: string | TemplateResult
|
title: string | TemplateResult
|
||||||
) => html`
|
) => html`
|
||||||
<div class="header_title">${title}</div>
|
<div class="header_title">${title}</div>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
|
.label=${hass.localize("ui.dialogs.generic.close")}
|
||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
dialogAction="close"
|
dialogAction="close"
|
||||||
class="header_button"
|
class="header_button"
|
||||||
|
@@ -1,19 +1,16 @@
|
|||||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
|
||||||
import { mdiDelete, mdiFileUpload } from "@mdi/js";
|
import { mdiClose } from "@mdi/js";
|
||||||
import { LitElement, PropertyValues, TemplateResult, css, html } from "lit";
|
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-button";
|
import "./ha-circular-progress";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
|
||||||
import { ensureArray } from "../common/array/ensure-array";
|
|
||||||
import { bytesToString } from "../util/bytes-to-string";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"file-picked": { files: File[] };
|
"file-picked": { files: FileList };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,22 +22,12 @@ export class HaFileUpload extends LitElement {
|
|||||||
|
|
||||||
@property() public icon?: string;
|
@property() public icon?: string;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label!: string;
|
||||||
|
|
||||||
@property() public secondary?: string;
|
@property() public value: string | TemplateResult | null = null;
|
||||||
|
|
||||||
@property() public supports?: string;
|
|
||||||
|
|
||||||
@property() public value?: File | File[] | FileList | string;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) private multiple = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public disabled: boolean = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) private uploading = false;
|
@property({ type: Boolean }) private uploading = false;
|
||||||
|
|
||||||
@property({ type: Number }) private progress?: number;
|
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "auto-open-file-dialog" })
|
@property({ type: Boolean, attribute: "auto-open-file-dialog" })
|
||||||
private autoOpenFileDialog = false;
|
private autoOpenFileDialog = false;
|
||||||
|
|
||||||
@@ -58,102 +45,72 @@ export class HaFileUpload extends LitElement {
|
|||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
${this.uploading
|
${this.uploading
|
||||||
? html`<div class="container">
|
? html`<ha-circular-progress
|
||||||
<div class="row">
|
alt="Uploading"
|
||||||
<span class="header"
|
size="large"
|
||||||
>${this.value
|
active
|
||||||
? this.hass?.localize(
|
></ha-circular-progress>`
|
||||||
"ui.components.file-upload.uploading_name",
|
: html`
|
||||||
{ name: this.value }
|
<label
|
||||||
)
|
for="input"
|
||||||
: this.hass?.localize(
|
class="mdc-text-field mdc-text-field--filled ${classMap({
|
||||||
"ui.components.file-upload.uploading"
|
"mdc-text-field--focused": this._drag,
|
||||||
)}</span
|
"mdc-text-field--with-leading-icon": Boolean(this.icon),
|
||||||
|
"mdc-text-field--with-trailing-icon": Boolean(this.value),
|
||||||
|
})}"
|
||||||
|
@drop=${this._handleDrop}
|
||||||
|
@dragenter=${this._handleDragStart}
|
||||||
|
@dragover=${this._handleDragStart}
|
||||||
|
@dragleave=${this._handleDragEnd}
|
||||||
|
@dragend=${this._handleDragEnd}
|
||||||
|
>
|
||||||
|
<span class="mdc-text-field__ripple"></span>
|
||||||
|
<span
|
||||||
|
class="mdc-floating-label ${this.value || this._drag
|
||||||
|
? "mdc-floating-label--float-above"
|
||||||
|
: ""}"
|
||||||
|
id="label"
|
||||||
|
>${this.label}</span
|
||||||
>
|
>
|
||||||
${this.progress
|
${this.icon
|
||||||
? html`<span class="progress"
|
? html`<span
|
||||||
>${this.progress}${blankBeforePercent(
|
class="mdc-text-field__icon mdc-text-field__icon--leading"
|
||||||
this.hass!.locale
|
|
||||||
)}%</span
|
|
||||||
>`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
<mwc-linear-progress
|
|
||||||
.indeterminate=${!this.progress}
|
|
||||||
.progress=${this.progress ? this.progress / 100 : undefined}
|
|
||||||
></mwc-linear-progress>
|
|
||||||
</div>`
|
|
||||||
: html`<label
|
|
||||||
for=${this.value ? "" : "input"}
|
|
||||||
class="container ${classMap({
|
|
||||||
dragged: this._drag,
|
|
||||||
multiple: this.multiple,
|
|
||||||
value: Boolean(this.value),
|
|
||||||
})}"
|
|
||||||
@drop=${this._handleDrop}
|
|
||||||
@dragenter=${this._handleDragStart}
|
|
||||||
@dragover=${this._handleDragStart}
|
|
||||||
@dragleave=${this._handleDragEnd}
|
|
||||||
@dragend=${this._handleDragEnd}
|
|
||||||
>${!this.value
|
|
||||||
? html`<ha-svg-icon
|
|
||||||
class="big-icon"
|
|
||||||
.path=${this.icon || mdiFileUpload}
|
|
||||||
></ha-svg-icon>
|
|
||||||
<ha-button unelevated @click=${this._openFilePicker}>
|
|
||||||
${this.label ||
|
|
||||||
this.hass?.localize("ui.components.file-upload.label")}
|
|
||||||
</ha-button>
|
|
||||||
<span class="secondary"
|
|
||||||
>${this.secondary ||
|
|
||||||
this.hass?.localize(
|
|
||||||
"ui.components.file-upload.secondary"
|
|
||||||
)}</span
|
|
||||||
>
|
>
|
||||||
<span class="supports">${this.supports}</span>`
|
<ha-icon-button
|
||||||
: typeof this.value === "string"
|
@click=${this._openFilePicker}
|
||||||
? html`<div class="row">
|
.path=${this.icon}
|
||||||
<div class="value" @click=${this._openFilePicker}>
|
></ha-icon-button>
|
||||||
<ha-svg-icon
|
</span>`
|
||||||
.path=${this.icon || mdiFileUpload}
|
: ""}
|
||||||
></ha-svg-icon>
|
<div class="value">${this.value}</div>
|
||||||
${this.value}
|
<input
|
||||||
</div>
|
id="input"
|
||||||
<ha-icon-button
|
type="file"
|
||||||
@click=${this._clearValue}
|
class="mdc-text-field__input file"
|
||||||
.label=${this.hass?.localize("ui.common.delete") ||
|
accept=${this.accept}
|
||||||
"Delete"}
|
@change=${this._handleFilePicked}
|
||||||
.path=${mdiDelete}
|
aria-labelledby="label"
|
||||||
></ha-icon-button>
|
/>
|
||||||
</div>`
|
${this.value
|
||||||
: (this.value instanceof FileList
|
? html`<span
|
||||||
? Array.from(this.value)
|
class="mdc-text-field__icon mdc-text-field__icon--trailing"
|
||||||
: ensureArray(this.value)
|
>
|
||||||
).map(
|
<ha-icon-button
|
||||||
(file) =>
|
slot="suffix"
|
||||||
html`<div class="row">
|
@click=${this._clearValue}
|
||||||
<div class="value" @click=${this._openFilePicker}>
|
.label=${this.hass?.localize("ui.common.close") ||
|
||||||
<ha-svg-icon
|
"close"}
|
||||||
.path=${this.icon || mdiFileUpload}
|
.path=${mdiClose}
|
||||||
></ha-svg-icon>
|
></ha-icon-button>
|
||||||
${file.name} - ${bytesToString(file.size)}
|
</span>`
|
||||||
</div>
|
: ""}
|
||||||
<ha-icon-button
|
<span
|
||||||
@click=${this._clearValue}
|
class="mdc-line-ripple ${this._drag
|
||||||
.label=${this.hass?.localize("ui.common.delete") ||
|
? "mdc-line-ripple--active"
|
||||||
"Delete"}
|
: ""}"
|
||||||
.path=${mdiDelete}
|
></span>
|
||||||
></ha-icon-button>
|
</label>
|
||||||
</div>`
|
`}
|
||||||
)}
|
|
||||||
<input
|
|
||||||
id="input"
|
|
||||||
type="file"
|
|
||||||
class="file"
|
|
||||||
.accept=${this.accept}
|
|
||||||
.multiple=${this.multiple}
|
|
||||||
@change=${this._handleFilePicked}
|
|
||||||
/></label>`}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,12 +122,7 @@ export class HaFileUpload extends LitElement {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (ev.dataTransfer?.files) {
|
if (ev.dataTransfer?.files) {
|
||||||
fireEvent(this, "file-picked", {
|
fireEvent(this, "file-picked", { files: ev.dataTransfer.files });
|
||||||
files:
|
|
||||||
this.multiple || ev.dataTransfer.files.length === 1
|
|
||||||
? Array.from(ev.dataTransfer.files)
|
|
||||||
: [ev.dataTransfer.files[0]],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this._drag = false;
|
this._drag = false;
|
||||||
}
|
}
|
||||||
@@ -188,121 +140,93 @@ export class HaFileUpload extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleFilePicked(ev) {
|
private _handleFilePicked(ev) {
|
||||||
if (ev.target.files.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.value = ev.target.files;
|
|
||||||
fireEvent(this, "file-picked", { files: ev.target.files });
|
fireEvent(this, "file-picked", { files: ev.target.files });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearValue(ev: Event) {
|
private _clearValue(ev: Event) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
this.value = null;
|
||||||
this._input!.value = "";
|
this._input!.value = "";
|
||||||
this.value = undefined;
|
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return css`
|
return [
|
||||||
:host {
|
styles,
|
||||||
display: block;
|
css`
|
||||||
height: 240px;
|
:host {
|
||||||
}
|
display: block;
|
||||||
:host([disabled]) {
|
}
|
||||||
pointer-events: none;
|
.mdc-text-field--filled {
|
||||||
color: var(--disabled-text-color);
|
height: auto;
|
||||||
}
|
padding-top: 16px;
|
||||||
.container {
|
cursor: pointer;
|
||||||
position: relative;
|
}
|
||||||
display: flex;
|
.mdc-text-field--filled.mdc-text-field--with-trailing-icon {
|
||||||
flex-direction: column;
|
padding-top: 28px;
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
|
||||||
border: solid 1px
|
color: var(--secondary-text-color);
|
||||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
}
|
||||||
border-radius: var(--mdc-shape-small, 4px);
|
.mdc-text-field--filled.mdc-text-field--with-trailing-icon
|
||||||
height: 100%;
|
.mdc-text-field__icon {
|
||||||
}
|
align-self: flex-end;
|
||||||
label.container {
|
}
|
||||||
border: dashed 1px
|
.mdc-text-field__icon--leading {
|
||||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
margin-bottom: 12px;
|
||||||
cursor: pointer;
|
inset-inline-start: initial;
|
||||||
}
|
inset-inline-end: 0px;
|
||||||
:host([disabled]) .container {
|
direction: var(--direction);
|
||||||
border-color: var(--disabled-color);
|
}
|
||||||
}
|
.mdc-text-field--filled .mdc-floating-label--float-above {
|
||||||
label.dragged {
|
transform: scale(0.75);
|
||||||
border-color: var(--primary-color);
|
top: 8px;
|
||||||
}
|
}
|
||||||
.dragged:before {
|
.mdc-floating-label {
|
||||||
position: absolute;
|
inset-inline-start: 16px !important;
|
||||||
top: 0;
|
inset-inline-end: initial !important;
|
||||||
right: 0;
|
direction: var(--direction);
|
||||||
bottom: 0;
|
}
|
||||||
left: 0;
|
.mdc-text-field--filled .mdc-floating-label {
|
||||||
background-color: var(--primary-color);
|
inset-inline-start: 48px !important;
|
||||||
content: "";
|
inset-inline-end: initial !important;
|
||||||
opacity: var(--dark-divider-opacity);
|
direction: var(--direction);
|
||||||
pointer-events: none;
|
}
|
||||||
border-radius: var(--mdc-shape-small, 4px);
|
.mdc-text-field__icon--trailing {
|
||||||
}
|
pointer-events: auto !important;
|
||||||
label.value {
|
}
|
||||||
cursor: default;
|
.dragged:before {
|
||||||
}
|
position: var(--layout-fit_-_position);
|
||||||
label.value.multiple {
|
top: var(--layout-fit_-_top);
|
||||||
justify-content: unset;
|
right: var(--layout-fit_-_right);
|
||||||
overflow: auto;
|
bottom: var(--layout-fit_-_bottom);
|
||||||
}
|
left: var(--layout-fit_-_left);
|
||||||
.highlight {
|
background: currentColor;
|
||||||
color: var(--primary-color);
|
content: "";
|
||||||
}
|
opacity: var(--dark-divider-opacity);
|
||||||
.row {
|
pointer-events: none;
|
||||||
display: flex;
|
border-radius: 4px;
|
||||||
width: 100%;
|
}
|
||||||
align-items: center;
|
.value {
|
||||||
justify-content: space-between;
|
width: 100%;
|
||||||
padding: 0 16px;
|
}
|
||||||
box-sizing: border-box;
|
input.file {
|
||||||
}
|
display: none;
|
||||||
ha-button {
|
}
|
||||||
margin-bottom: 4px;
|
img {
|
||||||
}
|
max-width: 100%;
|
||||||
.supports {
|
max-height: 125px;
|
||||||
color: var(--secondary-text-color);
|
}
|
||||||
font-size: 12px;
|
ha-icon-button {
|
||||||
}
|
--mdc-icon-button-size: 24px;
|
||||||
:host([disabled]) .secondary {
|
--mdc-icon-size: 20px;
|
||||||
color: var(--disabled-text-color);
|
}
|
||||||
}
|
ha-circular-progress {
|
||||||
input.file {
|
display: block;
|
||||||
display: none;
|
text-align-last: center;
|
||||||
}
|
}
|
||||||
.value {
|
`,
|
||||||
cursor: pointer;
|
];
|
||||||
}
|
|
||||||
.value ha-svg-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
.big-icon {
|
|
||||||
--mdc-icon-size: 48px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
ha-button {
|
|
||||||
--mdc-button-outline-color: var(--primary-color);
|
|
||||||
--mdc-icon-button-size: 24px;
|
|
||||||
}
|
|
||||||
mwc-linear-progress {
|
|
||||||
width: 100%;
|
|
||||||
padding: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.progress {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -27,8 +27,7 @@ export const computeInitialHaFormData = (
|
|||||||
data[field.name] = 0.0;
|
data[field.name] = 0.0;
|
||||||
} else if (field.type === "select") {
|
} else if (field.type === "select") {
|
||||||
if (field.options.length) {
|
if (field.options.length) {
|
||||||
const val = field.options[0];
|
data[field.name] = field.options[0][0];
|
||||||
data[field.name] = Array.isArray(val) ? val[0] : val;
|
|
||||||
}
|
}
|
||||||
} else if (field.type === "positive_time_period_dict") {
|
} else if (field.type === "positive_time_period_dict") {
|
||||||
data[field.name] = {
|
data[field.name] = {
|
||||||
@@ -61,10 +60,8 @@ export const computeInitialHaFormData = (
|
|||||||
data[field.name] = selector.number?.min ?? 0;
|
data[field.name] = selector.number?.min ?? 0;
|
||||||
} else if ("select" in selector) {
|
} else if ("select" in selector) {
|
||||||
if (selector.select?.options.length) {
|
if (selector.select?.options.length) {
|
||||||
const firstOption = selector.select.options[0];
|
const val = selector.select.options[0];
|
||||||
const val =
|
data[field.name] = Array.isArray(val) ? val[0] : val;
|
||||||
typeof firstOption === "string" ? firstOption : firstOption.value;
|
|
||||||
data[field.name] = selector.select.multiple ? [val] : val;
|
|
||||||
}
|
}
|
||||||
} else if ("duration" in selector) {
|
} else if ("duration" in selector) {
|
||||||
data[field.name] = {
|
data[field.name] = {
|
||||||
|
@@ -68,7 +68,6 @@ export class HaFormString extends LitElement implements HaFormElement {
|
|||||||
: this.schema.description?.suffix}
|
: this.schema.description?.suffix}
|
||||||
.validationMessage=${this.schema.required ? "Required" : undefined}
|
.validationMessage=${this.schema.required ? "Required" : undefined}
|
||||||
@input=${this._valueChanged}
|
@input=${this._valueChanged}
|
||||||
@change=${this._valueChanged}
|
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
${isPassword
|
${isPassword
|
||||||
? html`<ha-icon-button
|
? html`<ha-icon-button
|
||||||
|
@@ -7,12 +7,6 @@ import { hsv2rgb, rgb2hex } from "../common/color/convert-color";
|
|||||||
import { rgbw2rgb, rgbww2rgb } from "../common/color/convert-light-color";
|
import { rgbw2rgb, rgbww2rgb } from "../common/color/convert-light-color";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HASSDomEvents {
|
|
||||||
"cursor-moved": { value?: any };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function xy2polar(x: number, y: number) {
|
function xy2polar(x: number, y: number) {
|
||||||
const r = Math.sqrt(x * x + y * y);
|
const r = Math.sqrt(x * x + y * y);
|
||||||
const phi = Math.atan2(y, x);
|
const phi = Math.atan2(y, x);
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
||||||
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { isUnavailableState, OFF } from "../data/entity";
|
import { isUnavailableState, OFF } from "../data/entity";
|
||||||
import { HumidifierEntity } from "../data/humidifier";
|
import { HumidifierEntity } from "../data/humidifier";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@@ -19,8 +21,12 @@ class HaHumidifierState extends LitElement {
|
|||||||
${this._localizeState()}
|
${this._localizeState()}
|
||||||
${this.stateObj.attributes.mode
|
${this.stateObj.attributes.mode
|
||||||
? html`-
|
? html`-
|
||||||
${this.hass.formatEntityAttributeValue(
|
${computeAttributeValueDisplay(
|
||||||
|
this.hass.localize,
|
||||||
this.stateObj,
|
this.stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities,
|
||||||
"mode"
|
"mode"
|
||||||
)}`
|
)}`
|
||||||
: ""}
|
: ""}
|
||||||
@@ -72,17 +78,24 @@ class HaHumidifierState extends LitElement {
|
|||||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateString = this.hass.formatEntityState(this.stateObj);
|
const stateString = computeStateDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities
|
||||||
|
);
|
||||||
|
|
||||||
if (this.stateObj.attributes.action && this.stateObj.state !== OFF) {
|
return this.stateObj.attributes.action && this.stateObj.state !== OFF
|
||||||
const actionString = this.hass.formatEntityAttributeValue(
|
? `${computeAttributeValueDisplay(
|
||||||
this.stateObj,
|
this.hass.localize,
|
||||||
"action"
|
this.stateObj,
|
||||||
);
|
this.hass.locale,
|
||||||
return `${actionString} (${stateString})`;
|
this.hass.config,
|
||||||
}
|
this.hass.entities,
|
||||||
|
"action"
|
||||||
return stateString;
|
)} (${stateString})`
|
||||||
|
: stateString;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@@ -7,7 +7,6 @@ import { formatLanguageCode } from "../common/language/format_language";
|
|||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import { FrontendLocaleData } from "../data/translation";
|
import { FrontendLocaleData } from "../data/translation";
|
||||||
import "../resources/intl-polyfill";
|
import "../resources/intl-polyfill";
|
||||||
import { translationMetadata } from "../resources/translations-metadata";
|
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
@@ -21,7 +20,7 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
@property() public languages?: string[];
|
@property() public languages?: string[];
|
||||||
|
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||||
|
|
||||||
@@ -42,18 +41,7 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
protected updated(changedProperties: PropertyValues) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has("languages") || changedProperties.has("value")) {
|
||||||
const localeChanged =
|
|
||||||
changedProperties.has("hass") &&
|
|
||||||
this.hass &&
|
|
||||||
changedProperties.get("hass") &&
|
|
||||||
changedProperties.get("hass").locale.language !==
|
|
||||||
this.hass.locale.language;
|
|
||||||
if (
|
|
||||||
changedProperties.has("languages") ||
|
|
||||||
changedProperties.has("value") ||
|
|
||||||
localeChanged
|
|
||||||
) {
|
|
||||||
this._select.layoutOptions();
|
this._select.layoutOptions();
|
||||||
if (this._select.value !== this.value) {
|
if (this._select.value !== this.value) {
|
||||||
fireEvent(this, "value-changed", { value: this._select.value });
|
fireEvent(this, "value-changed", { value: this._select.value });
|
||||||
@@ -63,27 +51,24 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
const languageOptions = this._getLanguagesOptions(
|
const languageOptions = this._getLanguagesOptions(
|
||||||
this.languages ?? this._defaultLanguages,
|
this.languages ?? this._defaultLanguages,
|
||||||
this.nativeName,
|
this.hass.locale,
|
||||||
this.hass?.locale
|
this.nativeName
|
||||||
);
|
);
|
||||||
const selectedItemIndex = languageOptions.findIndex(
|
const selectedItem = languageOptions.find(
|
||||||
(option) => option.value === this.value
|
(option) => option.value === this.value
|
||||||
);
|
);
|
||||||
if (selectedItemIndex === -1) {
|
if (!selectedItem) {
|
||||||
this.value = undefined;
|
this.value = undefined;
|
||||||
}
|
}
|
||||||
if (localeChanged) {
|
|
||||||
this._select.select(selectedItemIndex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getLanguagesOptions = memoizeOne(
|
private _getLanguagesOptions = memoizeOne(
|
||||||
(languages: string[], nativeName: boolean, locale?: FrontendLocaleData) => {
|
(languages: string[], locale: FrontendLocaleData, nativeName: boolean) => {
|
||||||
let options: { label: string; value: string }[] = [];
|
let options: { label: string; value: string }[] = [];
|
||||||
|
|
||||||
if (nativeName) {
|
if (nativeName) {
|
||||||
const translations = translationMetadata.translations;
|
const translations = this.hass.translationMetadata.translations;
|
||||||
options = languages.map((lang) => {
|
options = languages.map((lang) => {
|
||||||
let label = translations[lang]?.nativeName;
|
let label = translations[lang]?.nativeName;
|
||||||
if (!label) {
|
if (!label) {
|
||||||
@@ -102,14 +87,14 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
label,
|
label,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else if (locale) {
|
} else {
|
||||||
options = languages.map((lang) => ({
|
options = languages.map((lang) => ({
|
||||||
value: lang,
|
value: lang,
|
||||||
label: formatLanguageCode(lang, locale),
|
label: formatLanguageCode(lang, locale),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.noSort && locale) {
|
if (!this.noSort) {
|
||||||
options.sort((a, b) =>
|
options.sort((a, b) =>
|
||||||
caseInsensitiveStringCompare(a.label, b.label, locale.language)
|
caseInsensitiveStringCompare(a.label, b.label, locale.language)
|
||||||
);
|
);
|
||||||
@@ -119,14 +104,20 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private _computeDefaultLanguageOptions() {
|
private _computeDefaultLanguageOptions() {
|
||||||
this._defaultLanguages = Object.keys(translationMetadata.translations);
|
if (!this.hass.translationMetadata?.translations) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._defaultLanguages = Object.keys(
|
||||||
|
this.hass.translationMetadata.translations
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const languageOptions = this._getLanguagesOptions(
|
const languageOptions = this._getLanguagesOptions(
|
||||||
this.languages ?? this._defaultLanguages,
|
this.languages ?? this._defaultLanguages,
|
||||||
this.nativeName,
|
this.hass.locale,
|
||||||
this.hass?.locale
|
this.nativeName
|
||||||
);
|
);
|
||||||
|
|
||||||
const value =
|
const value =
|
||||||
@@ -134,10 +125,9 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-select
|
||||||
.label=${this.label ??
|
.label=${this.label ||
|
||||||
(this.hass?.localize("ui.components.language-picker.language") ||
|
this.hass.localize("ui.components.language-picker.language")}
|
||||||
"Language")}
|
.value=${value}
|
||||||
.value=${value || ""}
|
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@selected=${this._changed}
|
@selected=${this._changed}
|
||||||
@@ -147,9 +137,9 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
>
|
>
|
||||||
${languageOptions.length === 0
|
${languageOptions.length === 0
|
||||||
? html`<ha-list-item value=""
|
? html`<ha-list-item value=""
|
||||||
>${this.hass?.localize(
|
>${this.hass.localize(
|
||||||
"ui.components.language-picker.no_languages"
|
"ui.components.language-picker.no_languages"
|
||||||
) || "No languages"}</ha-list-item
|
)}</ha-list-item
|
||||||
>`
|
>`
|
||||||
: languageOptions.map(
|
: languageOptions.map(
|
||||||
(option) => html`
|
(option) => html`
|
||||||
@@ -172,7 +162,7 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
private _changed(ev): void {
|
private _changed(ev): void {
|
||||||
const target = ev.target as HaSelect;
|
const target = ev.target as HaSelect;
|
||||||
if (target.value === "" || target.value === this.value) {
|
if (!this.hass || target.value === "" || target.value === this.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.value = target.value;
|
this.value = target.value;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { mdiImagePlus } from "@mdi/js";
|
import { mdiImagePlus } from "@mdi/js";
|
||||||
import { LitElement, TemplateResult, css, html } from "lit";
|
import { html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
|
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
|
||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
showImageCropperDialog,
|
showImageCropperDialog,
|
||||||
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-button";
|
|
||||||
import "./ha-circular-progress";
|
import "./ha-circular-progress";
|
||||||
import "./ha-file-upload";
|
import "./ha-file-upload";
|
||||||
|
|
||||||
@@ -21,12 +20,6 @@ export class HaPictureUpload extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public secondary?: string;
|
|
||||||
|
|
||||||
@property() public supports?: string;
|
|
||||||
|
|
||||||
@property() public currentImageAltText?: string;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public crop = false;
|
@property({ type: Boolean }) public crop = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public cropOptions?: CropOptions;
|
@property({ attribute: false }) public cropOptions?: CropOptions;
|
||||||
@@ -36,44 +29,19 @@ export class HaPictureUpload extends LitElement {
|
|||||||
@state() private _uploading = false;
|
@state() private _uploading = false;
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
if (!this.value) {
|
return html`
|
||||||
return html`
|
<ha-file-upload
|
||||||
<ha-file-upload
|
.hass=${this.hass}
|
||||||
.hass=${this.hass}
|
.icon=${mdiImagePlus}
|
||||||
.icon=${mdiImagePlus}
|
.label=${this.label ||
|
||||||
.label=${this.label ||
|
this.hass.localize("ui.components.picture-upload.label")}
|
||||||
this.hass.localize("ui.components.picture-upload.label")}
|
.uploading=${this._uploading}
|
||||||
.secondary=${this.secondary}
|
.value=${this.value ? html`<img .src=${this.value} />` : ""}
|
||||||
.supports=${this.supports ||
|
@file-picked=${this._handleFilePicked}
|
||||||
this.hass.localize("ui.components.picture-upload.supported_formats")}
|
@change=${this._handleFileCleared}
|
||||||
.uploading=${this._uploading}
|
accept="image/png, image/jpeg, image/gif"
|
||||||
@file-picked=${this._handleFilePicked}
|
></ha-file-upload>
|
||||||
@change=${this._handleFileCleared}
|
`;
|
||||||
accept="image/png, image/jpeg, image/gif"
|
|
||||||
></ha-file-upload>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return html`<div class="center-vertical">
|
|
||||||
<div class="value">
|
|
||||||
<img
|
|
||||||
.src=${this.value}
|
|
||||||
alt=${this.currentImageAltText ||
|
|
||||||
this.hass.localize("ui.components.picture-upload.current_image_alt")}
|
|
||||||
/>
|
|
||||||
<ha-button
|
|
||||||
@click=${this._handleChangeClick}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.picture-upload.change_picture"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
</ha-button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleChangeClick() {
|
|
||||||
this.value = null;
|
|
||||||
fireEvent(this, "change");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handleFilePicked(ev) {
|
private async _handleFilePicked(ev) {
|
||||||
@@ -132,35 +100,6 @@ export class HaPictureUpload extends LitElement {
|
|||||||
this._uploading = false;
|
this._uploading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
height: 240px;
|
|
||||||
}
|
|
||||||
ha-file-upload {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.center-vertical {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.value {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 200px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-radius: var(--file-upload-image-border-radius);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -47,9 +47,6 @@ export class HaSelect extends SelectBase {
|
|||||||
.mdc-select__anchor {
|
.mdc-select__anchor {
|
||||||
width: var(--ha-select-min-width, 200px);
|
width: var(--ha-select-min-width, 200px);
|
||||||
}
|
}
|
||||||
.mdc-select--filled .mdc-select__anchor {
|
|
||||||
height: var(--ha-select-height, 56px);
|
|
||||||
}
|
|
||||||
.mdc-select--filled .mdc-floating-label {
|
.mdc-select--filled .mdc-floating-label {
|
||||||
inset-inline-start: 12px;
|
inset-inline-start: 12px;
|
||||||
inset-inline-end: initial;
|
inset-inline-end: initial;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { Action } from "../../data/script";
|
import { Action } from "../../data/script";
|
||||||
import { ActionSelector } from "../../data/selector";
|
import { ActionSelector } from "../../data/selector";
|
||||||
@@ -19,13 +19,10 @@ export class HaActionSelector extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
|
||||||
<ha-automation-action
|
<ha-automation-action
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.actions=${this.value || []}
|
.actions=${this.value || []}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.nested=${this.selector.action?.nested}
|
|
||||||
.reOrderMode=${this.selector.action?.reorder_mode}
|
|
||||||
></ha-automation-action>
|
></ha-automation-action>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -40,11 +37,6 @@ export class HaActionSelector extends LitElement {
|
|||||||
opacity: var(--light-disabled-opacity);
|
opacity: var(--light-disabled-opacity);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { Condition } from "../../data/automation";
|
import { Condition } from "../../data/automation";
|
||||||
import { ConditionSelector } from "../../data/selector";
|
import { ConditionSelector } from "../../data/selector";
|
||||||
@@ -19,13 +19,10 @@ export class HaConditionSelector extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
|
||||||
<ha-automation-condition
|
<ha-automation-condition
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.conditions=${this.value || []}
|
.conditions=${this.value || []}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.nested=${this.selector.condition?.nested}
|
|
||||||
.reOrderMode=${this.selector.condition?.reorder_mode}
|
|
||||||
></ha-automation-condition>
|
></ha-automation-condition>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -40,11 +37,6 @@ export class HaConditionSelector extends LitElement {
|
|||||||
opacity: var(--light-disabled-opacity);
|
opacity: var(--light-disabled-opacity);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -37,12 +37,9 @@ export class HaFileSelector extends LitElement {
|
|||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.supports=${this.helper}
|
.helper=${this.helper}
|
||||||
.uploading=${this._busy}
|
.uploading=${this._busy}
|
||||||
.value=${this.value
|
.value=${this.value ? this._filename?.name || "Unknown file" : ""}
|
||||||
? this._filename?.name ||
|
|
||||||
this.hass.localize("ui.components.selectors.file.unknown_file")
|
|
||||||
: undefined}
|
|
||||||
@file-picked=${this._uploadFile}
|
@file-picked=${this._uploadFile}
|
||||||
@change=${this._removeFile}
|
@change=${this._removeFile}
|
||||||
></ha-file-upload>
|
></ha-file-upload>
|
||||||
|
440
src/components/ha-temp-color-picker.ts
Normal file
440
src/components/ha-temp-color-picker.ts
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
|
||||||
|
import { LitElement, PropertyValues, css, html, svg } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { rgb2hex } from "../common/color/convert-color";
|
||||||
|
import {
|
||||||
|
DEFAULT_MAX_KELVIN,
|
||||||
|
DEFAULT_MIN_KELVIN,
|
||||||
|
temperature2rgb,
|
||||||
|
} from "../common/color/convert-light-color";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
|
||||||
|
const SAFE_ZONE_FACTOR = 0.9;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"cursor-moved": { value?: any };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const A11Y_KEY_CODES = new Set([
|
||||||
|
"ArrowRight",
|
||||||
|
"ArrowUp",
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowDown",
|
||||||
|
"PageUp",
|
||||||
|
"PageDown",
|
||||||
|
"Home",
|
||||||
|
"End",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function xy2polar(x: number, y: number) {
|
||||||
|
const r = Math.sqrt(x * x + y * y);
|
||||||
|
const phi = Math.atan2(y, x);
|
||||||
|
return [r, phi];
|
||||||
|
}
|
||||||
|
|
||||||
|
function polar2xy(r: number, phi: number) {
|
||||||
|
const x = Math.cos(phi) * r;
|
||||||
|
const y = Math.sin(phi) * r;
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawColorWheel(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
minTemp: number,
|
||||||
|
maxTemp: number
|
||||||
|
) {
|
||||||
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
|
const radius = ctx.canvas.width / 2;
|
||||||
|
|
||||||
|
const min = Math.max(minTemp, 2000);
|
||||||
|
const max = Math.min(maxTemp, 40000);
|
||||||
|
|
||||||
|
for (let y = -radius; y < radius; y += 1) {
|
||||||
|
const x = radius * Math.sqrt(1 - (y / radius) ** 2);
|
||||||
|
|
||||||
|
const fraction = (y / (radius * SAFE_ZONE_FACTOR) + 1) / 2;
|
||||||
|
|
||||||
|
const temperature = Math.max(
|
||||||
|
Math.min(min + fraction * (max - min), max),
|
||||||
|
min
|
||||||
|
);
|
||||||
|
|
||||||
|
const color = rgb2hex(temperature2rgb(temperature));
|
||||||
|
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(radius - x, radius + y - 0.5, 2 * x, 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-temp-color-picker")
|
||||||
|
class HaTempColorPicker extends LitElement {
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Number, attribute: false })
|
||||||
|
public renderSize?: number;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public value?: number;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public min = DEFAULT_MIN_KELVIN;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public max = DEFAULT_MAX_KELVIN;
|
||||||
|
|
||||||
|
@query("#canvas") private _canvas!: HTMLCanvasElement;
|
||||||
|
|
||||||
|
private _mc?: HammerManager;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _pressed?: string;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _cursorPosition?: [number, number];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _localValue?: number;
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this._setupListeners();
|
||||||
|
this._generateColorWheel();
|
||||||
|
this.setAttribute("role", "slider");
|
||||||
|
this.setAttribute("aria-orientation", "vertical");
|
||||||
|
if (!this.hasAttribute("tabindex")) {
|
||||||
|
this.setAttribute("tabindex", "0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateColorWheel() {
|
||||||
|
const ctx = this._canvas.getContext("2d")!;
|
||||||
|
drawColorWheel(ctx, this.min, this.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._destroyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has("_localValue")) {
|
||||||
|
this.setAttribute("aria-valuenow", this._localValue?.toString() ?? "");
|
||||||
|
}
|
||||||
|
if (changedProps.has("min") || changedProps.has("max")) {
|
||||||
|
this._generateColorWheel();
|
||||||
|
this._resetPosition();
|
||||||
|
}
|
||||||
|
if (changedProps.has("min")) {
|
||||||
|
this.setAttribute("aria-valuemin", this.min.toString());
|
||||||
|
}
|
||||||
|
if (changedProps.has("max")) {
|
||||||
|
this.setAttribute("aria-valuemax", this.max.toString());
|
||||||
|
}
|
||||||
|
if (changedProps.has("value")) {
|
||||||
|
if (this._localValue !== this.value) {
|
||||||
|
this._resetPosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setupListeners() {
|
||||||
|
if (this._canvas && !this._mc) {
|
||||||
|
this._mc = new Manager(this._canvas);
|
||||||
|
this._mc.add(
|
||||||
|
new Pan({
|
||||||
|
direction: DIRECTION_ALL,
|
||||||
|
enable: true,
|
||||||
|
threshold: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._mc.add(new Tap({ event: "singletap" }));
|
||||||
|
|
||||||
|
let savedPosition;
|
||||||
|
this._mc.on("panstart", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._pressed = e.pointerType;
|
||||||
|
savedPosition = this._cursorPosition;
|
||||||
|
});
|
||||||
|
this._mc.on("pancancel", () => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._pressed = undefined;
|
||||||
|
this._cursorPosition = savedPosition;
|
||||||
|
});
|
||||||
|
this._mc.on("panmove", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._cursorPosition = this._getPositionFromEvent(e);
|
||||||
|
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||||
|
fireEvent(this, "cursor-moved", { value: this._localValue });
|
||||||
|
});
|
||||||
|
this._mc.on("panend", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._pressed = undefined;
|
||||||
|
this._cursorPosition = this._getPositionFromEvent(e);
|
||||||
|
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||||
|
fireEvent(this, "cursor-moved", { value: undefined });
|
||||||
|
fireEvent(this, "value-changed", { value: this._localValue });
|
||||||
|
});
|
||||||
|
|
||||||
|
this._mc.on("singletap", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._cursorPosition = this._getPositionFromEvent(e);
|
||||||
|
this._localValue = this._getValueFromCoord(...this._cursorPosition);
|
||||||
|
fireEvent(this, "value-changed", { value: this._localValue });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addEventListener("keydown", this._handleKeyDown);
|
||||||
|
this.addEventListener("keyup", this._handleKeyUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resetPosition() {
|
||||||
|
if (this.value === undefined) {
|
||||||
|
this._cursorPosition = undefined;
|
||||||
|
this._localValue = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [, y] = this._getCoordsFromValue(this.value);
|
||||||
|
const currentX = this._cursorPosition?.[0] ?? 0;
|
||||||
|
const x =
|
||||||
|
Math.sign(currentX) * Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX));
|
||||||
|
this._cursorPosition = [x, y];
|
||||||
|
this._localValue = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCoordsFromValue = (temperature: number): [number, number] => {
|
||||||
|
if (this.value === this.min) {
|
||||||
|
return [0, -1];
|
||||||
|
}
|
||||||
|
if (this.value === this.max) {
|
||||||
|
return [0, 1];
|
||||||
|
}
|
||||||
|
const fraction = (temperature - this.min) / (this.max - this.min);
|
||||||
|
const y = (2 * fraction - 1) * SAFE_ZONE_FACTOR;
|
||||||
|
return [0, y];
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getValueFromCoord = (_x: number, y: number): number => {
|
||||||
|
const fraction = (y / SAFE_ZONE_FACTOR + 1) / 2;
|
||||||
|
const temperature = Math.max(
|
||||||
|
Math.min(this.min + fraction * (this.max - this.min), this.max),
|
||||||
|
this.min
|
||||||
|
);
|
||||||
|
return Math.round(temperature);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getPositionFromEvent = (e: HammerInput): [number, number] => {
|
||||||
|
const x = e.center.x;
|
||||||
|
const y = e.center.y;
|
||||||
|
const boundingRect = e.target.getBoundingClientRect();
|
||||||
|
const offsetX = boundingRect.left;
|
||||||
|
const offsetY = boundingRect.top;
|
||||||
|
const maxX = e.target.clientWidth;
|
||||||
|
const maxY = e.target.clientHeight;
|
||||||
|
|
||||||
|
const _x = (2 * (x - offsetX)) / maxX - 1;
|
||||||
|
const _y = (2 * (y - offsetY)) / maxY - 1;
|
||||||
|
|
||||||
|
const [r, phi] = xy2polar(_x, _y);
|
||||||
|
const [__x, __y] = polar2xy(Math.min(1, r), phi);
|
||||||
|
return [__x, __y];
|
||||||
|
};
|
||||||
|
|
||||||
|
private _destroyListeners() {
|
||||||
|
if (this._mc) {
|
||||||
|
this._mc.destroy();
|
||||||
|
this._mc = undefined;
|
||||||
|
}
|
||||||
|
this.removeEventListener("keydown", this._handleKeyDown);
|
||||||
|
this.removeEventListener("keyup", this._handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const step = 1;
|
||||||
|
const tenPercentStep = Math.max(step, (this.max - this.min) / 10);
|
||||||
|
const currentValue =
|
||||||
|
this._localValue ?? Math.round((this.max + this.min) / 2);
|
||||||
|
switch (e.code) {
|
||||||
|
case "ArrowRight":
|
||||||
|
case "ArrowUp":
|
||||||
|
this._localValue = Math.round(Math.min(currentValue + step, this.max));
|
||||||
|
break;
|
||||||
|
case "ArrowLeft":
|
||||||
|
case "ArrowDown":
|
||||||
|
this._localValue = Math.round(Math.max(currentValue - step, this.min));
|
||||||
|
break;
|
||||||
|
case "PageUp":
|
||||||
|
this._localValue = Math.round(
|
||||||
|
Math.min(currentValue + tenPercentStep, this.max)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "PageDown":
|
||||||
|
this._localValue = Math.round(
|
||||||
|
Math.max(currentValue - tenPercentStep, this.min)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
this._localValue = this.min;
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
this._localValue = this.max;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this._localValue != null) {
|
||||||
|
const [_, y] = this._getCoordsFromValue(this._localValue);
|
||||||
|
const currentX = this._cursorPosition?.[0] ?? 0;
|
||||||
|
const x =
|
||||||
|
Math.sign(currentX) *
|
||||||
|
Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX));
|
||||||
|
this._cursorPosition = [x, y];
|
||||||
|
fireEvent(this, "cursor-moved", { value: this._localValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleKeyUp(e: KeyboardEvent) {
|
||||||
|
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
this.value = this._localValue;
|
||||||
|
fireEvent(this, "value-changed", { value: this._localValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const size = this.renderSize || 400;
|
||||||
|
const canvasSize = size * window.devicePixelRatio;
|
||||||
|
|
||||||
|
const rgb = temperature2rgb(
|
||||||
|
this._localValue ?? Math.round((this.max + this.min) / 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [x, y] = this._cursorPosition ?? [0, 0];
|
||||||
|
|
||||||
|
const cx = ((x + 1) * size) / 2;
|
||||||
|
const cy = ((y + 1) * size) / 2;
|
||||||
|
|
||||||
|
const markerPosition = `${cx}px, ${cy}px`;
|
||||||
|
const markerScale = this._pressed
|
||||||
|
? this._pressed === "touch"
|
||||||
|
? "2.5"
|
||||||
|
: "1.5"
|
||||||
|
: "1";
|
||||||
|
const markerOffset =
|
||||||
|
this._pressed === "touch" ? `0px, -${size / 16}px` : "0px, 0px";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container ${classMap({ pressed: Boolean(this._pressed) })}">
|
||||||
|
<canvas id="canvas" .width=${canvasSize} .height=${canvasSize}></canvas>
|
||||||
|
<svg
|
||||||
|
id="interaction"
|
||||||
|
viewBox="0 0 ${size} ${size}"
|
||||||
|
overflow="visible"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<defs>${this.renderSVGFilter()}</defs>
|
||||||
|
<g
|
||||||
|
style=${styleMap({
|
||||||
|
fill: rgb2hex(rgb),
|
||||||
|
transform: `translate(${markerPosition})`,
|
||||||
|
})}
|
||||||
|
class="cursor"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="16"
|
||||||
|
style=${styleMap({
|
||||||
|
fill: rgb2hex(rgb),
|
||||||
|
transform: `translate(${markerOffset}) scale(${markerScale})`,
|
||||||
|
visibility: this._cursorPosition ? undefined : "hidden",
|
||||||
|
})}
|
||||||
|
></circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSVGFilter() {
|
||||||
|
return svg`
|
||||||
|
<filter
|
||||||
|
id="marker-shadow"
|
||||||
|
x="-50%"
|
||||||
|
y="-50%"
|
||||||
|
width="200%"
|
||||||
|
height="200%"
|
||||||
|
filterUnits="objectBoundingBox"
|
||||||
|
>
|
||||||
|
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.3" flood-color="rgba(0, 0, 0, 1)"/>
|
||||||
|
<feDropShadow dx="0" dy="1" stdDeviation="3" flood-opacity="0.15" flood-color="rgba(0, 0, 0, 1)"/>
|
||||||
|
</filter>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: box-shadow 180ms ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
:host(:focus-visible) canvas {
|
||||||
|
box-shadow: 0 0 0 2px rgb(255, 160, 0);
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
circle {
|
||||||
|
fill: black;
|
||||||
|
stroke: white;
|
||||||
|
stroke-width: 2;
|
||||||
|
filter: url(#marker-shadow);
|
||||||
|
}
|
||||||
|
.container:not(.pressed) circle {
|
||||||
|
transition:
|
||||||
|
transform 100ms ease-in-out,
|
||||||
|
fill 100ms ease-in-out;
|
||||||
|
}
|
||||||
|
.container:not(.pressed) .cursor {
|
||||||
|
transition: transform 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-temp-color-picker": HaTempColorPicker;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
/* eslint-plugin-disable lit */
|
/* eslint-plugin-disable lit */
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { formatNumber } from "../common/number/format_number";
|
import { formatNumber } from "../common/number/format_number";
|
||||||
import LocalizeMixin from "../mixins/localize-mixin";
|
import LocalizeMixin from "../mixins/localize-mixin";
|
||||||
|
|
||||||
@@ -83,7 +84,12 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_localizeState(stateObj) {
|
_localizeState(stateObj) {
|
||||||
return this.hass.formatEntityState(stateObj);
|
return computeStateDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.entities
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define("ha-water_heater-state", HaWaterHeaterState);
|
customElements.define("ha-water_heater-state", HaWaterHeaterState);
|
||||||
|
15
src/components/language-datalist.ts
Normal file
15
src/components/language-datalist.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export const createLanguageListEl = (hass: HomeAssistant) => {
|
||||||
|
const list = document.createElement("datalist");
|
||||||
|
list.id = "languages";
|
||||||
|
for (const [language, metadata] of Object.entries(
|
||||||
|
hass.translationMetadata.translations
|
||||||
|
)) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = language;
|
||||||
|
option.innerText = metadata.nativeName;
|
||||||
|
list.appendChild(option);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
};
|
@@ -61,18 +61,7 @@ export const createAuthForUser = async (
|
|||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const changePassword = (
|
export const adminChangePassword = async (
|
||||||
hass: HomeAssistant,
|
|
||||||
current_password: string,
|
|
||||||
new_password: string
|
|
||||||
) =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "config/auth_provider/homeassistant/change_password",
|
|
||||||
current_password,
|
|
||||||
new_password,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const adminChangePassword = (
|
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
userId: string,
|
userId: string,
|
||||||
password: string
|
password: string
|
||||||
@@ -82,8 +71,3 @@ export const adminChangePassword = (
|
|||||||
user_id: userId,
|
user_id: userId,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteAllRefreshTokens = (hass: HomeAssistant) =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "auth/delete_all_refresh_tokens",
|
|
||||||
});
|
|
||||||
|
@@ -238,13 +238,11 @@ export interface ZoneCondition extends BaseCondition {
|
|||||||
zone: string;
|
zone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
|
|
||||||
|
|
||||||
export interface TimeCondition extends BaseCondition {
|
export interface TimeCondition extends BaseCondition {
|
||||||
condition: "time";
|
condition: "time";
|
||||||
after?: string;
|
after?: string;
|
||||||
before?: string;
|
before?: string;
|
||||||
weekday?: Weekday | Weekday[];
|
weekday?: string | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TemplateCondition extends BaseCondition {
|
export interface TemplateCondition extends BaseCondition {
|
||||||
|
@@ -6,7 +6,11 @@ import {
|
|||||||
formatTimeWithSeconds,
|
formatTimeWithSeconds,
|
||||||
} from "../common/datetime/format_time";
|
} from "../common/datetime/format_time";
|
||||||
import secondsToDuration from "../common/datetime/seconds_to_duration";
|
import secondsToDuration from "../common/datetime/seconds_to_duration";
|
||||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
import {
|
||||||
|
computeAttributeNameDisplay,
|
||||||
|
computeAttributeValueDisplay,
|
||||||
|
} from "../common/entity/compute_attribute_display";
|
||||||
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
import "../resources/intl-polyfill";
|
import "../resources/intl-polyfill";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@@ -231,14 +235,23 @@ const tryDescribeTrigger = (
|
|||||||
for (const state of trigger.from.values()) {
|
for (const state of trigger.from.values()) {
|
||||||
from.push(
|
from.push(
|
||||||
trigger.attribute
|
trigger.attribute
|
||||||
? hass
|
? computeAttributeValueDisplay(
|
||||||
.formatEntityAttributeValue(
|
hass.localize,
|
||||||
stateObj,
|
stateObj,
|
||||||
trigger.attribute,
|
hass.locale,
|
||||||
state
|
hass.config,
|
||||||
)
|
hass.entities,
|
||||||
.toString()
|
trigger.attribute,
|
||||||
: hass.formatEntityState(stateObj, state)
|
state
|
||||||
|
).toString()
|
||||||
|
: computeStateDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
|
hass.entities,
|
||||||
|
state
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (from.length !== 0) {
|
if (from.length !== 0) {
|
||||||
@@ -248,16 +261,23 @@ const tryDescribeTrigger = (
|
|||||||
} else {
|
} else {
|
||||||
base += ` from ${
|
base += ` from ${
|
||||||
trigger.attribute
|
trigger.attribute
|
||||||
? hass
|
? computeAttributeValueDisplay(
|
||||||
.formatEntityAttributeValue(
|
hass.localize,
|
||||||
stateObj,
|
stateObj,
|
||||||
trigger.attribute,
|
hass.locale,
|
||||||
trigger.from
|
hass.config,
|
||||||
)
|
hass.entities,
|
||||||
.toString()
|
trigger.attribute,
|
||||||
: hass
|
trigger.from
|
||||||
.formatEntityState(stateObj, trigger.from.toString())
|
).toString()
|
||||||
.toString()
|
: computeStateDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
|
hass.entities,
|
||||||
|
trigger.from.toString()
|
||||||
|
).toString()
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,14 +292,23 @@ const tryDescribeTrigger = (
|
|||||||
for (const state of trigger.to.values()) {
|
for (const state of trigger.to.values()) {
|
||||||
to.push(
|
to.push(
|
||||||
trigger.attribute
|
trigger.attribute
|
||||||
? hass
|
? computeAttributeValueDisplay(
|
||||||
.formatEntityAttributeValue(
|
hass.localize,
|
||||||
stateObj,
|
stateObj,
|
||||||
trigger.attribute,
|
hass.locale,
|
||||||
state
|
hass.config,
|
||||||
)
|
hass.entities,
|
||||||
.toString()
|
trigger.attribute,
|
||||||
: hass.formatEntityState(stateObj, state).toString()
|
state
|
||||||
|
).toString()
|
||||||
|
: computeStateDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
|
hass.entities,
|
||||||
|
state
|
||||||
|
).toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (to.length !== 0) {
|
if (to.length !== 0) {
|
||||||
@@ -289,14 +318,23 @@ const tryDescribeTrigger = (
|
|||||||
} else {
|
} else {
|
||||||
base += ` to ${
|
base += ` to ${
|
||||||
trigger.attribute
|
trigger.attribute
|
||||||
? hass
|
? computeAttributeValueDisplay(
|
||||||
.formatEntityAttributeValue(
|
hass.localize,
|
||||||
stateObj,
|
stateObj,
|
||||||
trigger.attribute,
|
hass.locale,
|
||||||
trigger.to
|
hass.config,
|
||||||
)
|
hass.entities,
|
||||||
.toString()
|
trigger.attribute,
|
||||||
: hass.formatEntityState(stateObj, trigger.to.toString())
|
trigger.to
|
||||||
|
).toString()
|
||||||
|
: computeStateDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
|
hass.entities,
|
||||||
|
trigger.to.toString()
|
||||||
|
)
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -784,27 +822,45 @@ const tryDescribeCondition = (
|
|||||||
for (const state of condition.state.values()) {
|
for (const state of condition.state.values()) {
|
||||||
states.push(
|
states.push(
|
||||||
condition.attribute
|
condition.attribute
|
||||||
? hass
|
? computeAttributeValueDisplay(
|
||||||
.formatEntityAttributeValue(
|
hass.localize,
|
||||||
stateObj,
|
stateObj,
|
||||||
condition.attribute,
|
hass.locale,
|
||||||
state
|
hass.config,
|
||||||
)
|
hass.entities,
|
||||||
.toString()
|
condition.attribute,
|
||||||
: hass.formatEntityState(stateObj, state)
|
state
|
||||||
|
).toString()
|
||||||
|
: computeStateDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
|
hass.entities,
|
||||||
|
state
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (condition.state !== "") {
|
} else if (condition.state !== "") {
|
||||||
states.push(
|
states.push(
|
||||||
condition.attribute
|
condition.attribute
|
||||||
? hass
|
? computeAttributeValueDisplay(
|
||||||
.formatEntityAttributeValue(
|
hass.localize,
|
||||||
stateObj,
|
stateObj,
|
||||||
condition.attribute,
|
hass.locale,
|
||||||
condition.state
|
hass.config,
|
||||||
)
|
hass.entities,
|
||||||
.toString()
|
condition.attribute,
|
||||||
: hass.formatEntityState(stateObj, condition.state.toString())
|
condition.state
|
||||||
|
).toString()
|
||||||
|
: computeStateDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
|
hass.entities,
|
||||||
|
condition.state.toString()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { EntityFilter } from "../common/entity/entity_filter";
|
import { EntityFilter } from "../common/entity/entity_filter";
|
||||||
|
import { PlaceholderContainer } from "../panels/config/automation/thingtalk/dialog-thingtalk";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
import { AutomationConfig } from "./automation";
|
||||||
|
|
||||||
interface CloudStatusNotLoggedIn {
|
interface CloudStatusNotLoggedIn {
|
||||||
logged_in: false;
|
logged_in: false;
|
||||||
@@ -11,7 +13,6 @@ export interface CertificateInformation {
|
|||||||
common_name: string;
|
common_name: string;
|
||||||
expire_date: string;
|
expire_date: string;
|
||||||
fingerprint: string;
|
fingerprint: string;
|
||||||
alternative_names: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CloudPreferences {
|
export interface CloudPreferences {
|
||||||
@@ -65,6 +66,11 @@ export interface CloudWebhook {
|
|||||||
managed?: boolean;
|
managed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ThingTalkConversion {
|
||||||
|
config: Partial<AutomationConfig>;
|
||||||
|
placeholders: PlaceholderContainer;
|
||||||
|
}
|
||||||
|
|
||||||
export const cloudLogin = (
|
export const cloudLogin = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
email: string,
|
email: string,
|
||||||
@@ -130,6 +136,9 @@ export const disconnectCloudRemote = (hass: HomeAssistant) =>
|
|||||||
export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) =>
|
export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) =>
|
||||||
hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" });
|
hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" });
|
||||||
|
|
||||||
|
export const convertThingTalk = (hass: HomeAssistant, query: string) =>
|
||||||
|
hass.callWS<ThingTalkConversion>({ type: "cloud/thingtalk/convert", query });
|
||||||
|
|
||||||
export const updateCloudPref = (
|
export const updateCloudPref = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
prefs: {
|
prefs: {
|
||||||
|
@@ -67,7 +67,7 @@ export const DOMAIN_ATTRIBUTES_UNITS: Record<string, Record<string, string>> = {
|
|||||||
sun: {
|
sun: {
|
||||||
elevation: "°",
|
elevation: "°",
|
||||||
},
|
},
|
||||||
vacuum: {
|
vaccum: {
|
||||||
battery_level: "%",
|
battery_level: "%",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -6,7 +6,6 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
|
|||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { LightColor } from "./light";
|
import { LightColor } from "./light";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
|
||||||
|
|
||||||
type entityCategory = "config" | "diagnostic";
|
type entityCategory = "config" | "diagnostic";
|
||||||
|
|
||||||
@@ -130,29 +129,15 @@ export interface EntityRegistryEntryUpdateParams {
|
|||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const batteryPriorities = ["sensor", "binary_sensor"];
|
|
||||||
export const findBatteryEntity = <T extends { entity_id: string }>(
|
export const findBatteryEntity = <T extends { entity_id: string }>(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entities: T[]
|
entities: T[]
|
||||||
): T | undefined => {
|
): T | undefined =>
|
||||||
const batteryEntities = entities
|
entities.find(
|
||||||
.filter(
|
(entity) =>
|
||||||
(entity) =>
|
hass.states[entity.entity_id] &&
|
||||||
hass.states[entity.entity_id] &&
|
hass.states[entity.entity_id].attributes.device_class === "battery"
|
||||||
hass.states[entity.entity_id].attributes.device_class === "battery" &&
|
);
|
||||||
batteryPriorities.includes(computeDomain(entity.entity_id))
|
|
||||||
)
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
batteryPriorities.indexOf(computeDomain(a.entity_id)) -
|
|
||||||
batteryPriorities.indexOf(computeDomain(b.entity_id))
|
|
||||||
);
|
|
||||||
if (batteryEntities.length > 0) {
|
|
||||||
return batteryEntities[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findBatteryChargingEntity = <T extends { entity_id: string }>(
|
export const findBatteryChargingEntity = <T extends { entity_id: string }>(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@@ -1,25 +1,46 @@
|
|||||||
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
|
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
interface EntitySource {
|
interface EntitySourceConfigEntry {
|
||||||
|
source: "config_entry";
|
||||||
domain: string;
|
domain: string;
|
||||||
|
custom_component: boolean;
|
||||||
|
config_entry: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EntitySources = Record<string, EntitySource>;
|
interface EntitySourcePlatformConfig {
|
||||||
|
source: "platform_config";
|
||||||
|
domain: string;
|
||||||
|
custom_component: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const fetchEntitySources = (hass: HomeAssistant): Promise<EntitySources> =>
|
export type EntitySources = Record<
|
||||||
hass.callWS({ type: "entity/source" });
|
string,
|
||||||
|
EntitySourceConfigEntry | EntitySourcePlatformConfig
|
||||||
|
>;
|
||||||
|
|
||||||
|
const fetchEntitySources = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id?: string
|
||||||
|
): Promise<EntitySources> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "entity/source",
|
||||||
|
entity_id,
|
||||||
|
});
|
||||||
|
|
||||||
export const fetchEntitySourcesWithCache = (
|
export const fetchEntitySourcesWithCache = (
|
||||||
hass: HomeAssistant
|
hass: HomeAssistant,
|
||||||
|
entity_id?: string
|
||||||
): Promise<EntitySources> =>
|
): Promise<EntitySources> =>
|
||||||
timeCachePromiseFunc(
|
entity_id
|
||||||
"_entitySources",
|
? fetchEntitySources(hass, entity_id)
|
||||||
// cache for 30 seconds
|
: timeCachePromiseFunc(
|
||||||
30000,
|
"_entitySources",
|
||||||
fetchEntitySources,
|
// cache for 30 seconds
|
||||||
// We base the cache on number of states. If number of states
|
30000,
|
||||||
// changes we force a refresh
|
fetchEntitySources,
|
||||||
(hass2) => Object.keys(hass2.states).length,
|
// We base the cache on number of states. If number of states
|
||||||
hass
|
// changes we force a refresh
|
||||||
);
|
(hass2) => Object.keys(hass2.states).length,
|
||||||
|
hass
|
||||||
|
);
|
||||||
|
@@ -17,11 +17,6 @@ export interface GroupEntity extends HassEntityBase {
|
|||||||
attributes: GroupEntityAttributes;
|
attributes: GroupEntityAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupPreview {
|
|
||||||
state: string;
|
|
||||||
attributes: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const computeGroupDomain = (
|
export const computeGroupDomain = (
|
||||||
stateObj: GroupEntity
|
stateObj: GroupEntity
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
@@ -32,15 +27,35 @@ export const computeGroupDomain = (
|
|||||||
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
|
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const subscribePreviewGroup = (
|
export const subscribePreviewGroupSensor = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
flow_id: string,
|
flow_id: string,
|
||||||
flow_type: "config_flow" | "options_flow",
|
flow_type: "config_flow" | "options_flow",
|
||||||
user_input: Record<string, any>,
|
user_input: Record<string, any>,
|
||||||
callback: (preview: GroupPreview) => void
|
callback: (preview: {
|
||||||
|
state: string;
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
}) => void
|
||||||
): Promise<UnsubscribeFunc> =>
|
): Promise<UnsubscribeFunc> =>
|
||||||
hass.connection.subscribeMessage(callback, {
|
hass.connection.subscribeMessage(callback, {
|
||||||
type: "group/start_preview",
|
type: "group/sensor/start_preview",
|
||||||
|
flow_id,
|
||||||
|
flow_type,
|
||||||
|
user_input,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const subscribePreviewGroupBinarySensor = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
flow_id: string,
|
||||||
|
flow_type: "config_flow" | "options_flow",
|
||||||
|
user_input: Record<string, any>,
|
||||||
|
callback: (preview: {
|
||||||
|
state: string;
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
}) => void
|
||||||
|
): Promise<UnsubscribeFunc> =>
|
||||||
|
hass.connection.subscribeMessage(callback, {
|
||||||
|
type: "group/binary_sensor/start_preview",
|
||||||
flow_id,
|
flow_id,
|
||||||
flow_type,
|
flow_type,
|
||||||
user_input,
|
user_input,
|
||||||
|
@@ -5,12 +5,14 @@ import {
|
|||||||
DOMAINS_WITH_DYNAMIC_PICTURE,
|
DOMAINS_WITH_DYNAMIC_PICTURE,
|
||||||
} from "../common/const";
|
} from "../common/const";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
import { autoCaseNoun } from "../common/translations/auto_case_noun";
|
import { autoCaseNoun } from "../common/translations/auto_case_noun";
|
||||||
import { LocalizeFunc } from "../common/translations/localize";
|
import { LocalizeFunc } from "../common/translations/localize";
|
||||||
import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
|
import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { UNAVAILABLE, UNKNOWN } from "./entity";
|
import { UNAVAILABLE, UNKNOWN } from "./entity";
|
||||||
|
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
||||||
|
|
||||||
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
||||||
export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"];
|
export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"];
|
||||||
@@ -337,9 +339,14 @@ export const localizeStateMessage = (
|
|||||||
|
|
||||||
// TODO: This is not working yet, as we don't get historic attribute values
|
// TODO: This is not working yet, as we don't get historic attribute values
|
||||||
|
|
||||||
const event_type = hass
|
const event_type = computeAttributeValueDisplay(
|
||||||
.formatEntityAttributeValue(stateObj, "event_type")
|
hass!.localize,
|
||||||
?.toString();
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
|
hass.entities,
|
||||||
|
"event_type"
|
||||||
|
)?.toString();
|
||||||
|
|
||||||
if (!event_type) {
|
if (!event_type) {
|
||||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`);
|
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`);
|
||||||
@@ -385,7 +392,16 @@ export const localizeStateMessage = (
|
|||||||
return hass.localize(
|
return hass.localize(
|
||||||
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
|
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
|
||||||
"state",
|
"state",
|
||||||
stateObj ? hass.formatEntityState(stateObj, state) : state
|
stateObj
|
||||||
|
? computeStateDisplay(
|
||||||
|
localize,
|
||||||
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
|
hass.entities,
|
||||||
|
state
|
||||||
|
)
|
||||||
|
: state
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -54,10 +54,8 @@ export type Selector =
|
|||||||
| UiColorSelector;
|
| UiColorSelector;
|
||||||
|
|
||||||
export interface ActionSelector {
|
export interface ActionSelector {
|
||||||
action: {
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
reorder_mode?: boolean;
|
action: {} | null;
|
||||||
nested?: boolean;
|
|
||||||
} | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddonSelector {
|
export interface AddonSelector {
|
||||||
@@ -100,10 +98,8 @@ export interface ColorTempSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ConditionSelector {
|
export interface ConditionSelector {
|
||||||
condition: {
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
reorder_mode?: boolean;
|
condition: {} | null;
|
||||||
nested?: boolean;
|
|
||||||
} | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConversationAgentSelector {
|
export interface ConversationAgentSelector {
|
||||||
|
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
import durationToSeconds from "../common/datetime/duration_to_seconds";
|
import durationToSeconds from "../common/datetime/duration_to_seconds";
|
||||||
import secondsToDuration from "../common/datetime/seconds_to_duration";
|
import secondsToDuration from "../common/datetime/seconds_to_duration";
|
||||||
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
export type TimerEntity = HassEntityBase & {
|
export type TimerEntity = HassEntityBase & {
|
||||||
@@ -89,13 +90,25 @@ export const computeDisplayTimer = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stateObj.state === "idle" || timeRemaining === 0) {
|
if (stateObj.state === "idle" || timeRemaining === 0) {
|
||||||
return hass.formatEntityState(stateObj);
|
return computeStateDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
|
hass.entities
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let display = secondsToDuration(timeRemaining || 0);
|
let display = secondsToDuration(timeRemaining || 0);
|
||||||
|
|
||||||
if (stateObj.state === "paused") {
|
if (stateObj.state === "paused") {
|
||||||
display = `${display} (${hass.formatEntityState(stateObj)})`;
|
display = `${display} (${computeStateDisplay(
|
||||||
|
hass.localize,
|
||||||
|
stateObj,
|
||||||
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
|
hass.entities
|
||||||
|
)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return display;
|
return display;
|
||||||
|
@@ -36,9 +36,7 @@ export const enum WeatherEntityFeature {
|
|||||||
FORECAST_TWICE_DAILY = 4,
|
FORECAST_TWICE_DAILY = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModernForecastType = "hourly" | "daily" | "twice_daily";
|
export type ForecastType = "legacy" | "hourly" | "daily" | "twice_daily";
|
||||||
|
|
||||||
export type ForecastType = ModernForecastType | "legacy";
|
|
||||||
|
|
||||||
interface ForecastAttribute {
|
interface ForecastAttribute {
|
||||||
temperature: number;
|
temperature: number;
|
||||||
@@ -638,7 +636,7 @@ export const getForecast = (
|
|||||||
export const subscribeForecast = (
|
export const subscribeForecast = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entity_id: string,
|
entity_id: string,
|
||||||
forecast_type: ModernForecastType,
|
forecast_type: "daily" | "hourly" | "twice_daily",
|
||||||
callback: (forecastevent: ForecastEvent) => void
|
callback: (forecastevent: ForecastEvent) => void
|
||||||
) =>
|
) =>
|
||||||
hass.connection.subscribeMessage<ForecastEvent>(callback, {
|
hass.connection.subscribeMessage<ForecastEvent>(callback, {
|
||||||
@@ -647,31 +645,15 @@ export const subscribeForecast = (
|
|||||||
entity_id,
|
entity_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getSupportedForecastTypes = (
|
|
||||||
stateObj: HassEntityBase
|
|
||||||
): ModernForecastType[] => {
|
|
||||||
const supported: ModernForecastType[] = [];
|
|
||||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
|
|
||||||
supported.push("daily");
|
|
||||||
}
|
|
||||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
|
|
||||||
supported.push("twice_daily");
|
|
||||||
}
|
|
||||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
|
|
||||||
supported.push("hourly");
|
|
||||||
}
|
|
||||||
return supported;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDefaultForecastType = (stateObj: HassEntityBase) => {
|
export const getDefaultForecastType = (stateObj: HassEntityBase) => {
|
||||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
|
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
|
||||||
return "daily";
|
return "daily";
|
||||||
}
|
}
|
||||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
|
|
||||||
return "twice_daily";
|
|
||||||
}
|
|
||||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
|
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
|
||||||
return "hourly";
|
return "hourly";
|
||||||
}
|
}
|
||||||
|
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
|
||||||
|
return "twice_daily";
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
@@ -1,65 +1,29 @@
|
|||||||
import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { HomeAssistant } from "../types";
|
|
||||||
|
|
||||||
export interface RenderTemplateResult {
|
export interface RenderTemplateResult {
|
||||||
result: string;
|
result: string;
|
||||||
listeners: TemplateListeners;
|
listeners: TemplateListeners;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RenderTemplateError {
|
interface TemplateListeners {
|
||||||
error: string;
|
|
||||||
level: "ERROR" | "WARNING";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TemplateListeners {
|
|
||||||
all: boolean;
|
all: boolean;
|
||||||
domains: string[];
|
domains: string[];
|
||||||
entities: string[];
|
entities: string[];
|
||||||
time: boolean;
|
time: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TemplatePreview = TemplatePreviewState | TemplatePreviewError;
|
|
||||||
|
|
||||||
interface TemplatePreviewState {
|
|
||||||
state: string;
|
|
||||||
attributes: Record<string, any>;
|
|
||||||
listeners: TemplateListeners;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TemplatePreviewError {
|
|
||||||
error: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const subscribeRenderTemplate = (
|
export const subscribeRenderTemplate = (
|
||||||
conn: Connection,
|
conn: Connection,
|
||||||
onChange: (result: RenderTemplateResult | RenderTemplateError) => void,
|
onChange: (result: RenderTemplateResult) => void,
|
||||||
params: {
|
params: {
|
||||||
template: string;
|
template: string;
|
||||||
entity_ids?: string | string[];
|
entity_ids?: string | string[];
|
||||||
variables?: Record<string, unknown>;
|
variables?: Record<string, unknown>;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
strict?: boolean;
|
strict?: boolean;
|
||||||
report_errors?: boolean;
|
|
||||||
}
|
}
|
||||||
): Promise<UnsubscribeFunc> =>
|
): Promise<UnsubscribeFunc> =>
|
||||||
conn.subscribeMessage(
|
conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), {
|
||||||
(msg: RenderTemplateResult | RenderTemplateError) => onChange(msg),
|
type: "render_template",
|
||||||
{
|
...params,
|
||||||
type: "render_template",
|
|
||||||
...params,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const subscribePreviewTemplate = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
flow_id: string,
|
|
||||||
flow_type: "config_flow" | "options_flow",
|
|
||||||
user_input: Record<string, any>,
|
|
||||||
callback: (preview: TemplatePreview) => void
|
|
||||||
): Promise<UnsubscribeFunc> =>
|
|
||||||
hass.connection.subscribeMessage(callback, {
|
|
||||||
type: "template/start_preview",
|
|
||||||
flow_id,
|
|
||||||
flow_type,
|
|
||||||
user_input,
|
|
||||||
});
|
});
|
||||||
|
@@ -404,6 +404,8 @@ export interface RequestedGrant {
|
|||||||
clientSideAuth: boolean;
|
clientSideAuth: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const nodeStatus = ["unknown", "asleep", "awake", "dead", "alive"];
|
||||||
|
|
||||||
export const fetchZwaveNetworkStatus = (
|
export const fetchZwaveNetworkStatus = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device_or_entry_id: {
|
device_or_entry_id: {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||||
import { isUnavailableState } from "../../../data/entity";
|
import { isUnavailableState } from "../../../data/entity";
|
||||||
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
|
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
|
||||||
@@ -20,7 +21,6 @@ class EntityPreviewRow extends LitElement {
|
|||||||
return html`<state-badge
|
return html`<state-badge
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.stateObj=${stateObj}
|
.stateObj=${stateObj}
|
||||||
stateColor
|
|
||||||
></state-badge>
|
></state-badge>
|
||||||
<div class="name" .title=${computeStateName(stateObj)}>
|
<div class="name" .title=${computeStateName(stateObj)}>
|
||||||
${computeStateName(stateObj)}
|
${computeStateName(stateObj)}
|
||||||
@@ -35,7 +35,13 @@ class EntityPreviewRow extends LitElement {
|
|||||||
capitalize
|
capitalize
|
||||||
></hui-timestamp-display>
|
></hui-timestamp-display>
|
||||||
`
|
`
|
||||||
: this.hass.formatEntityState(stateObj)}
|
: computeStateDisplay(
|
||||||
|
this.hass!.localize,
|
||||||
|
stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities
|
||||||
|
)}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,20 +2,19 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
|||||||
import { LitElement, html } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { FlowType } from "../../../data/data_entry_flow";
|
import { FlowType } from "../../../data/data_entry_flow";
|
||||||
import { GroupPreview, subscribePreviewGroup } from "../../../data/group";
|
import { subscribePreviewGroupBinarySensor } from "../../../data/group";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import "./entity-preview-row";
|
import "./entity-preview-row";
|
||||||
import { debounce } from "../../../common/util/debounce";
|
|
||||||
|
|
||||||
@customElement("flow-preview-group")
|
@customElement("flow-preview-group_binary_sensor")
|
||||||
class FlowPreviewGroup extends LitElement {
|
class FlowPreviewGroupBinarySensor extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public flowType!: FlowType;
|
@property() public flowType!: FlowType;
|
||||||
|
|
||||||
public handler!: string;
|
public handler!: string;
|
||||||
|
|
||||||
@property() public stepId!: string;
|
public stepId!: string;
|
||||||
|
|
||||||
@property() public flowId!: string;
|
@property() public flowId!: string;
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ class FlowPreviewGroup extends LitElement {
|
|||||||
|
|
||||||
willUpdate(changedProps) {
|
willUpdate(changedProps) {
|
||||||
if (changedProps.has("stepData")) {
|
if (changedProps.has("stepData")) {
|
||||||
this._debouncedSubscribePreview();
|
this._subscribePreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +45,13 @@ class FlowPreviewGroup extends LitElement {
|
|||||||
></entity-preview-row>`;
|
></entity-preview-row>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setPreview = (preview: GroupPreview) => {
|
private _setPreview = (preview: {
|
||||||
|
state: string;
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
}) => {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
this._preview = {
|
this._preview = {
|
||||||
entity_id: `${this.stepId}.___flow_preview___`,
|
entity_id: "binary_sensor.flow_preview",
|
||||||
last_changed: now,
|
last_changed: now,
|
||||||
last_updated: now,
|
last_updated: now,
|
||||||
context: { id: "", parent_id: null, user_id: null },
|
context: { id: "", parent_id: null, user_id: null },
|
||||||
@@ -57,10 +59,6 @@ class FlowPreviewGroup extends LitElement {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
private _debouncedSubscribePreview = debounce(() => {
|
|
||||||
this._subscribePreview();
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
private async _subscribePreview() {
|
private async _subscribePreview() {
|
||||||
if (this._unsub) {
|
if (this._unsub) {
|
||||||
(await this._unsub)();
|
(await this._unsub)();
|
||||||
@@ -70,7 +68,7 @@ class FlowPreviewGroup extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this._unsub = subscribePreviewGroup(
|
this._unsub = subscribePreviewGroupBinarySensor(
|
||||||
this.hass,
|
this.hass,
|
||||||
this.flowId,
|
this.flowId,
|
||||||
this.flowType,
|
this.flowType,
|
||||||
@@ -85,6 +83,6 @@ class FlowPreviewGroup extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"flow-preview-group": FlowPreviewGroup;
|
"flow-preview-group_binary_sensor": FlowPreviewGroupBinarySensor;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,92 @@
|
|||||||
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { FlowType } from "../../../data/data_entry_flow";
|
||||||
|
import { subscribePreviewGroupSensor } from "../../../data/group";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import "./entity-preview-row";
|
||||||
|
|
||||||
|
@customElement("flow-preview-group_sensor")
|
||||||
|
class FlowPreviewGroupSensor extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public flowType!: FlowType;
|
||||||
|
|
||||||
|
public handler!: string;
|
||||||
|
|
||||||
|
public stepId!: string;
|
||||||
|
|
||||||
|
@property() public flowId!: string;
|
||||||
|
|
||||||
|
@property() public stepData!: Record<string, any>;
|
||||||
|
|
||||||
|
@state() private _preview?: HassEntity;
|
||||||
|
|
||||||
|
private _unsub?: Promise<UnsubscribeFunc>;
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
if (this._unsub) {
|
||||||
|
this._unsub.then((unsub) => unsub());
|
||||||
|
this._unsub = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
willUpdate(changedProps) {
|
||||||
|
if (changedProps.has("stepData")) {
|
||||||
|
this._subscribePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`<entity-preview-row
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${this._preview}
|
||||||
|
></entity-preview-row>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setPreview = (preview: {
|
||||||
|
state: string;
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
}) => {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
this._preview = {
|
||||||
|
entity_id: "sensor.flow_preview",
|
||||||
|
last_changed: now,
|
||||||
|
last_updated: now,
|
||||||
|
context: { id: "", parent_id: null, user_id: null },
|
||||||
|
...preview,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
private async _subscribePreview() {
|
||||||
|
if (this._unsub) {
|
||||||
|
(await this._unsub)();
|
||||||
|
this._unsub = undefined;
|
||||||
|
}
|
||||||
|
if (this.flowType === "repair_flow") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.stepData.type) {
|
||||||
|
this._preview = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this._unsub = subscribePreviewGroupSensor(
|
||||||
|
this.hass,
|
||||||
|
this.flowId,
|
||||||
|
this.flowType,
|
||||||
|
this.stepData,
|
||||||
|
this._setPreview
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this._preview = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"flow-preview-group_sensor": FlowPreviewGroupSensor;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,179 +0,0 @@
|
|||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import { LitElement, html, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { debounce } from "../../../common/util/debounce";
|
|
||||||
import { FlowType } from "../../../data/data_entry_flow";
|
|
||||||
import {
|
|
||||||
TemplateListeners,
|
|
||||||
TemplatePreview,
|
|
||||||
subscribePreviewTemplate,
|
|
||||||
} from "../../../data/ws-templates";
|
|
||||||
import { HomeAssistant } from "../../../types";
|
|
||||||
import "./entity-preview-row";
|
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
|
||||||
|
|
||||||
@customElement("flow-preview-template")
|
|
||||||
class FlowPreviewTemplate extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public flowType!: FlowType;
|
|
||||||
|
|
||||||
public handler!: string;
|
|
||||||
|
|
||||||
@property() public stepId!: string;
|
|
||||||
|
|
||||||
@property() public flowId!: string;
|
|
||||||
|
|
||||||
@property() public stepData!: Record<string, any>;
|
|
||||||
|
|
||||||
@state() private _preview?: HassEntity;
|
|
||||||
|
|
||||||
@state() private _listeners?: TemplateListeners;
|
|
||||||
|
|
||||||
@state() private _error?: string;
|
|
||||||
|
|
||||||
private _unsub?: Promise<UnsubscribeFunc>;
|
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
if (this._unsub) {
|
|
||||||
this._unsub.then((unsub) => unsub());
|
|
||||||
this._unsub = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
willUpdate(changedProps) {
|
|
||||||
if (changedProps.has("stepData")) {
|
|
||||||
this._debouncedSubscribePreview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (this._error) {
|
|
||||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
|
||||||
}
|
|
||||||
return html`<entity-preview-row
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${this._preview}
|
|
||||||
></entity-preview-row>
|
|
||||||
${this._listeners?.time
|
|
||||||
? html`
|
|
||||||
<p>
|
|
||||||
${this.hass.localize("ui.dialogs.helper_settings.template.time")}
|
|
||||||
</p>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${!this._listeners
|
|
||||||
? nothing
|
|
||||||
: this._listeners.all
|
|
||||||
? html`
|
|
||||||
<p class="all_listeners">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.dialogs.helper_settings.template.all_listeners"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
`
|
|
||||||
: this._listeners.domains.length || this._listeners.entities.length
|
|
||||||
? html`
|
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.dialogs.helper_settings.template.listeners"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
${this._listeners.domains
|
|
||||||
.sort()
|
|
||||||
.map(
|
|
||||||
(domain) => html`
|
|
||||||
<li>
|
|
||||||
<b
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.dialogs.helper_settings.template.domain"
|
|
||||||
)}</b
|
|
||||||
>: ${domain}
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${this._listeners.entities
|
|
||||||
.sort()
|
|
||||||
.map(
|
|
||||||
(entity_id) => html`
|
|
||||||
<li>
|
|
||||||
<b
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.dialogs.helper_settings.template.entity"
|
|
||||||
)}</b
|
|
||||||
>: ${entity_id}
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
`
|
|
||||||
: !this._listeners.time
|
|
||||||
? html`<p class="all_listeners">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.dialogs.helper_settings.template.no_listeners"
|
|
||||||
)}
|
|
||||||
</p>`
|
|
||||||
: nothing} `;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setPreview = (preview: TemplatePreview) => {
|
|
||||||
if ("error" in preview) {
|
|
||||||
this._error = preview.error;
|
|
||||||
this._preview = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._error = undefined;
|
|
||||||
this._listeners = preview.listeners;
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
this._preview = {
|
|
||||||
entity_id: `${this.stepId}.___flow_preview___`,
|
|
||||||
last_changed: now,
|
|
||||||
last_updated: now,
|
|
||||||
context: { id: "", parent_id: null, user_id: null },
|
|
||||||
attributes: preview.attributes,
|
|
||||||
state: preview.state,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
private _debouncedSubscribePreview = debounce(() => {
|
|
||||||
this._subscribePreview();
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
private async _subscribePreview() {
|
|
||||||
if (this._unsub) {
|
|
||||||
(await this._unsub)();
|
|
||||||
this._unsub = undefined;
|
|
||||||
}
|
|
||||||
if (this.flowType === "repair_flow") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this._unsub = subscribePreviewTemplate(
|
|
||||||
this.hass,
|
|
||||||
this.flowId,
|
|
||||||
this.flowType,
|
|
||||||
this.stepData,
|
|
||||||
this._setPreview
|
|
||||||
);
|
|
||||||
await this._unsub;
|
|
||||||
fireEvent(this, "set-flow-errors", { errors: {} });
|
|
||||||
} catch (err: any) {
|
|
||||||
if (typeof err.message === "string") {
|
|
||||||
this._error = err.message;
|
|
||||||
} else {
|
|
||||||
this._error = undefined;
|
|
||||||
fireEvent(this, "set-flow-errors", err.message);
|
|
||||||
}
|
|
||||||
this._unsub = undefined;
|
|
||||||
this._preview = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"flow-preview-template": FlowPreviewTemplate;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,6 +1,6 @@
|
|||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import { ConfigEntry } from "../../data/config_entries";
|
import { ConfigEntry } from "../../data/config_entries";
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName, IntegrationManifest } from "../../data/integration";
|
||||||
import {
|
import {
|
||||||
createOptionsFlow,
|
createOptionsFlow,
|
||||||
deleteOptionsFlow,
|
deleteOptionsFlow,
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
handleOptionsFlowStep,
|
handleOptionsFlowStep,
|
||||||
} from "../../data/options_flow";
|
} from "../../data/options_flow";
|
||||||
import {
|
import {
|
||||||
DataEntryFlowDialogParams,
|
|
||||||
loadDataEntryFlowDialog,
|
loadDataEntryFlowDialog,
|
||||||
showFlowDialog,
|
showFlowDialog,
|
||||||
} from "./show-dialog-data-entry-flow";
|
} from "./show-dialog-data-entry-flow";
|
||||||
@@ -18,14 +17,14 @@ export const loadOptionsFlowDialog = loadDataEntryFlowDialog;
|
|||||||
export const showOptionsFlowDialog = (
|
export const showOptionsFlowDialog = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
configEntry: ConfigEntry,
|
configEntry: ConfigEntry,
|
||||||
dialogParams?: Omit<DataEntryFlowDialogParams, "flowConfig">
|
manifest?: IntegrationManifest | null
|
||||||
): void =>
|
): void =>
|
||||||
showFlowDialog(
|
showFlowDialog(
|
||||||
element,
|
element,
|
||||||
{
|
{
|
||||||
startFlowHandler: configEntry.entry_id,
|
startFlowHandler: configEntry.entry_id,
|
||||||
domain: configEntry.domain,
|
domain: configEntry.domain,
|
||||||
...dialogParams,
|
manifest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flowType: "options_flow",
|
flowType: "options_flow",
|
||||||
|
@@ -70,7 +70,7 @@ class StepFlowForm extends LitElement {
|
|||||||
></ha-form>
|
></ha-form>
|
||||||
</div>
|
</div>
|
||||||
${step.preview
|
${step.preview
|
||||||
? html`<div class="preview" @set-flow-errors=${this._setError}>
|
? html`<div class="preview">
|
||||||
<h3>
|
<h3>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.integrations.config_flow.preview"
|
"ui.panel.config.integrations.config_flow.preview"
|
||||||
@@ -107,10 +107,6 @@ class StepFlowForm extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setError(ev: CustomEvent) {
|
|
||||||
this.step = { ...this.step, errors: ev.detail };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0);
|
setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0);
|
||||||
@@ -257,9 +253,6 @@ class StepFlowForm extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
|
||||||
"set-flow-errors": { errors: DataEntryFlowStepForm["errors"] };
|
|
||||||
}
|
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"step-flow-form": StepFlowForm;
|
"step-flow-form": StepFlowForm;
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ import {
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { UNIT_F } from "../../../../common/const";
|
import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
|
||||||
import { stateActive } from "../../../../common/entity/state_active";
|
import { stateActive } from "../../../../common/entity/state_active";
|
||||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||||
@@ -67,7 +67,7 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
|||||||
private get _step() {
|
private get _step() {
|
||||||
return (
|
return (
|
||||||
this.stateObj.attributes.target_temp_step ||
|
this.stateObj.attributes.target_temp_step ||
|
||||||
(this.hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
|
(this.hass.config.unit_system.temperature.indexOf("F") === -1 ? 0.5 : 1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,10 +161,14 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
|||||||
|
|
||||||
const action = this.stateObj.attributes.hvac_action;
|
const action = this.stateObj.attributes.hvac_action;
|
||||||
|
|
||||||
const actionLabel = this.hass.formatEntityAttributeValue(
|
const actionLabel = computeAttributeValueDisplay(
|
||||||
|
this.hass.localize,
|
||||||
this.stateObj,
|
this.stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities,
|
||||||
"hvac_action"
|
"hvac_action"
|
||||||
);
|
) as string;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<p class="label">
|
<p class="label">
|
||||||
@@ -275,21 +279,15 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeModes = this.stateObj.attributes.hvac_modes.filter(
|
||||||
|
(m) => m !== "off"
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
supportsTargetTemperature &&
|
supportsTargetTemperature &&
|
||||||
this._targetTemperature.value != null &&
|
this._targetTemperature.value != null &&
|
||||||
this.stateObj.state !== UNAVAILABLE
|
this.stateObj.state !== UNAVAILABLE
|
||||||
) {
|
) {
|
||||||
const heatCoolModes = this.stateObj.attributes.hvac_modes.filter((m) =>
|
|
||||||
["heat", "cool", "heat_cool"].includes(m)
|
|
||||||
);
|
|
||||||
const sliderMode =
|
|
||||||
SLIDER_MODES[
|
|
||||||
heatCoolModes.length === 1 && ["off", "auto"].includes(mode)
|
|
||||||
? heatCoolModes[0]
|
|
||||||
: mode
|
|
||||||
];
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="container"
|
class="container"
|
||||||
@@ -300,7 +298,9 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
|||||||
>
|
>
|
||||||
<ha-control-circular-slider
|
<ha-control-circular-slider
|
||||||
.inactive=${!active}
|
.inactive=${!active}
|
||||||
.mode=${sliderMode}
|
.mode=${mode === "off" && activeModes.length === 1
|
||||||
|
? SLIDER_MODES[activeModes[0]]
|
||||||
|
: SLIDER_MODES[mode]}
|
||||||
.value=${this._targetTemperature.value}
|
.value=${this._targetTemperature.value}
|
||||||
.min=${this._min}
|
.min=${this._min}
|
||||||
.max=${this._max}
|
.max=${this._max}
|
||||||
|
@@ -35,7 +35,6 @@ export class HaMoreInfoCoverPosition extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const openColor = stateColorCss(this.stateObj, "open");
|
|
||||||
const color = stateColorCss(this.stateObj);
|
const color = stateColorCss(this.stateObj);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -54,8 +53,6 @@ export class HaMoreInfoCoverPosition extends LitElement {
|
|||||||
"current_position"
|
"current_position"
|
||||||
)}
|
)}
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
// Use open color for inactive state to avoid grey slider that looks disabled
|
|
||||||
"--state-cover-inactive-color": openColor,
|
|
||||||
"--control-slider-color": color,
|
"--control-slider-color": color,
|
||||||
"--control-slider-background": color,
|
"--control-slider-background": color,
|
||||||
})}
|
})}
|
||||||
@@ -71,6 +68,8 @@ export class HaMoreInfoCoverPosition extends LitElement {
|
|||||||
height: 45vh;
|
height: 45vh;
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
/* Force inactive state to be colored for the slider */
|
||||||
|
--state-cover-inactive-color: var(--state-cover-active-color);
|
||||||
--control-slider-thickness: 100px;
|
--control-slider-thickness: 100px;
|
||||||
--control-slider-border-radius: 24px;
|
--control-slider-border-radius: 24px;
|
||||||
--control-slider-color: var(--primary-color);
|
--control-slider-color: var(--primary-color);
|
||||||
|
@@ -72,8 +72,8 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const openColor = stateColorCss(this.stateObj, "open");
|
|
||||||
const color = stateColorCss(this.stateObj);
|
const color = stateColorCss(this.stateObj);
|
||||||
|
const isUnavailable = this.stateObj.state === UNAVAILABLE;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-control-slider
|
<ha-control-slider
|
||||||
@@ -90,12 +90,10 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
|
|||||||
"current_tilt_position"
|
"current_tilt_position"
|
||||||
)}
|
)}
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
// Use open color for inactive state to avoid grey slider that looks disabled
|
|
||||||
"--state-cover-inactive-color": openColor,
|
|
||||||
"--control-slider-color": color,
|
"--control-slider-color": color,
|
||||||
"--control-slider-background": color,
|
"--control-slider-background": color,
|
||||||
})}
|
})}
|
||||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
.disabled=${isUnavailable}
|
||||||
>
|
>
|
||||||
<div slot="background" class="gradient"></div>
|
<div slot="background" class="gradient"></div>
|
||||||
</ha-control-slider>
|
</ha-control-slider>
|
||||||
@@ -108,6 +106,8 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
|
|||||||
height: 45vh;
|
height: 45vh;
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
/* Force inactive state to be colored for the slider */
|
||||||
|
--state-cover-inactive-color: var(--state-cover-active-color);
|
||||||
--control-slider-thickness: 100px;
|
--control-slider-thickness: 100px;
|
||||||
--control-slider-border-radius: 24px;
|
--control-slider-border-radius: 24px;
|
||||||
--control-slider-color: var(--primary-color);
|
--control-slider-color: var(--primary-color);
|
||||||
|
@@ -2,6 +2,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
|
import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
|
||||||
|
import { computeStateDisplay } from "../../../../common/entity/compute_state_display";
|
||||||
import { stateActive } from "../../../../common/entity/state_active";
|
import { stateActive } from "../../../../common/entity/state_active";
|
||||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||||
import "../../../../components/ha-control-select";
|
import "../../../../components/ha-control-select";
|
||||||
@@ -11,12 +12,12 @@ import { UNAVAILABLE } from "../../../../data/entity";
|
|||||||
import {
|
import {
|
||||||
computeFanSpeedCount,
|
computeFanSpeedCount,
|
||||||
computeFanSpeedIcon,
|
computeFanSpeedIcon,
|
||||||
FAN_SPEED_COUNT_MAX_FOR_BUTTONS,
|
|
||||||
FAN_SPEEDS,
|
|
||||||
FanEntity,
|
FanEntity,
|
||||||
fanPercentageToSpeed,
|
fanPercentageToSpeed,
|
||||||
FanSpeed,
|
FanSpeed,
|
||||||
fanSpeedToPercentage,
|
fanSpeedToPercentage,
|
||||||
|
FAN_SPEEDS,
|
||||||
|
FAN_SPEED_COUNT_MAX_FOR_BUTTONS,
|
||||||
} from "../../../../data/fan";
|
} from "../../../../data/fan";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
|
||||||
@@ -67,7 +68,14 @@ export class HaMoreInfoFanSpeed extends LitElement {
|
|||||||
|
|
||||||
private _localizeSpeed(speed: FanSpeed) {
|
private _localizeSpeed(speed: FanSpeed) {
|
||||||
if (speed === "on" || speed === "off") {
|
if (speed === "on" || speed === "off") {
|
||||||
return this.hass.formatEntityState(this.stateObj, speed);
|
return computeStateDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities,
|
||||||
|
speed
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
this.hass.localize(`ui.dialogs.more_info_control.fan.speed.${speed}`) ||
|
this.hass.localize(`ui.dialogs.more_info_control.fan.speed.${speed}`) ||
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user