mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-31 12:00:26 +00:00
Compare commits
106 Commits
20241127.2
...
fix_new_sc
Author | SHA1 | Date | |
---|---|---|---|
![]() |
637eb6e894 | ||
![]() |
f688780677 | ||
![]() |
0ce98a86e6 | ||
![]() |
bf624f5ca7 | ||
![]() |
ce5ce37de7 | ||
![]() |
da727d3a3a | ||
![]() |
9d83cc6988 | ||
![]() |
ed625d4e0b | ||
![]() |
84157c8ea5 | ||
![]() |
8f19c0abb0 | ||
![]() |
008647aa7a | ||
![]() |
b86d6021da | ||
![]() |
98af479fd3 | ||
![]() |
d1c981bc19 | ||
![]() |
1b0e53d3d9 | ||
![]() |
0d49927541 | ||
![]() |
9e8d452438 | ||
![]() |
ddd646007e | ||
![]() |
787fba82bd | ||
![]() |
1393a3ade8 | ||
![]() |
bf24d67066 | ||
![]() |
9774deef6d | ||
![]() |
8390c6e29b | ||
![]() |
c78d371a9c | ||
![]() |
7750299a66 | ||
![]() |
43f31dd455 | ||
![]() |
df21900341 | ||
![]() |
6934f0626c | ||
![]() |
ea5bf17780 | ||
![]() |
0b7af715a8 | ||
![]() |
8579bee053 | ||
![]() |
400ddbf625 | ||
![]() |
5fdefc0c20 | ||
![]() |
c72c74828e | ||
![]() |
e0b157d280 | ||
![]() |
e02736b4e2 | ||
![]() |
af049274d9 | ||
![]() |
7a12fd2853 | ||
![]() |
f724d8e7a9 | ||
![]() |
0d7e0df194 | ||
![]() |
7d567bc386 | ||
![]() |
ad52386ae0 | ||
![]() |
512fee47b6 | ||
![]() |
4092f56ea5 | ||
![]() |
926972cce6 | ||
![]() |
d51eea7bb6 | ||
![]() |
a3ca889ca3 | ||
![]() |
a7406b3201 | ||
![]() |
b54057cc4b | ||
![]() |
d77dd5300e | ||
![]() |
9396d5cf8c | ||
![]() |
a78ddb50fd | ||
![]() |
64e8b636b9 | ||
![]() |
2c604ff946 | ||
![]() |
f8ce7c2ce1 | ||
![]() |
af1622e306 | ||
![]() |
3c03dfb322 | ||
![]() |
697a99f913 | ||
![]() |
06925f0716 | ||
![]() |
afcfdb5140 | ||
![]() |
6126280f2d | ||
![]() |
8e6f4886e8 | ||
![]() |
480de9ef03 | ||
![]() |
614c4ec404 | ||
![]() |
d43291ae52 | ||
![]() |
7c486ec969 | ||
![]() |
19f54b6ba2 | ||
![]() |
97cc9f9ab9 | ||
![]() |
a104d38fc9 | ||
![]() |
2582023ab2 | ||
![]() |
e731f060f1 | ||
![]() |
c3942d244d | ||
![]() |
f4ef4c628a | ||
![]() |
a0f3e4f785 | ||
![]() |
2c13c5a18c | ||
![]() |
22cfd40ccc | ||
![]() |
5c7d9b3fa3 | ||
![]() |
55b6aa09d9 | ||
![]() |
96395dd5e1 | ||
![]() |
7191edd3c6 | ||
![]() |
6169379f4c | ||
![]() |
5500dd1332 | ||
![]() |
5c681896f3 | ||
![]() |
d826113319 | ||
![]() |
f72b298f97 | ||
![]() |
90b7cad7ac | ||
![]() |
e0239486bc | ||
![]() |
e10fb3c168 | ||
![]() |
5ee8bee6dc | ||
![]() |
4b678ffb41 | ||
![]() |
82c23026b3 | ||
![]() |
94b7b60fe0 | ||
![]() |
e1553a7ccf | ||
![]() |
fb0ca49742 | ||
![]() |
97015788a2 | ||
![]() |
0cf05ea2cc | ||
![]() |
acbe77c0d6 | ||
![]() |
bede8c66c5 | ||
![]() |
de87aee15b | ||
![]() |
36312cc273 | ||
![]() |
3120184d63 | ||
![]() |
a1be9d923e | ||
![]() |
7dee34ca75 | ||
![]() |
f7c8c6e3e8 | ||
![]() |
3411967fd9 | ||
![]() |
125805d6d1 |
@@ -4,13 +4,12 @@
|
||||
# - released in the last year + current alpha/beta versions
|
||||
# - Firefox extended support release (ESR)
|
||||
# - with global utilization at or above 0.5%
|
||||
# - must support dynamic import of ES modules
|
||||
# - exclude browsers no longer being maintained
|
||||
# - exclude dead browsers (no security maintenance for 2+ years)
|
||||
# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
|
||||
unreleased versions
|
||||
last 1 year
|
||||
Firefox ESR
|
||||
>= 0.5% and supports es6-module-dynamic-import
|
||||
>= 0.5%
|
||||
not dead
|
||||
not KaiOS > 0
|
||||
not QQAndroid > 0
|
||||
@@ -20,23 +19,18 @@ not UCAndroid > 0
|
||||
# Legacy builds are served when modern requirements are not met and support browsers:
|
||||
# - released in the last 7 years + current alpha/beta versionss
|
||||
# - with global utilization at or above 0.05%
|
||||
# The lattermost query ensures that support for popular old browsers is not dropped too early
|
||||
# (e.g. IE 11, Android 4.4, or Samsung 4).
|
||||
#
|
||||
# In addition, legacy browsers must support some minimum features that cannot be polyfilled:
|
||||
# - ES5 (strict mode)
|
||||
# - web sockets to communicate with backend
|
||||
# - inline SVG used widely in buttons, widgets, etc.
|
||||
# - custom events used for most user interactions
|
||||
# - CSS flexbox used in the majority of the layout
|
||||
# Nearly all of these are redundant with the above rules.
|
||||
# As of May 2023, only web sockets must be added to the query.
|
||||
# - exclude dead browsers (no security maintenance for 2+ years)
|
||||
# - exclude Opera Mini which does not support web sockets
|
||||
unreleased versions
|
||||
last 7 years
|
||||
>= 0.05% and supports websockets
|
||||
>= 0.05%
|
||||
not dead
|
||||
not op_mini all
|
||||
|
||||
[legacy-sw]
|
||||
# Same as legacy plus supports service workers
|
||||
unreleased versions
|
||||
last 7 years
|
||||
>= 0.05% and supports websockets and supports serviceworkers
|
||||
>= 0.05% and supports serviceworkers
|
||||
not dead
|
||||
not op_mini all
|
||||
|
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- uses: actions/cache@v4.1.2
|
||||
- uses: actions/cache@v4.2.0
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: "node_modules"
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.1.2
|
||||
uses: actions/cache@v4.2.0
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- uses: actions/cache@v4.1.2
|
||||
- uses: actions/cache@v4.2.0
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: "node_modules"
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- uses: actions/cache@v4.1.2
|
||||
- uses: actions/cache@v4.2.0
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: "node_modules"
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- uses: actions/cache@v4.1.2
|
||||
- uses: actions/cache@v4.2.0
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: "node_modules"
|
||||
|
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@v2.1.13
|
||||
uses: relative-ci/agent-action@v2.1.14
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
59
.github/workflows/release.yaml
vendored
59
.github/workflows/release.yaml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.12"
|
||||
@@ -82,3 +81,61 @@ jobs:
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
requirements: "requirements.txt"
|
||||
|
||||
release-landing-page:
|
||||
name: Release landing-page frontend
|
||||
if: github.event.release.prerelease == false
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Download Translations
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
- name: Build landing-page
|
||||
run: landing-page/script/build_landing_page
|
||||
- name: Tar folder
|
||||
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@v2.1.0
|
||||
with:
|
||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
||||
|
||||
release-supervisor:
|
||||
name: Release supervisor frontend
|
||||
if: github.event.release.prerelease == false
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Download Translations
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
- name: Build supervisor
|
||||
run: hassio/script/build_hassio
|
||||
- name: Tar folder
|
||||
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@v2.1.0
|
||||
with:
|
||||
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
|
||||
|
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.5.2.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.5.3.cjs
|
||||
|
@@ -53,6 +53,11 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
__SUPERVISOR__: false,
|
||||
__BACKWARDS_COMPAT__: false,
|
||||
__STATIC_PATH__: "/static/",
|
||||
__HASS_URL__: `\`${
|
||||
"HASS_URL" in process.env
|
||||
? process.env["HASS_URL"]
|
||||
: "${location.protocol}//${location.host}"
|
||||
}\``,
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
),
|
||||
|
@@ -5,9 +5,6 @@ const paths = require("./paths.cjs");
|
||||
const isTrue = (value) => value === "1" || value?.toLowerCase() === "true";
|
||||
|
||||
module.exports = {
|
||||
useWDS() {
|
||||
return isTrue(process.env.WDS);
|
||||
},
|
||||
isProdBuild() {
|
||||
return (
|
||||
process.env.NODE_ENV === "production" || module.exports.isStatsBuild()
|
||||
|
@@ -8,7 +8,6 @@ import "./gen-icons-json.js";
|
||||
import "./locale-data.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
import "./wds.js";
|
||||
import "./rspack.js";
|
||||
|
||||
gulp.task(
|
||||
@@ -26,7 +25,7 @@ gulp.task(
|
||||
"build-locale-data"
|
||||
),
|
||||
"copy-static-app",
|
||||
env.useWDS() ? "wds-watch-app" : "rspack-watch-app"
|
||||
"rspack-watch-app"
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -3,7 +3,6 @@
|
||||
import { constants } from "node:zlib";
|
||||
import gulp from "gulp";
|
||||
import brotli from "gulp-brotli";
|
||||
import zopfli from "gulp-zopfli-green";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
const filesGlob = "*.{js,json,css,svg,xml}";
|
||||
@@ -13,7 +12,6 @@ const brotliOptions = {
|
||||
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
||||
},
|
||||
};
|
||||
const zopfliOptions = { threshold: 150 };
|
||||
|
||||
const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
|
||||
gulp
|
||||
@@ -29,20 +27,6 @@ const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
|
||||
.pipe(brotli(brotliOptions))
|
||||
.pipe(gulp.dest(rootDir));
|
||||
|
||||
const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
|
||||
gulp
|
||||
.src(
|
||||
[
|
||||
`${rootDir}/**/${filesGlob}`,
|
||||
compressModern ? undefined : `!${modernDir}/**/${filesGlob}`,
|
||||
`!${rootDir}/{sw-modern,service_worker}.js`,
|
||||
`${rootDir}/{authorize,onboarding}.html`,
|
||||
].filter(Boolean),
|
||||
{ base: rootDir }
|
||||
)
|
||||
.pipe(zopfli(zopfliOptions))
|
||||
.pipe(gulp.dest(rootDir));
|
||||
|
||||
const compressAppBrotli = () =>
|
||||
compressDistBrotli(paths.app_output_root, paths.app_output_latest);
|
||||
const compressHassioBrotli = () =>
|
||||
@@ -52,17 +36,5 @@ const compressHassioBrotli = () =>
|
||||
false
|
||||
);
|
||||
|
||||
const compressAppZopfli = () =>
|
||||
compressDistZopfli(paths.app_output_root, paths.app_output_latest);
|
||||
const compressHassioZopfli = () =>
|
||||
compressDistZopfli(
|
||||
paths.hassio_output_root,
|
||||
paths.hassio_output_latest,
|
||||
true
|
||||
);
|
||||
|
||||
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
|
||||
gulp.task(
|
||||
"compress-hassio",
|
||||
gulp.parallel(compressHassioBrotli, compressHassioZopfli)
|
||||
);
|
||||
gulp.task("compress-app", compressAppBrotli);
|
||||
gulp.task("compress-hassio", compressHassioBrotli);
|
||||
|
@@ -11,7 +11,6 @@ import { minify } from "html-minifier-terser";
|
||||
import template from "lodash.template";
|
||||
import { dirname, extname, resolve } from "node:path";
|
||||
import { htmlMinifierOptions, terserOptions } from "../bundle.cjs";
|
||||
import env from "../env.cjs";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
// macOS companion app has no way to obtain the Safari version used by WKWebView,
|
||||
@@ -56,7 +55,6 @@ const getCommonTemplateVars = () => {
|
||||
{ ignorePatch: true, allowHigherVersions: true }
|
||||
);
|
||||
return {
|
||||
useWDS: env.useWDS(),
|
||||
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
|
||||
};
|
||||
};
|
||||
@@ -92,13 +90,11 @@ const minifyHtml = (content, ext) => {
|
||||
};
|
||||
|
||||
// Function to generate a dev task for each project's configuration
|
||||
// Note Currently WDS paths are hard-coded to only work for app
|
||||
const genPagesDevTask =
|
||||
(
|
||||
pageEntries,
|
||||
inputRoot,
|
||||
outputRoot,
|
||||
useWDS = false,
|
||||
inputSub = "src/html",
|
||||
publicRoot = ""
|
||||
) =>
|
||||
@@ -109,17 +105,13 @@ const genPagesDevTask =
|
||||
resolve(inputRoot, inputSub, `${page}.template`),
|
||||
{
|
||||
...commonVars,
|
||||
latestEntryJS: entries.map((entry) =>
|
||||
useWDS
|
||||
? `http://localhost:8000/src/entrypoints/${entry}.ts`
|
||||
: `${publicRoot}/frontend_latest/${entry}.js`
|
||||
latestEntryJS: entries.map(
|
||||
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
|
||||
),
|
||||
es5EntryJS: entries.map(
|
||||
(entry) => `${publicRoot}/frontend_es5/${entry}.js`
|
||||
),
|
||||
latestCustomPanelJS: useWDS
|
||||
? "http://localhost:8000/src/entrypoints/custom-panel.ts"
|
||||
: `${publicRoot}/frontend_latest/custom-panel.js`,
|
||||
latestCustomPanelJS: `${publicRoot}/frontend_latest/custom-panel.js`,
|
||||
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
|
||||
}
|
||||
);
|
||||
@@ -176,12 +168,7 @@ const APP_PAGE_ENTRIES = {
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-app-dev",
|
||||
genPagesDevTask(
|
||||
APP_PAGE_ENTRIES,
|
||||
paths.polymer_dir,
|
||||
paths.app_output_root,
|
||||
env.useWDS()
|
||||
)
|
||||
genPagesDevTask(APP_PAGE_ENTRIES, paths.polymer_dir, paths.app_output_root)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
@@ -287,7 +274,6 @@ gulp.task(
|
||||
HASSIO_PAGE_ENTRIES,
|
||||
paths.hassio_dir,
|
||||
paths.hassio_output_root,
|
||||
undefined,
|
||||
"src",
|
||||
paths.hassio_publicPath
|
||||
)
|
||||
|
@@ -66,7 +66,7 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
tokenAuth = JSON.parse(await readFile(TOKEN_FILE, "utf-8"));
|
||||
} catch {
|
||||
if (!allowTokenSetup) {
|
||||
console.log("No token found so build wil continue with English only");
|
||||
console.log("No token found so build will continue with English only");
|
||||
return;
|
||||
}
|
||||
const auth = createOAuthDeviceAuth({
|
||||
|
@@ -9,7 +9,6 @@ const outDir = join(paths.build_dir, "locale-data");
|
||||
|
||||
const INTL_POLYFILLS = {
|
||||
"intl-datetimeformat": "DateTimeFormat",
|
||||
"intl-durationFormat": "DurationFormat",
|
||||
"intl-displaynames": "DisplayNames",
|
||||
"intl-listformat": "ListFormat",
|
||||
"intl-numberformat": "NumberFormat",
|
||||
|
@@ -1,10 +0,0 @@
|
||||
import gulp from "gulp";
|
||||
import { startDevServer } from "@web/dev-server";
|
||||
|
||||
gulp.task("wds-watch-app", async () => {
|
||||
startDevServer({
|
||||
config: {
|
||||
watch: true,
|
||||
},
|
||||
});
|
||||
});
|
@@ -25,9 +25,9 @@ class HcLovelace extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public lovelaceConfig!: LovelaceConfig;
|
||||
|
||||
@property() public viewPath?: string | number | null;
|
||||
@property({ attribute: false }) public viewPath?: string | number | null;
|
||||
|
||||
@property() public urlPath: string | null = null;
|
||||
@property({ attribute: false }) public urlPath: string | null = null;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const index = this._viewIndex;
|
||||
|
@@ -144,10 +144,10 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
|
||||
if (senderId) {
|
||||
this.sendMessage(senderId, status);
|
||||
this._sendMessage(senderId, status);
|
||||
} else {
|
||||
for (const sender of castContext.getSenders()) {
|
||||
this.sendMessage(sender.id, status);
|
||||
this._sendMessage(sender.id, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,10 +164,10 @@ export class HcMain extends HassElement {
|
||||
};
|
||||
|
||||
if (senderId) {
|
||||
this.sendMessage(senderId, error);
|
||||
this._sendMessage(senderId, error);
|
||||
} else {
|
||||
for (const sender of castContext.getSenders()) {
|
||||
this.sendMessage(sender.id, error);
|
||||
this._sendMessage(sender.id, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,7 +394,7 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage(senderId: string, response: any) {
|
||||
private _sendMessage(senderId: string, response: any) {
|
||||
castContext.sendCustomMessage(CAST_NS, senderId, response);
|
||||
}
|
||||
}
|
||||
|
@@ -46,7 +46,6 @@ class CastDemoRow extends LitElement implements LovelaceRow {
|
||||
this.requestUpdate();
|
||||
});
|
||||
mgr.castContext.addEventListener(
|
||||
// eslint-disable-next-line no-undef
|
||||
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
(ev) => {
|
||||
// On Android, opening a new session always results in SESSION_RESUMED.
|
||||
|
@@ -26,7 +26,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _switching = false;
|
||||
|
||||
private _hidden = localStorage.hide_demo_card;
|
||||
private _hidden = window.localStorage.getItem("hide_demo_card");
|
||||
|
||||
public getCardSize() {
|
||||
return this._hidden ? 0 : 2;
|
||||
|
@@ -7,10 +7,10 @@ import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
baseDirectory: _dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
@@ -114,12 +114,10 @@ export default [
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"off",
|
||||
"error",
|
||||
{
|
||||
selector: "default",
|
||||
format: ["camelCase", "snake_case"],
|
||||
leadingUnderscore: "allow",
|
||||
trailingUnderscore: "allow",
|
||||
selector: ["objectLiteralProperty", "objectLiteralMethod"],
|
||||
format: null,
|
||||
},
|
||||
{
|
||||
selector: ["variable"],
|
||||
@@ -127,10 +125,27 @@ export default [
|
||||
leadingUnderscore: "allow",
|
||||
trailingUnderscore: "allow",
|
||||
},
|
||||
{
|
||||
selector: ["variable"],
|
||||
modifiers: ["exported"],
|
||||
format: ["camelCase", "PascalCase", "UPPER_CASE"],
|
||||
},
|
||||
{
|
||||
selector: "typeLike",
|
||||
format: ["PascalCase"],
|
||||
},
|
||||
{
|
||||
selector: "method",
|
||||
modifiers: ["public"],
|
||||
format: ["camelCase"],
|
||||
leadingUnderscore: "forbid",
|
||||
},
|
||||
{
|
||||
selector: "method",
|
||||
modifiers: ["private"],
|
||||
format: ["camelCase"],
|
||||
leadingUnderscore: "require",
|
||||
},
|
||||
],
|
||||
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
@@ -147,16 +162,16 @@ export default [
|
||||
],
|
||||
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"lit/attribute-names": "warn",
|
||||
"lit/attribute-names": "error",
|
||||
"lit/attribute-value-entities": "off",
|
||||
"lit/no-template-map": "off",
|
||||
"lit/no-native-attributes": "warn",
|
||||
"lit/no-this-assign-in-render": "warn",
|
||||
"lit/no-native-attributes": "error",
|
||||
"lit/no-this-assign-in-render": "error",
|
||||
"lit-a11y/click-events-have-key-events": ["off"],
|
||||
"lit-a11y/no-autofocus": "off",
|
||||
"lit-a11y/alt-text": "warn",
|
||||
"lit-a11y/anchor-is-valid": "warn",
|
||||
"lit-a11y/role-has-required-aria-attrs": "warn",
|
||||
"lit-a11y/alt-text": "error",
|
||||
"lit-a11y/anchor-is-valid": "error",
|
||||
"lit-a11y/role-has-required-aria-attrs": "error",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/no-import-type-side-effects": "error",
|
||||
},
|
||||
|
@@ -9,6 +9,7 @@ import "../../../src/components/ha-card";
|
||||
|
||||
@customElement("demo-black-white-row")
|
||||
class DemoBlackWhiteRow extends LitElement {
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property() title!: string;
|
||||
|
||||
@property() value?: any;
|
||||
|
@@ -18,7 +18,8 @@ class DemoCard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public config!: DemoCardConfig;
|
||||
|
||||
@property({ type: Boolean }) public showConfig = false;
|
||||
@property({ attribute: "show-config", type: Boolean })
|
||||
public showConfig = false;
|
||||
|
||||
@state() private _size?: number;
|
||||
|
||||
|
@@ -44,11 +44,11 @@ class DemoCards extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
_showConfigToggled(ev) {
|
||||
private _showConfigToggled(ev) {
|
||||
this._showConfig = ev.target.checked;
|
||||
}
|
||||
|
||||
_darkThemeToggled(ev) {
|
||||
private _darkThemeToggled(ev) {
|
||||
applyThemesOnElement(this._container, { themes: {} } as any, "default", {
|
||||
dark: ev.target.checked,
|
||||
});
|
||||
|
@@ -10,9 +10,10 @@ import type { HomeAssistant } from "../../../src/types";
|
||||
class DemoMoreInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entityId!: string;
|
||||
@property({ attribute: false }) public entityId!: string;
|
||||
|
||||
@property({ type: Boolean }) public showConfig = false;
|
||||
@property({ attribute: "show-config", type: Boolean })
|
||||
public showConfig = false;
|
||||
|
||||
render() {
|
||||
const state = this._getState(this.entityId, this.hass.states);
|
||||
@@ -23,7 +24,7 @@ class DemoMoreInfo extends LitElement {
|
||||
<state-card-content
|
||||
.stateObj=${state}
|
||||
.hass=${this.hass}
|
||||
inDialog
|
||||
in-dialog
|
||||
></state-card-content>
|
||||
|
||||
<more-info-content
|
||||
|
@@ -58,11 +58,11 @@ class DemoMoreInfos extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
_showConfigToggled(ev) {
|
||||
private _showConfigToggled(ev) {
|
||||
this._showConfig = ev.target.checked;
|
||||
}
|
||||
|
||||
_darkThemeToggled(ev) {
|
||||
private _darkThemeToggled(ev) {
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector("#container"),
|
||||
{
|
||||
|
@@ -182,7 +182,7 @@ class HaGallery extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
_menuTapped() {
|
||||
private _menuTapped() {
|
||||
this._drawer.open = !this._drawer.open;
|
||||
}
|
||||
|
||||
|
@@ -63,11 +63,6 @@ class DemoHaAutomationEditorAction extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valueChanged = (ev) => {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
};
|
||||
return html`
|
||||
<div class="options">
|
||||
<ha-formfield label="Disabled">
|
||||
@@ -92,7 +87,7 @@ class DemoHaAutomationEditorAction extends LitElement {
|
||||
.actions=${this.data[sampleIdx]}
|
||||
.sampleIdx=${sampleIdx}
|
||||
.disabled=${this._disabled}
|
||||
@value-changed=${valueChanged}
|
||||
@value-changed=${this._handleValueChange}
|
||||
></ha-automation-action>
|
||||
`
|
||||
)}
|
||||
@@ -102,6 +97,12 @@ class DemoHaAutomationEditorAction extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleValueChange(ev) {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _handleOptionChange(ev) {
|
||||
this[`_${ev.target.name}`] = ev.target.checked;
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
@@ -104,11 +103,6 @@ export class DemoAutomationEditorCondition extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valueChanged = (ev) => {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
};
|
||||
return html`
|
||||
<div class="options">
|
||||
<ha-formfield label="Disabled">
|
||||
@@ -133,7 +127,7 @@ export class DemoAutomationEditorCondition extends LitElement {
|
||||
.conditions=${this.data[sampleIdx]}
|
||||
.sampleIdx=${sampleIdx}
|
||||
.disabled=${this._disabled}
|
||||
@value-changed=${valueChanged}
|
||||
@value-changed=${this._handleValueChange}
|
||||
></ha-automation-condition>
|
||||
`
|
||||
)}
|
||||
@@ -143,6 +137,12 @@ export class DemoAutomationEditorCondition extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleValueChange(ev) {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _handleOptionChange(ev) {
|
||||
this[`_${ev.target.name}`] = ev.target.checked;
|
||||
}
|
||||
|
@@ -149,11 +149,6 @@ export class DemoAutomationEditorTrigger extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valueChanged = (ev) => {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
};
|
||||
return html`
|
||||
<div class="options">
|
||||
<ha-formfield label="Disabled">
|
||||
@@ -178,7 +173,7 @@ export class DemoAutomationEditorTrigger extends LitElement {
|
||||
.triggers=${this.data[sampleIdx]}
|
||||
.sampleIdx=${sampleIdx}
|
||||
.disabled=${this._disabled}
|
||||
@value-changed=${valueChanged}
|
||||
@value-changed=${this._handleValueChange}
|
||||
></ha-automation-trigger>
|
||||
`
|
||||
)}
|
||||
@@ -188,6 +183,12 @@ export class DemoAutomationEditorTrigger extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleValueChange(ev) {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _handleOptionChange(ev) {
|
||||
this[`_${ev.target.name}`] = ev.target.checked;
|
||||
}
|
||||
|
@@ -31,22 +31,17 @@ export class DemoAutomationTrace extends LitElement {
|
||||
<hat-script-graph
|
||||
.trace=${trace.trace}
|
||||
.selected=${this._selected[idx]}
|
||||
@graph-node-selected=${(ev) => {
|
||||
this._selected = { ...this._selected, [idx]: ev.detail.path };
|
||||
}}
|
||||
@graph-node-selected=${this._handleGraphNodeSelected}
|
||||
.sampleIdx=${idx}
|
||||
></hat-script-graph>
|
||||
<hat-trace-timeline
|
||||
allowPick
|
||||
allow-pick
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
.selectedPath=${this._selected[idx]}
|
||||
@value-changed=${(ev) => {
|
||||
this._selected = {
|
||||
...this._selected,
|
||||
[idx]: ev.detail.value,
|
||||
};
|
||||
}}
|
||||
@value-changed=${this._handleTimelineValueChanged}
|
||||
.sampleIdx=${idx}
|
||||
></hat-trace-timeline>
|
||||
<button @click=${() => console.log(trace)}>Log trace</button>
|
||||
</div>
|
||||
@@ -63,6 +58,16 @@ export class DemoAutomationTrace extends LitElement {
|
||||
hass.updateTranslations("config", "en");
|
||||
}
|
||||
|
||||
private _handleTimelineValueChanged(ev) {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this._selected = { ...this._selected, [sampleIdx]: ev.detail.value };
|
||||
}
|
||||
|
||||
private _handleGraphNodeSelected(ev) {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this._selected = { ...this._selected, [sampleIdx]: ev.detail.path };
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
|
@@ -489,14 +489,8 @@ class DemoHaForm extends LitElement {
|
||||
.title=${info.title}
|
||||
.value=${this.data[idx]}
|
||||
.disabled=${this.disabled[idx]}
|
||||
@submitted=${() => {
|
||||
this.disabled[idx] = true;
|
||||
this.requestUpdate();
|
||||
setTimeout(() => {
|
||||
this.disabled[idx] = false;
|
||||
this.requestUpdate();
|
||||
}, 2000);
|
||||
}}
|
||||
@submitted=${this._handleSubmit}
|
||||
.sampleIdx=${idx}
|
||||
>
|
||||
${["light", "dark"].map(
|
||||
(slot) => html`
|
||||
@@ -511,10 +505,8 @@ class DemoHaForm extends LitElement {
|
||||
.computeLabel=${(schema) =>
|
||||
translations[schema.name] || schema.name}
|
||||
.computeHelper=${() => "Helper text"}
|
||||
@value-changed=${(e) => {
|
||||
this.data[idx] = e.detail.value;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
.sampleIdx=${idx}
|
||||
></ha-form>
|
||||
`
|
||||
)}
|
||||
@@ -523,6 +515,22 @@ class DemoHaForm extends LitElement {
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleValueChanged(ev) {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _handleSubmit(ev) {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.disabled[sampleIdx] = true;
|
||||
this.requestUpdate();
|
||||
setTimeout(() => {
|
||||
this.disabled[sampleIdx] = false;
|
||||
this.requestUpdate();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import "@material/mwc-button";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@@ -591,13 +590,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
</div>
|
||||
${SCHEMAS.map((info, idx) => {
|
||||
const data = this.data[idx];
|
||||
const valueChanged = (ev) => {
|
||||
this.data[idx] = {
|
||||
...data,
|
||||
[ev.target.key]: ev.detail.value,
|
||||
};
|
||||
this.requestUpdate();
|
||||
};
|
||||
return html`
|
||||
<demo-black-white-row .title=${info.name}>
|
||||
${["light", "dark"].map((slot) =>
|
||||
@@ -614,7 +606,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
.value=${data[key] ?? value!.default}
|
||||
.disabled=${this._disabled}
|
||||
.required=${this._required}
|
||||
@value-changed=${valueChanged}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
.sampleIdx=${idx}
|
||||
.helper=${this._helper ? "Helper text" : undefined}
|
||||
></ha-selector>
|
||||
</ha-settings-row>
|
||||
@@ -627,6 +620,15 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleValueChanged(ev) {
|
||||
const idx = ev.target.sampleIdx;
|
||||
this.data[idx] = {
|
||||
...this.data[idx],
|
||||
[ev.target.key]: ev.detail.value,
|
||||
};
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _handleOptionChange(ev) {
|
||||
this[`_${ev.target.name}`] = ev.target.checked;
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import { customElement, query } from "lit/decorators";
|
||||
import { CoverEntityFeature } from "../../../../src/data/cover";
|
||||
import { LightColorMode } from "../../../../src/data/light";
|
||||
import { LockEntityFeature } from "../../../../src/data/lock";
|
||||
import { MediaPlayerEntityFeature } from "../../../../src/data/media-player";
|
||||
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
@@ -28,6 +29,10 @@ const ENTITIES = [
|
||||
device_class: "lock",
|
||||
supported_features: LockEntityFeature.OPEN,
|
||||
}),
|
||||
getEntity("media_player", "living_room", "playing", {
|
||||
friendly_name: "Living room speaker",
|
||||
supported_features: MediaPlayerEntityFeature.VOLUME_SET,
|
||||
}),
|
||||
getEntity("climate", "thermostat", "heat", {
|
||||
current_temperature: 73,
|
||||
min_temp: 45,
|
||||
@@ -197,6 +202,15 @@ const CONFIGS = [
|
||||
- type: "lock-open-door"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Media player volume slider feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: media_player.living_room
|
||||
features:
|
||||
- type: "media-player-volume-slider"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Vacuum commands feature",
|
||||
config: `
|
||||
|
@@ -136,7 +136,7 @@ export class HassioAddonStore extends LitElement {
|
||||
this._manageRepositories(repositoryUrl);
|
||||
}
|
||||
|
||||
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ export class HassioAddonStore extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private apiCalled(ev) {
|
||||
private _apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this._loadData();
|
||||
}
|
||||
|
@@ -58,7 +58,7 @@ export class HassioBackups extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@state() private _selectedBackups: string[] = [];
|
||||
|
||||
@@ -74,7 +74,7 @@ export class HassioBackups extends LitElement {
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hass && this._firstUpdatedCalled) {
|
||||
this.fetchBackups();
|
||||
this._fetchBackups();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export class HassioBackups extends LitElement {
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass && this.isConnected) {
|
||||
this.fetchBackups();
|
||||
this._fetchBackups();
|
||||
}
|
||||
this._firstUpdatedCalled = true;
|
||||
}
|
||||
@@ -198,7 +198,7 @@ export class HassioBackups extends LitElement {
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
clickable
|
||||
selectable
|
||||
hasFab
|
||||
has-fab
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? "/config/system"
|
||||
@@ -280,7 +280,7 @@ export class HassioBackups extends LitElement {
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this.fetchBackups();
|
||||
this._fetchBackups();
|
||||
break;
|
||||
case 1:
|
||||
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
|
||||
@@ -303,13 +303,13 @@ export class HassioBackups extends LitElement {
|
||||
showHassioBackupDialog(this, {
|
||||
slug,
|
||||
supervisor: this.supervisor,
|
||||
onDelete: () => this.fetchBackups(),
|
||||
onDelete: () => this._fetchBackups(),
|
||||
}),
|
||||
reloadBackup: () => this.fetchBackups(),
|
||||
reloadBackup: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
private async fetchBackups() {
|
||||
private async _fetchBackups() {
|
||||
this._isLoading = true;
|
||||
await reloadHassioBackups(this.hass);
|
||||
this._backups = await fetchHassioBackups(this.hass);
|
||||
@@ -341,7 +341,7 @@ export class HassioBackups extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this.fetchBackups();
|
||||
await this._fetchBackups();
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ export class HassioBackups extends LitElement {
|
||||
showHassioBackupDialog(this, {
|
||||
slug,
|
||||
supervisor: this.supervisor,
|
||||
onDelete: () => this.fetchBackups(),
|
||||
onDelete: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ export class HassioBackups extends LitElement {
|
||||
}
|
||||
showHassioCreateBackupDialog(this, {
|
||||
supervisor: this.supervisor!,
|
||||
onCreate: () => this.fetchBackups(),
|
||||
onCreate: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -9,23 +9,24 @@ import type { HomeAssistant } from "../../../src/types";
|
||||
class HassioCardContent extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property() public title!: string;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@property({ type: Boolean }) public available = true;
|
||||
|
||||
@property({ type: Boolean }) public showTopbar = false;
|
||||
@property({ attribute: false }) public showTopbar = false;
|
||||
|
||||
@property() public topbarClass?: string;
|
||||
@property({ attribute: false }) public topbarClass?: string;
|
||||
|
||||
@property() public iconTitle?: string;
|
||||
@property({ attribute: false }) public iconTitle?: string;
|
||||
|
||||
@property() public iconClass?: string;
|
||||
@property({ attribute: false }) public iconClass?: string;
|
||||
|
||||
@property() public icon = mdiHelpCircle;
|
||||
|
||||
@property() public iconImage?: string;
|
||||
@property({ attribute: false }) public iconImage?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
@@ -35,7 +36,11 @@ class HassioCardContent extends LitElement {
|
||||
${this.iconImage
|
||||
? html`
|
||||
<div class="icon_image ${this.iconClass}">
|
||||
<img src=${this.iconImage} .title=${this.iconTitle} />
|
||||
<img
|
||||
src=${this.iconImage}
|
||||
.title=${this.iconTitle}
|
||||
alt=${this.iconTitle ?? ""}
|
||||
/>
|
||||
<div></div>
|
||||
</div>
|
||||
`
|
||||
|
@@ -73,23 +73,24 @@ export class SupervisorBackupContent extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
||||
|
||||
@property() public backupType: HassioBackupDetail["type"] = "full";
|
||||
@property({ attribute: false })
|
||||
public backupType: HassioBackupDetail["type"] = "full";
|
||||
|
||||
@property({ attribute: false }) public folders?: CheckboxItem[];
|
||||
|
||||
@property({ attribute: false }) public addons?: AddonCheckboxItem[];
|
||||
|
||||
@property({ type: Boolean }) public homeAssistant = false;
|
||||
@property({ attribute: false }) public homeAssistant = false;
|
||||
|
||||
@property({ type: Boolean }) public backupHasPassword = false;
|
||||
@property({ attribute: false }) public backupHasPassword = false;
|
||||
|
||||
@property({ type: Boolean }) public onboarding = false;
|
||||
|
||||
@property() public backupName = "";
|
||||
@property({ attribute: false }) public backupName = "";
|
||||
|
||||
@property() public backupPassword = "";
|
||||
@property({ attribute: false }) public backupPassword = "";
|
||||
|
||||
@property() public confirmBackupPassword = "";
|
||||
@property({ attribute: false }) public confirmBackupPassword = "";
|
||||
|
||||
@query("ha-textfield, ha-radio, ha-checkbox", true) private _focusTarget;
|
||||
|
||||
@@ -191,7 +192,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.homeAssistant}
|
||||
@change=${this.toggleHomeAssistant}
|
||||
@change=${this._toggleHomeAssistant}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
@@ -277,7 +278,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private toggleHomeAssistant() {
|
||||
private _toggleHomeAssistant() {
|
||||
this.homeAssistant = !this.homeAssistant;
|
||||
}
|
||||
|
||||
|
@@ -7,9 +7,9 @@ import "../../../src/components/ha-svg-icon";
|
||||
class SupervisorFormfieldLabel extends LitElement {
|
||||
@property({ type: String }) public label!: string;
|
||||
|
||||
@property({ type: String }) public imageUrl?: string;
|
||||
@property({ attribute: false }) public imageUrl?: string;
|
||||
|
||||
@property({ type: String }) public iconPath?: string;
|
||||
@property({ attribute: false }) public iconPath?: string;
|
||||
|
||||
@property({ type: String }) public version?: string;
|
||||
|
||||
|
@@ -76,7 +76,7 @@ class HassioDashboard extends LitElement {
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
hasFab
|
||||
has-fab
|
||||
>
|
||||
<span slot="header">
|
||||
${this.supervisor.localize(
|
||||
|
@@ -95,7 +95,7 @@ class HassioDatadiskDialog extends LitElement {
|
||||
.label=${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.select_device"
|
||||
)}
|
||||
@selected=${this._select_device}
|
||||
@selected=${this._selectDevice}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this.devices.map(
|
||||
@@ -137,7 +137,7 @@ class HassioDatadiskDialog extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _select_device(ev) {
|
||||
private _selectDevice(ev) {
|
||||
this.selectedDevice = ev.target.value;
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import type { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
|
||||
class HassioMarkdownDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property() public title!: string;
|
||||
|
||||
@property() public content!: string;
|
||||
|
@@ -394,7 +394,7 @@ export class DialogHassioNetwork
|
||||
`;
|
||||
}
|
||||
|
||||
_toArray(data: string | string[]): string[] {
|
||||
private _toArray(data: string | string[]): string[] {
|
||||
if (Array.isArray(data)) {
|
||||
if (data && typeof data[0] === "string") {
|
||||
data = data[0];
|
||||
@@ -409,7 +409,7 @@ export class DialogHassioNetwork
|
||||
return data;
|
||||
}
|
||||
|
||||
_toString(data: string | string[]): string {
|
||||
private _toString(data: string | string[]): string {
|
||||
if (!data) {
|
||||
return "";
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ class HassioIngressView extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public ingressPanel = false;
|
||||
@property({ attribute: false }) public ingressPanel = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
|
@@ -58,10 +58,10 @@ const SUPERVISOR_UPDATE_NAMES = {
|
||||
supervisor: "Home Assistant Supervisor",
|
||||
};
|
||||
|
||||
type updateType = "os" | "supervisor" | "core" | "addon";
|
||||
type UpdateType = "os" | "supervisor" | "core" | "addon";
|
||||
|
||||
const changelogUrl = (
|
||||
entry: updateType,
|
||||
entry: UpdateType,
|
||||
version: string
|
||||
): string | undefined => {
|
||||
if (entry === "addon") {
|
||||
@@ -99,7 +99,7 @@ class UpdateAvailableCard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public addonSlug?: string;
|
||||
|
||||
@state() private _updateType?: updateType;
|
||||
@state() private _updateType?: UpdateType;
|
||||
|
||||
@state() private _changelogContent?: string;
|
||||
|
||||
@@ -222,7 +222,7 @@ class UpdateAvailableCard extends LitElement {
|
||||
const updateType = ["core", "os", "supervisor"].includes(pathPart)
|
||||
? pathPart
|
||||
: "addon";
|
||||
this._updateType = updateType as updateType;
|
||||
this._updateType = updateType as UpdateType;
|
||||
|
||||
switch (updateType) {
|
||||
case "addon":
|
||||
|
@@ -64,9 +64,9 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
<ha-language-picker
|
||||
.value=${this.language}
|
||||
.label=${""}
|
||||
nativeName
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inlineArrow
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<a
|
||||
href="https://www.home-assistant.io/getting-started/onboarding/"
|
||||
@@ -122,7 +122,10 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
if (language !== this.language && language) {
|
||||
this.language = language;
|
||||
try {
|
||||
localStorage.setItem("selectedLanguage", JSON.stringify(language));
|
||||
window.localStorage.setItem(
|
||||
"selectedLanguage",
|
||||
JSON.stringify(language)
|
||||
);
|
||||
} catch (err: any) {
|
||||
// Ignore
|
||||
}
|
||||
|
64
package.json
64
package.json
@@ -8,7 +8,7 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "script/build_frontend",
|
||||
"lint:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore",
|
||||
"lint:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
|
||||
"format:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:prettier": "prettier . --cache --check",
|
||||
"format:prettier": "prettier . --cache --write",
|
||||
@@ -30,21 +30,21 @@
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
"@codemirror/autocomplete": "6.18.3",
|
||||
"@codemirror/commands": "6.7.1",
|
||||
"@codemirror/language": "6.10.3",
|
||||
"@codemirror/language": "6.10.6",
|
||||
"@codemirror/legacy-modes": "6.4.2",
|
||||
"@codemirror/search": "6.5.8",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.35.0",
|
||||
"@codemirror/view": "6.35.2",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.16.5",
|
||||
"@formatjs/intl-displaynames": "6.8.5",
|
||||
"@formatjs/intl-durationformat": "0.6.4",
|
||||
"@formatjs/intl-datetimeformat": "6.16.6",
|
||||
"@formatjs/intl-displaynames": "6.8.6",
|
||||
"@formatjs/intl-durationformat": "0.6.5",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.3",
|
||||
"@formatjs/intl-listformat": "7.7.5",
|
||||
"@formatjs/intl-locale": "4.2.5",
|
||||
"@formatjs/intl-numberformat": "8.14.5",
|
||||
"@formatjs/intl-pluralrules": "5.3.5",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.5",
|
||||
"@formatjs/intl-listformat": "7.7.6",
|
||||
"@formatjs/intl-locale": "4.2.6",
|
||||
"@formatjs/intl-numberformat": "8.14.6",
|
||||
"@formatjs/intl-pluralrules": "5.3.6",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.6",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
@@ -91,8 +91,8 @@
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.5.4",
|
||||
"@vaadin/vaadin-themable-mixin": "24.5.4",
|
||||
"@vaadin/combo-box": "24.5.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.5.5",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -101,7 +101,7 @@
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "2.3.1",
|
||||
"chart.js": "4.4.6",
|
||||
"chart.js": "4.4.7",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.39.0",
|
||||
@@ -111,20 +111,20 @@
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"element-internals-polyfill": "1.3.11",
|
||||
"element-internals-polyfill": "1.3.12",
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.7.7",
|
||||
"intl-messageformat": "10.7.8",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"lit": "2.8.0",
|
||||
"lit-html": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "15.0.2",
|
||||
"marked": "15.0.3",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@@ -162,23 +162,21 @@
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.17.0",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.8.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.2",
|
||||
"@octokit/rest": "21.0.2",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rsdoctor/rspack-plugin": "0.4.8",
|
||||
"@rspack/cli": "1.1.4",
|
||||
"@rspack/core": "1.1.4",
|
||||
"@rsdoctor/rspack-plugin": "0.4.11",
|
||||
"@rspack/cli": "1.1.5",
|
||||
"@rspack/core": "1.1.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.19",
|
||||
"@types/chromecast-caf-receiver": "6.0.20",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.14",
|
||||
"@types/leaflet": "1.9.15",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.4.2",
|
||||
@@ -191,13 +189,12 @@
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "7.18.0",
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
"@vitest/coverage-v8": "2.1.5",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@vitest/coverage-v8": "2.1.8",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.15.0",
|
||||
"eslint": "9.16.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "18.0.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
@@ -214,30 +211,29 @@
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "25.0.1",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.10",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.13",
|
||||
"magic-string": "0.30.14",
|
||||
"map-stream": "0.0.7",
|
||||
"object-hash": "3.0.0",
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"prettier": "3.4.2",
|
||||
"rspack-manifest-plugin": "5.0.2",
|
||||
"serve-handler": "6.1.6",
|
||||
"sinon": "19.0.2",
|
||||
"systemjs": "6.15.1",
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.7.2",
|
||||
"vitest": "2.1.5",
|
||||
"vitest": "2.1.8",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
@@ -251,7 +247,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"globals": "15.12.0"
|
||||
"globals": "15.13.0"
|
||||
},
|
||||
"packageManager": "yarn@4.5.2"
|
||||
"packageManager": "yarn@4.5.3"
|
||||
}
|
||||
|
@@ -43,12 +43,6 @@
|
||||
"description": "Group date-fns with dependent timezone package",
|
||||
"groupName": "date-fns",
|
||||
"matchPackageNames": ["date-fns", "date-fns-tz"]
|
||||
},
|
||||
{
|
||||
"description": "Group and temporarily disable WDS packages",
|
||||
"groupName": "Web Dev Server",
|
||||
"enabled": false,
|
||||
"matchPackageNames": ["@web/dev-server{/,}**"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
70
script/develop_and_serve
Executable file
70
script/develop_and_serve
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# This script can be used to develop and test the frontend without having to
|
||||
# link the build in a running core instance through the frontend/development_repo setting.
|
||||
#
|
||||
# WARNING:
|
||||
# If you have an active login session in the frontend. The core that was used
|
||||
# as a backend during the time of the login remains used until you logout again.
|
||||
# So if you reuse the url hosting the frontend, you will need to logout before
|
||||
# it will actually start using the core backend configured by this script.
|
||||
#
|
||||
# If you run this script without parameters, the frontend will be accessible under http://localhost:8124.
|
||||
# And it will use the core instance running under http://localhost:8123 as a backend.
|
||||
# Note that from a devcontainer, the frontend will be accessible under port 8124 on the host container.
|
||||
# Inside the devcontainer it will be accessible under port 8123 instead.
|
||||
# The core instance endpoint remains the same in both cases, as this is resolved from the browser.
|
||||
#
|
||||
# You can change the core instance the frontend connects to by passing the -c option.
|
||||
# For example: script/develop_and_serve -c https://myhost.duckdns.org:8123
|
||||
# This will also work for existing production core instances.
|
||||
# It does not need to be a development version hosted locally.
|
||||
#
|
||||
# You can change the port the frontend is served on by passing the -p option.
|
||||
# For example: script/develop_and_serve -p 8654
|
||||
# Note that if you are running from a devcontainer, you will need to setup
|
||||
# port forwarding as well if you want to access it from the container host.
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# parse input paramters
|
||||
if [ -n "$DEVCONTAINER" ]; then
|
||||
frontendPort=8123
|
||||
else
|
||||
frontendPort=8124
|
||||
fi
|
||||
|
||||
coreUrl=http://localhost:8123
|
||||
|
||||
while getopts p:c:h flag
|
||||
do
|
||||
case "${flag}" in
|
||||
p) frontendPort=${OPTARG};;
|
||||
c) coreUrl="${OPTARG}";;
|
||||
h) echo Documentation can be found inside "$0" && exit 0;;
|
||||
*) echo Documentation can be found inside "$0" && exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# display used settings
|
||||
if [ -n "$DEVCONTAINER" ]; then
|
||||
echo Frontend is available inside container as http://localhost:${frontendPort}
|
||||
if [ 8123 -eq $frontendPort ]; then
|
||||
echo Frontend is available on container host as http://localhost:8124
|
||||
fi
|
||||
else
|
||||
echo Frontend is hosted on http://localhost:${frontendPort}
|
||||
fi
|
||||
echo Core is used from ${coreUrl}
|
||||
|
||||
# build the frontend so it connects to the passed core
|
||||
HASS_URL="$coreUrl" ./script/develop &
|
||||
|
||||
# serve the frontend
|
||||
yarn dlx serve -l $frontendPort ./hass_frontend -s &
|
||||
|
||||
# keep the script running while serving
|
||||
wait
|
@@ -30,17 +30,17 @@ type State = "loading" | "error" | "step";
|
||||
export class HaAuthFlow extends LitElement {
|
||||
@property({ attribute: false }) public authProvider?: AuthProvider;
|
||||
|
||||
@property() public clientId?: string;
|
||||
@property({ attribute: false }) public clientId?: string;
|
||||
|
||||
@property() public redirectUri?: string;
|
||||
@property({ attribute: false }) public redirectUri?: string;
|
||||
|
||||
@property() public oauth2State?: string;
|
||||
@property({ attribute: false }) public oauth2State?: string;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public step?: DataEntryFlowStep;
|
||||
|
||||
@property({ type: Boolean }) public initStoreToken = false;
|
||||
@property({ attribute: false }) public initStoreToken = false;
|
||||
|
||||
@state() private _storeToken = false;
|
||||
|
||||
|
@@ -21,13 +21,13 @@ const appNames = {
|
||||
|
||||
@customElement("ha-authorize")
|
||||
export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
@property() public clientId?: string;
|
||||
@property({ attribute: false }) public clientId?: string;
|
||||
|
||||
@property() public redirectUri?: string;
|
||||
@property({ attribute: false }) public redirectUri?: string;
|
||||
|
||||
@property() public oauth2State?: string;
|
||||
@property({ attribute: false }) public oauth2State?: string;
|
||||
|
||||
@property() public translationFragment = "page-authorize";
|
||||
@property({ attribute: false }) public translationFragment = "page-authorize";
|
||||
|
||||
@state() private _authProvider?: AuthProvider;
|
||||
|
||||
@@ -202,9 +202,9 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
<ha-language-picker
|
||||
.value=${this.language}
|
||||
.label=${""}
|
||||
nativeName
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inlineArrow
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<a
|
||||
href="https://www.home-assistant.io/docs/authentication/"
|
||||
@@ -327,7 +327,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
this.language = language;
|
||||
|
||||
try {
|
||||
localStorage.setItem("selectedLanguage", JSON.stringify(language));
|
||||
window.localStorage.setItem("selectedLanguage", JSON.stringify(language));
|
||||
} catch (err: any) {
|
||||
// Ignore
|
||||
}
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import type { AuthData } from "home-assistant-js-websocket";
|
||||
import { extractSearchParam } from "../url/search-params";
|
||||
|
||||
const storage = window.localStorage || {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__tokenCache: {
|
||||
@@ -38,9 +36,15 @@ export function saveTokens(tokens: AuthData | null) {
|
||||
|
||||
if (tokenCache.writeEnabled) {
|
||||
try {
|
||||
storage.hassTokens = JSON.stringify(tokens);
|
||||
window.localStorage.setItem("hassTokens", JSON.stringify(tokens));
|
||||
} catch (err: any) {
|
||||
// write failed, ignore it. Happens if storage is full or private mode.
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"Failed to store tokens; Are you in private mode or is your storage full?"
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error storing tokens:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,12 +55,11 @@ export function enableWrite() {
|
||||
saveTokens(tokenCache.tokens);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadTokens() {
|
||||
if (tokenCache.tokens === undefined) {
|
||||
try {
|
||||
// Delete the old token cache.
|
||||
delete storage.tokens;
|
||||
const tokens = storage.hassTokens;
|
||||
const tokens = window.localStorage.getItem("hassTokens");
|
||||
if (tokens) {
|
||||
tokenCache.tokens = JSON.parse(tokens);
|
||||
tokenCache.writeEnabled = true;
|
||||
|
@@ -25,9 +25,11 @@ export const rgb2hex = (rgb: [number, number, number]): string =>
|
||||
// Copyright (c) 2011-2019, Gregor Aisch
|
||||
|
||||
// Constants for XYZ and LAB conversion
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const Xn = 0.95047;
|
||||
const Yn = 1;
|
||||
const Zn = 1.08883;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
const t0 = 0.137931034; // 4 / 29
|
||||
const t1 = 0.206896552; // 6 / 29
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { DurationFormat } from "@formatjs/intl-durationformat";
|
||||
import type { DurationInput } from "@formatjs/intl-durationformat/src/types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HaDurationData } from "../../components/ha-duration-input";
|
||||
@@ -49,7 +48,7 @@ export const formatNumericDuration = (
|
||||
|
||||
const formatDurationLongMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new DurationFormat(locale.language, {
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "long",
|
||||
})
|
||||
);
|
||||
@@ -61,7 +60,7 @@ export const formatDurationLong = (
|
||||
|
||||
const formatDigitalDurationMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new DurationFormat(locale.language, {
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "digital",
|
||||
hoursDisplay: "auto",
|
||||
})
|
||||
@@ -72,13 +71,13 @@ export const formatDurationDigital = (
|
||||
duration: HaDurationData
|
||||
) => formatDigitalDurationMem(locale).format(duration);
|
||||
|
||||
export const DURATION_UNITS = ["ms", "s", "min", "h", "d"] as const;
|
||||
export const DURATION_UNITS = ["min", "h", "d"] as const;
|
||||
|
||||
type DurationUnit = (typeof DURATION_UNITS)[number];
|
||||
|
||||
const formatDurationDayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new DurationFormat(locale.language, {
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
daysDisplay: "always",
|
||||
})
|
||||
@@ -86,7 +85,7 @@ const formatDurationDayMem = memoizeOne(
|
||||
|
||||
const formatDurationHourMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new DurationFormat(locale.language, {
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
hoursDisplay: "always",
|
||||
})
|
||||
@@ -94,28 +93,12 @@ const formatDurationHourMem = memoizeOne(
|
||||
|
||||
const formatDurationMinuteMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new DurationFormat(locale.language, {
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
minutesDisplay: "always",
|
||||
})
|
||||
);
|
||||
|
||||
const formatDurationSecondMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new DurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
secondsDisplay: "always",
|
||||
})
|
||||
);
|
||||
|
||||
const formatDurationMillisecondMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new DurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
millisecondsDisplay: "always",
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDuration = (
|
||||
locale: FrontendLocaleData,
|
||||
duration: string,
|
||||
@@ -155,22 +138,6 @@ export const formatDuration = (
|
||||
};
|
||||
return formatDurationMinuteMem(locale).format(input);
|
||||
}
|
||||
case "s": {
|
||||
const seconds = Math.floor(value);
|
||||
const milliseconds = Math.floor((value - seconds) * 1000);
|
||||
const input: DurationInput = {
|
||||
seconds,
|
||||
milliseconds,
|
||||
};
|
||||
return formatDurationSecondMem(locale).format(input);
|
||||
}
|
||||
case "ms": {
|
||||
const milliseconds = Math.floor(value);
|
||||
const input: DurationInput = {
|
||||
milliseconds,
|
||||
};
|
||||
return formatDurationMillisecondMem(locale).format(input);
|
||||
}
|
||||
default:
|
||||
throw new Error("Invalid duration unit");
|
||||
}
|
||||
|
@@ -29,7 +29,6 @@
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line
|
||||
interface HASSDomEvents {}
|
||||
}
|
||||
|
||||
|
@@ -14,10 +14,8 @@ export default function scrollToTarget(element, target) {
|
||||
const top = 0;
|
||||
const scroller = target;
|
||||
const easingFn = function easeOutQuad(t, b, c, d) {
|
||||
/* eslint-disable no-param-reassign, space-infix-ops, no-mixed-operators */
|
||||
t /= d;
|
||||
return -c * t * (t - 2) + b;
|
||||
/* eslint-enable no-param-reassign, space-infix-ops, no-mixed-operators */
|
||||
};
|
||||
const animationId = Math.random();
|
||||
const duration = 200;
|
||||
|
@@ -56,13 +56,15 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
}
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
|
||||
const is_number_domain =
|
||||
domain === "counter" || domain === "number" || domain === "input_number";
|
||||
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||
if (
|
||||
isNumericFromAttributes(
|
||||
attributes,
|
||||
domain === "sensor" ? sensorNumericDeviceClasses : []
|
||||
)
|
||||
) ||
|
||||
is_number_domain
|
||||
) {
|
||||
// state is duration
|
||||
if (
|
||||
@@ -165,20 +167,6 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
}
|
||||
}
|
||||
|
||||
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
|
||||
if (
|
||||
domain === "counter" ||
|
||||
domain === "number" ||
|
||||
domain === "input_number"
|
||||
) {
|
||||
// Format as an integer if the value and step are integers
|
||||
return formatNumber(
|
||||
state,
|
||||
locale,
|
||||
getNumberFormatOptions({ state, attributes } as HassEntity, entity)
|
||||
);
|
||||
}
|
||||
|
||||
// state is a timestamp
|
||||
if (
|
||||
[
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { historyPromise } from "../state/url-sync-mixin";
|
||||
import { fireEvent } from "./dom/fire_event";
|
||||
import { mainWindow } from "./dom/get_main_window";
|
||||
|
||||
@@ -17,11 +16,6 @@ export interface NavigateOptions {
|
||||
export const navigate = (path: string, options?: NavigateOptions) => {
|
||||
const replace = options?.replace || false;
|
||||
|
||||
if (historyPromise) {
|
||||
historyPromise.then(() => navigate(path, options));
|
||||
return;
|
||||
}
|
||||
|
||||
if (__DEMO__) {
|
||||
if (replace) {
|
||||
mainWindow.history.replaceState(
|
||||
|
@@ -12,7 +12,7 @@ export type FormatEntityAttributeValueFunc = (
|
||||
attribute: string,
|
||||
value?: any
|
||||
) => string;
|
||||
export type formatEntityAttributeNameFunc = (
|
||||
export type FormatEntityAttributeNameFunc = (
|
||||
stateObj: HassEntity,
|
||||
attribute: string
|
||||
) => string;
|
||||
@@ -26,7 +26,7 @@ export const computeFormatFunctions = async (
|
||||
): Promise<{
|
||||
formatEntityState: FormatEntityStateFunc;
|
||||
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
||||
formatEntityAttributeName: formatEntityAttributeNameFunc;
|
||||
formatEntityAttributeName: FormatEntityAttributeNameFunc;
|
||||
}> => {
|
||||
const { computeStateDisplay } = await import(
|
||||
"../entity/compute_state_display"
|
||||
|
@@ -94,6 +94,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
||||
resources: Resources,
|
||||
formats?: FormatsType
|
||||
): Promise<LocalizeFunc<Keys>> => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { IntlMessageFormat } = await import("intl-messageformat");
|
||||
await polyfillLocaleData(language);
|
||||
|
||||
|
@@ -53,9 +53,10 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@property({ type: Number }) public height?: number;
|
||||
|
||||
@property({ type: Number }) public paddingYAxis = 0;
|
||||
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
|
||||
|
||||
@property({ type: Boolean }) public externalHidden = false;
|
||||
@property({ attribute: "external-hidden", type: Boolean })
|
||||
public externalHidden = false;
|
||||
|
||||
@state() private _chartHeight?: number;
|
||||
|
||||
@@ -316,6 +317,7 @@ export class HaChartBase extends LitElement {
|
||||
.getContext("2d")!;
|
||||
this._loading = true;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
@@ -32,25 +32,28 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property() public identifier?: string;
|
||||
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
@property({ attribute: "show-names", type: Boolean })
|
||||
public showNames = true;
|
||||
|
||||
@property({ type: Boolean }) public clickForMoreInfo = true;
|
||||
@property({ attribute: "click-for-more-info", type: Boolean })
|
||||
public clickForMoreInfo = true;
|
||||
|
||||
@property({ attribute: false }) public startTime!: Date;
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@property({ type: Number }) public paddingYAxis = 0;
|
||||
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
|
||||
|
||||
@property({ type: Number }) public chartIndex?;
|
||||
@property({ attribute: false, type: Number }) public chartIndex?;
|
||||
|
||||
@property({ type: Boolean }) public logarithmicScale = false;
|
||||
@property({ attribute: "logarithmic-scale", type: Boolean })
|
||||
public logarithmicScale = false;
|
||||
|
||||
@property({ type: Number }) public minYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
|
||||
@property({ type: Number }) public maxYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
|
||||
@property({ type: Boolean }) public fitYData = false;
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
@state() private _chartData?: ChartData<"line">;
|
||||
|
||||
|
@@ -30,9 +30,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property() public identifier?: string;
|
||||
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
|
||||
|
||||
@property({ type: Boolean }) public clickForMoreInfo = true;
|
||||
@property({ attribute: "click-for-more-info", type: Boolean })
|
||||
public clickForMoreInfo = true;
|
||||
|
||||
@property({ type: Boolean }) public chunked = false;
|
||||
|
||||
@@ -40,9 +41,9 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@property({ type: Number }) public paddingYAxis = 0;
|
||||
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
|
||||
|
||||
@property({ type: Number }) public chartIndex?;
|
||||
@property({ attribute: false, type: Number }) public chartIndex?;
|
||||
|
||||
@state() private _chartData?: ChartData<"timeline">;
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
queryAll,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import type {
|
||||
@@ -58,21 +59,24 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||
|
||||
@property({ type: Number }) public hoursToShow?: number;
|
||||
@property({ attribute: false, type: Number }) public hoursToShow?: number;
|
||||
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
|
||||
|
||||
@property({ type: Boolean }) public clickForMoreInfo = true;
|
||||
@property({ attribute: "click-for-more-info", type: Boolean })
|
||||
public clickForMoreInfo = true;
|
||||
|
||||
@property({ type: Boolean }) public isLoadingData = false;
|
||||
@property({ attribute: "is-loading-data", type: Boolean })
|
||||
public isLoadingData = false;
|
||||
|
||||
@property({ type: Boolean }) public logarithmicScale = false;
|
||||
@property({ attribute: "logarithmic-scale", type: Boolean })
|
||||
public logarithmicScale = false;
|
||||
|
||||
@property({ type: Number }) public minYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
|
||||
@property({ type: Number }) public maxYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
|
||||
@property({ type: Boolean }) public fitYData = false;
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
private _computedStartTime!: Date;
|
||||
|
||||
@@ -122,6 +126,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
).concat(this.historyData.line)
|
||||
: this.historyData.line;
|
||||
|
||||
// eslint-disable-next-line lit/no-this-assign-in-render
|
||||
this._chartCount = combinedItems.length;
|
||||
|
||||
return this.virtualize
|
||||
@@ -139,12 +144,12 @@ export class StateHistoryCharts extends LitElement {
|
||||
)}`;
|
||||
}
|
||||
|
||||
private _renderHistoryItem = (
|
||||
item: TimelineEntity[] | LineChartUnit,
|
||||
index: number
|
||||
) => {
|
||||
private _renderHistoryItem: RenderItemFunction<
|
||||
TimelineEntity[] | LineChartUnit
|
||||
> = (item, index) => {
|
||||
if (!item || index === undefined) {
|
||||
return nothing;
|
||||
// eslint-disable-next-line lit/prefer-nothing
|
||||
return html``;
|
||||
}
|
||||
if (!Array.isArray(item)) {
|
||||
return html`<div class="entry-container">
|
||||
|
@@ -63,28 +63,28 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ type: Array }) public statTypes: Array<StatisticType> = [
|
||||
"sum",
|
||||
"min",
|
||||
"mean",
|
||||
"max",
|
||||
];
|
||||
@property({ attribute: false, type: Array })
|
||||
public statTypes: Array<StatisticType> = ["sum", "min", "mean", "max"];
|
||||
|
||||
@property() public chartType: ChartType = "line";
|
||||
@property({ attribute: false }) public chartType: ChartType = "line";
|
||||
|
||||
@property({ type: Number }) public minYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
|
||||
@property({ type: Number }) public maxYAxis?: number;
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
|
||||
@property({ type: Boolean }) public fitYData = false;
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
@property({ type: Boolean }) public hideLegend = false;
|
||||
@property({ attribute: "hide-legend", type: Boolean }) public hideLegend =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public logarithmicScale = false;
|
||||
@property({ attribute: "logarithmic-scale", type: Boolean })
|
||||
public logarithmicScale = false;
|
||||
|
||||
@property({ type: Boolean }) public isLoadingData = false;
|
||||
@property({ attribute: "is-loading-data", type: Boolean })
|
||||
public isLoadingData = false;
|
||||
|
||||
@property({ type: Boolean }) public clickForMoreInfo = true;
|
||||
@property({ attribute: "click-for-more-info", type: Boolean })
|
||||
public clickForMoreInfo = true;
|
||||
|
||||
@property() public period?: string;
|
||||
|
||||
@@ -167,7 +167,7 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-chart-base
|
||||
externalHidden
|
||||
external-hidden
|
||||
.hass=${this.hass}
|
||||
.data=${this._chartData}
|
||||
.extraData=${this._chartDatasetExtra}
|
||||
|
@@ -185,7 +185,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
}
|
||||
|
||||
_toggle(ev) {
|
||||
private _toggle(ev) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
@@ -266,7 +266,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
}
|
||||
|
||||
_reset() {
|
||||
private _reset() {
|
||||
this._columnOrder = undefined;
|
||||
this._hiddenColumns = undefined;
|
||||
|
||||
|
@@ -116,7 +116,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public clickable = false;
|
||||
|
||||
@property({ type: Boolean }) public hasFab = false;
|
||||
@property({ attribute: "has-fab", type: Boolean }) public hasFab = false;
|
||||
|
||||
/**
|
||||
* Add an extra row at the bottom of the data table
|
||||
@@ -127,24 +127,25 @@ export class HaDataTable extends LitElement {
|
||||
@property({ type: Boolean, attribute: "auto-height" })
|
||||
public autoHeight = false;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: String }) public id = "id";
|
||||
|
||||
@property({ type: String }) public noDataText?: string;
|
||||
@property({ attribute: false, type: String }) public noDataText?: string;
|
||||
|
||||
@property({ type: String }) public searchLabel?: string;
|
||||
@property({ attribute: false, type: String }) public searchLabel?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-label-float" })
|
||||
public noLabelFloat? = false;
|
||||
|
||||
@property({ type: String }) public filter = "";
|
||||
|
||||
@property() public groupColumn?: string;
|
||||
@property({ attribute: false }) public groupColumn?: string;
|
||||
|
||||
@property({ attribute: false }) public groupOrder?: string[];
|
||||
|
||||
@property() public sortColumn?: string;
|
||||
@property({ attribute: false }) public sortColumn?: string;
|
||||
|
||||
@property() public sortDirection: SortingDirection = null;
|
||||
@property({ attribute: false }) public sortDirection: SortingDirection = null;
|
||||
|
||||
@property({ attribute: false }) public initialCollapsedGroups?: string[];
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
} from "../common/datetime/localize_date";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const CustomDateRangePicker = Vue.extend({
|
||||
mixins: [DateRangePicker],
|
||||
methods: {
|
||||
@@ -53,6 +54,7 @@ const CustomDateRangePicker = Vue.extend({
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const Component = Vue.extend({
|
||||
props: {
|
||||
timePicker: {
|
||||
@@ -154,6 +156,7 @@ const Component = Vue.extend({
|
||||
});
|
||||
|
||||
// Assertion corrects HTMLElement type from package
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const WrappedElement = wrap(
|
||||
Vue,
|
||||
Component
|
||||
|
@@ -24,7 +24,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public deviceId?: string;
|
||||
@property({ attribute: false }) public deviceId?: string;
|
||||
|
||||
@property({ type: Object }) public value?: T;
|
||||
|
||||
|
@@ -75,7 +75,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ type: Array }) public createDomains?: string[];
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
|
@@ -13,7 +13,7 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
class HaEntityAttributePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entityId?: string;
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
/**
|
||||
* List of attributes to be hidden.
|
||||
@@ -23,6 +23,7 @@ class HaEntityAttributePicker extends LitElement {
|
||||
@property({ type: Array, attribute: "hide-attributes" })
|
||||
public hideAttributes?: string[];
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
@@ -34,6 +34,7 @@ const CREATE_ID = "___create-new-entity___";
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -49,7 +50,7 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Array }) public createDomains?: string[];
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
@@ -102,7 +103,8 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ type: Boolean }) public hideClearIcon = false;
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@property({ attribute: "item-label-path" }) public itemLabelPath =
|
||||
"friendly_name";
|
||||
|
@@ -79,6 +79,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
@@ -14,12 +14,13 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
class HaEntityStatePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entityId?: string;
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
@property() public attribute?: string;
|
||||
|
||||
@property({ attribute: false }) public extraOptions?: any[];
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
@@ -55,7 +55,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
|
||||
@property() public image?: string;
|
||||
|
||||
@property({ type: Boolean }) public showName = false;
|
||||
@property({ attribute: "show-name", type: Boolean }) public showName = false;
|
||||
|
||||
@state() private _timerTimeRemaining?: number;
|
||||
|
||||
@@ -66,13 +66,13 @@ export class HaStateLabelBadge extends LitElement {
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._connected = true;
|
||||
this.startInterval(this.state);
|
||||
this._startInterval(this.state);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._connected = false;
|
||||
this.clearInterval();
|
||||
this._clearInterval();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -151,7 +151,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (this._connected && changedProperties.has("state")) {
|
||||
this.startInterval(this.state);
|
||||
this._startInterval(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,28 +237,28 @@ export class HaStateLabelBadge extends LitElement {
|
||||
return entityState.attributes.unit_of_measurement || null;
|
||||
}
|
||||
|
||||
private clearInterval() {
|
||||
private _clearInterval() {
|
||||
if (this._updateRemaining) {
|
||||
clearInterval(this._updateRemaining);
|
||||
this._updateRemaining = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private startInterval(stateObj) {
|
||||
this.clearInterval();
|
||||
private _startInterval(stateObj) {
|
||||
this._clearInterval();
|
||||
if (stateObj && computeStateDomain(stateObj) === "timer") {
|
||||
this.calculateTimerRemaining(stateObj);
|
||||
this._calculateTimerRemaining(stateObj);
|
||||
|
||||
if (stateObj.state === "active") {
|
||||
this._updateRemaining = window.setInterval(
|
||||
() => this.calculateTimerRemaining(this.state),
|
||||
() => this._calculateTimerRemaining(this.state),
|
||||
1000
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private calculateTimerRemaining(stateObj) {
|
||||
private _calculateTimerRemaining(stateObj) {
|
||||
this._timerTimeRemaining = timerTimeRemaining(stateObj);
|
||||
}
|
||||
|
||||
|
@@ -39,7 +39,8 @@ export class HaStatisticPicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||
public allowCustomEntity;
|
||||
|
||||
@property({ type: Array }) public statisticIds?: StatisticsMetaData[];
|
||||
@property({ attribute: false, type: Array })
|
||||
public statisticIds?: StatisticsMetaData[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@@ -84,7 +85,8 @@ export class HaStatisticPicker extends LitElement {
|
||||
@property({ type: Array, attribute: "exclude-statistics" })
|
||||
public excludeStatistics?: string[];
|
||||
|
||||
@property() public helpMissingEntityUrl = "/more-info/statistics/";
|
||||
@property({ attribute: false }) public helpMissingEntityUrl =
|
||||
"/more-info/statistics/";
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
|
@@ -12,7 +12,7 @@ class HaStatisticsPicker extends LitElement {
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property({ type: Array }) public statisticIds?: string[];
|
||||
@property({ attribute: false, type: Array }) public statisticIds?: string[];
|
||||
|
||||
@property({ attribute: "statistic-types" })
|
||||
public statisticTypes?: "mean" | "sum";
|
||||
|
@@ -22,9 +22,9 @@ export class StateBadge extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property() public overrideIcon?: string;
|
||||
@property({ attribute: false }) public overrideIcon?: string;
|
||||
|
||||
@property() public overrideImage?: string;
|
||||
@property({ attribute: false }) public overrideImage?: string;
|
||||
|
||||
// Cannot be a boolean attribute because undefined is treated different than
|
||||
// false. When it is undefined, state is still colored for light entities.
|
||||
|
@@ -14,7 +14,7 @@ class StateInfo extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property({ type: Boolean }) public inDialog = false;
|
||||
@property({ attribute: "in-dialog", type: Boolean }) public inDialog = false;
|
||||
|
||||
@property() public color?: string;
|
||||
|
||||
@@ -32,7 +32,7 @@ class StateInfo extends LitElement {
|
||||
.color=${this.color}
|
||||
></state-badge>
|
||||
<div class="info">
|
||||
<div class="name" .title=${name} .inDialog=${this.inDialog}>
|
||||
<div class="name ${this.inDialog ? "in-dialog" : ""}" .title=${name}>
|
||||
${name}
|
||||
</div>
|
||||
${this.inDialog
|
||||
@@ -108,7 +108,7 @@ class StateInfo extends LitElement {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.name[inDialog],
|
||||
.name.in-dialog,
|
||||
:host([secondary-line]) .name {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ declare global {
|
||||
|
||||
@customElement("ha-alert")
|
||||
class HaAlert extends LitElement {
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property() public title = "";
|
||||
|
||||
@property({ attribute: "alert-type" }) public alertType:
|
||||
@@ -63,7 +64,7 @@ class HaAlert extends LitElement {
|
||||
<slot name="action">
|
||||
${this.dismissable
|
||||
? html`<ha-icon-button
|
||||
@click=${this._dismiss_clicked}
|
||||
@click=${this._dismissClicked}
|
||||
label="Dismiss alert"
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
@@ -75,7 +76,7 @@ class HaAlert extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _dismiss_clicked() {
|
||||
private _dismissClicked() {
|
||||
fireEvent(this, "alert-dismissed-clicked");
|
||||
}
|
||||
|
||||
|
@@ -183,7 +183,7 @@ export class HaAnsiToHtml extends LitElement {
|
||||
|
||||
/* eslint-disable no-cond-assign */
|
||||
let match;
|
||||
// eslint-disable-next-line
|
||||
|
||||
while ((match = re.exec(line)) !== null) {
|
||||
const j = match!.index;
|
||||
const substring = line.substring(i, j);
|
||||
|
639
src/components/ha-assist-chat.ts
Normal file
639
src/components/ha-assist-chat.ts
Normal file
@@ -0,0 +1,639 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, LitElement, html, nothing } from "lit";
|
||||
import { mdiAlertCircle, mdiMicrophone, mdiSend } from "@mdi/js";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import {
|
||||
runAssistPipeline,
|
||||
type AssistPipeline,
|
||||
} from "../data/assist_pipeline";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { ConversationEntityFeature } from "../data/conversation";
|
||||
import { AudioRecorder } from "../util/audio-recorder";
|
||||
import "./ha-alert";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
|
||||
interface AssistMessage {
|
||||
who: string;
|
||||
text?: string | TemplateResult;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
@customElement("ha-assist-chat")
|
||||
export class HaAssistChat extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public pipeline?: AssistPipeline;
|
||||
|
||||
@property({ type: Boolean, attribute: false })
|
||||
public startListening?: boolean;
|
||||
|
||||
@query("#message-input") private _messageInput!: HaTextField;
|
||||
|
||||
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
|
||||
|
||||
@state() private _conversation: AssistMessage[] = [];
|
||||
|
||||
@state() private _showSendButton = false;
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
private _conversationId: string | null = null;
|
||||
|
||||
private _audioRecorder?: AudioRecorder;
|
||||
|
||||
private _audioBuffer?: Int16Array[];
|
||||
|
||||
private _audio?: HTMLAudioElement;
|
||||
|
||||
private _stt_binary_handler_id?: number | null;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (!this.hasUpdated || changedProperties.has("pipeline")) {
|
||||
this._conversation = [
|
||||
{
|
||||
who: "hass",
|
||||
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (
|
||||
this.startListening &&
|
||||
this.pipeline &&
|
||||
this.pipeline.stt_engine &&
|
||||
AudioRecorder.isSupported
|
||||
) {
|
||||
this._toggleListening();
|
||||
}
|
||||
setTimeout(() => this._messageInput.focus(), 0);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("_conversation")) {
|
||||
this._scrollMessagesBottom();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._audioRecorder?.close();
|
||||
this._audioRecorder = undefined;
|
||||
this._audio?.pause();
|
||||
this._conversation = [];
|
||||
this._conversationId = null;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const controlHA = !this.pipeline
|
||||
? false
|
||||
: this.pipeline.prefer_local_intents ||
|
||||
(this.hass.states[this.pipeline.conversation_engine]
|
||||
? supportsFeature(
|
||||
this.hass.states[this.pipeline.conversation_engine],
|
||||
ConversationEntityFeature.CONTROL
|
||||
)
|
||||
: true);
|
||||
const supportsMicrophone = AudioRecorder.isSupported;
|
||||
const supportsSTT = this.pipeline?.stt_engine;
|
||||
|
||||
return html`
|
||||
${controlHA
|
||||
? nothing
|
||||
: html`
|
||||
<ha-alert>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice_command.conversation_no_control"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
<div class="messages">
|
||||
<div class="messages-container" id="scroll-container">
|
||||
${this._conversation!.map(
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
(message) => html`
|
||||
<div class="message ${classMap({ error: !!message.error, [message.who]: true })}">${message.text}</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input" slot="primaryAction">
|
||||
<ha-textfield
|
||||
id="message-input"
|
||||
@keyup=${this._handleKeyUp}
|
||||
@input=${this._handleInput}
|
||||
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
|
||||
.iconTrailing=${true}
|
||||
>
|
||||
<div slot="trailingIcon">
|
||||
${this._showSendButton || !supportsSTT
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="listening-icon"
|
||||
.path=${mdiSend}
|
||||
@click=${this._handleSendMessage}
|
||||
.disabled=${this._processing}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.voice_command.send_text"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
${this._audioRecorder?.active
|
||||
? html`
|
||||
<div class="bouncer">
|
||||
<div class="double-bounce1"></div>
|
||||
<div class="double-bounce2"></div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="listening-icon">
|
||||
<ha-icon-button
|
||||
.path=${mdiMicrophone}
|
||||
@click=${this._handleListeningButton}
|
||||
.disabled=${this._processing}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.voice_command.start_listening"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${!supportsMicrophone
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${mdiAlertCircle}
|
||||
class="unsupported"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-textfield>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _scrollMessagesBottom() {
|
||||
const scrollContainer = this._scrollContainer;
|
||||
if (!scrollContainer) {
|
||||
return;
|
||||
}
|
||||
scrollContainer.scrollTo(0, scrollContainer.scrollHeight);
|
||||
}
|
||||
|
||||
private _handleKeyUp(ev: KeyboardEvent) {
|
||||
const input = ev.target as HaTextField;
|
||||
if (!this._processing && ev.key === "Enter" && input.value) {
|
||||
this._processText(input.value);
|
||||
input.value = "";
|
||||
this._showSendButton = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInput(ev: InputEvent) {
|
||||
const value = (ev.target as HaTextField).value;
|
||||
if (value && !this._showSendButton) {
|
||||
this._showSendButton = true;
|
||||
} else if (!value && this._showSendButton) {
|
||||
this._showSendButton = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSendMessage() {
|
||||
if (this._messageInput.value) {
|
||||
this._processText(this._messageInput.value.trim());
|
||||
this._messageInput.value = "";
|
||||
this._showSendButton = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleListeningButton(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this._toggleListening();
|
||||
}
|
||||
|
||||
private async _toggleListening() {
|
||||
const supportsMicrophone = AudioRecorder.isSupported;
|
||||
if (!supportsMicrophone) {
|
||||
this._showNotSupportedMessage();
|
||||
return;
|
||||
}
|
||||
if (!this._audioRecorder?.active) {
|
||||
this._startListening();
|
||||
} else {
|
||||
this._stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
private _addMessage(message: AssistMessage) {
|
||||
this._conversation = [...this._conversation!, message];
|
||||
}
|
||||
|
||||
private async _showNotSupportedMessage() {
|
||||
this._addMessage({
|
||||
who: "hass",
|
||||
text:
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
html`${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_browser"
|
||||
)}
|
||||
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation",
|
||||
{
|
||||
documentation_link: html`<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/configuration/securing/#remote-access"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
|
||||
)}</a>`,
|
||||
}
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _startListening() {
|
||||
this._processing = true;
|
||||
this._audio?.pause();
|
||||
if (!this._audioRecorder) {
|
||||
this._audioRecorder = new AudioRecorder((audio) => {
|
||||
if (this._audioBuffer) {
|
||||
this._audioBuffer.push(audio);
|
||||
} else {
|
||||
this._sendAudioChunk(audio);
|
||||
}
|
||||
});
|
||||
}
|
||||
this._stt_binary_handler_id = undefined;
|
||||
this._audioBuffer = [];
|
||||
const userMessage: AssistMessage = {
|
||||
who: "user",
|
||||
text: "…",
|
||||
};
|
||||
await this._audioRecorder.start();
|
||||
|
||||
this._addMessage(userMessage);
|
||||
this.requestUpdate("_audioRecorder");
|
||||
|
||||
const hassMessage: AssistMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
};
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
(event) => {
|
||||
if (event.type === "run-start") {
|
||||
this._stt_binary_handler_id =
|
||||
event.data.runner_data.stt_binary_handler_id;
|
||||
}
|
||||
|
||||
// When we start STT stage, the WS has a binary handler
|
||||
if (event.type === "stt-start" && this._audioBuffer) {
|
||||
// Send the buffer over the WS to the STT engine.
|
||||
for (const buffer of this._audioBuffer) {
|
||||
this._sendAudioChunk(buffer);
|
||||
}
|
||||
this._audioBuffer = undefined;
|
||||
}
|
||||
|
||||
// Stop recording if the server is done with STT stage
|
||||
if (event.type === "stt-end") {
|
||||
this._stt_binary_handler_id = undefined;
|
||||
this._stopListening();
|
||||
userMessage.text = event.data.stt_output.text;
|
||||
this.requestUpdate("_conversation");
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
if (plain) {
|
||||
hassMessage.text = plain.speech;
|
||||
}
|
||||
this.requestUpdate("_conversation");
|
||||
}
|
||||
|
||||
if (event.type === "tts-end") {
|
||||
const url = event.data.tts_output.url;
|
||||
this._audio = new Audio(url);
|
||||
this._audio.play();
|
||||
this._audio.addEventListener("ended", this._unloadAudio);
|
||||
this._audio.addEventListener("pause", this._unloadAudio);
|
||||
this._audio.addEventListener("canplaythrough", this._playAudio);
|
||||
this._audio.addEventListener("error", this._audioError);
|
||||
}
|
||||
|
||||
if (event.type === "run-end") {
|
||||
this._stt_binary_handler_id = undefined;
|
||||
unsub();
|
||||
}
|
||||
|
||||
if (event.type === "error") {
|
||||
this._stt_binary_handler_id = undefined;
|
||||
if (userMessage.text === "…") {
|
||||
userMessage.text = event.data.message;
|
||||
userMessage.error = true;
|
||||
} else {
|
||||
hassMessage.text = event.data.message;
|
||||
hassMessage.error = true;
|
||||
}
|
||||
this._stopListening();
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
},
|
||||
{
|
||||
start_stage: "stt",
|
||||
end_stage: this.pipeline?.tts_engine ? "tts" : "intent",
|
||||
input: { sample_rate: this._audioRecorder.sampleRate! },
|
||||
pipeline: this.pipeline?.id,
|
||||
conversation_id: this._conversationId,
|
||||
}
|
||||
);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
title: "Error starting pipeline",
|
||||
text: err.message || err,
|
||||
});
|
||||
this._stopListening();
|
||||
} finally {
|
||||
this._processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _stopListening() {
|
||||
this._audioRecorder?.stop();
|
||||
this.requestUpdate("_audioRecorder");
|
||||
// We're currently STTing, so finish audio
|
||||
if (this._stt_binary_handler_id) {
|
||||
if (this._audioBuffer) {
|
||||
for (const chunk of this._audioBuffer) {
|
||||
this._sendAudioChunk(chunk);
|
||||
}
|
||||
}
|
||||
// Send empty message to indicate we're done streaming.
|
||||
this._sendAudioChunk(new Int16Array());
|
||||
this._stt_binary_handler_id = undefined;
|
||||
}
|
||||
this._audioBuffer = undefined;
|
||||
}
|
||||
|
||||
private _sendAudioChunk(chunk: Int16Array) {
|
||||
this.hass.connection.socket!.binaryType = "arraybuffer";
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this._stt_binary_handler_id == undefined) {
|
||||
return;
|
||||
}
|
||||
// Turn into 8 bit so we can prefix our handler ID.
|
||||
const data = new Uint8Array(1 + chunk.length * 2);
|
||||
data[0] = this._stt_binary_handler_id;
|
||||
data.set(new Uint8Array(chunk.buffer), 1);
|
||||
|
||||
this.hass.connection.socket!.send(data);
|
||||
}
|
||||
|
||||
private _playAudio = () => {
|
||||
this._audio?.play();
|
||||
};
|
||||
|
||||
private _audioError = () => {
|
||||
showAlertDialog(this, { title: "Error playing audio." });
|
||||
this._audio?.removeAttribute("src");
|
||||
};
|
||||
|
||||
private _unloadAudio = () => {
|
||||
this._audio?.removeAttribute("src");
|
||||
this._audio = undefined;
|
||||
};
|
||||
|
||||
private async _processText(text: string) {
|
||||
this._processing = true;
|
||||
this._audio?.pause();
|
||||
this._addMessage({ who: "user", text });
|
||||
const message: AssistMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
};
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
this._addMessage(message);
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
(event) => {
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
if (plain) {
|
||||
message.text = plain.speech;
|
||||
}
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
if (event.type === "error") {
|
||||
message.text = event.data.message;
|
||||
message.error = true;
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
},
|
||||
{
|
||||
start_stage: "intent",
|
||||
input: { text },
|
||||
end_stage: "intent",
|
||||
pipeline: this.pipeline?.id,
|
||||
conversation_id: this._conversationId,
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
message.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||
message.error = true;
|
||||
this.requestUpdate("_conversation");
|
||||
} finally {
|
||||
this._processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: var(--ha-assist-chat-min-height, 415px);
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin: 0 24px 16px;
|
||||
}
|
||||
.messages {
|
||||
flex: 1;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.messages-container {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
.message {
|
||||
white-space: pre-line;
|
||||
font-size: 18px;
|
||||
clear: both;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.message {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.message p {
|
||||
margin: 0;
|
||||
}
|
||||
.message p:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
float: var(--float-end);
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.hass {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
float: var(--float-start);
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(--secondary-background-color);
|
||||
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.user a {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.message.hass a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background-color: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: absolute;
|
||||
}
|
||||
.double-bounce1,
|
||||
.double-bounce2 {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.2;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-animation: sk-bounce 2s infinite ease-in-out;
|
||||
animation: sk-bounce 2s infinite ease-in-out;
|
||||
}
|
||||
.double-bounce2 {
|
||||
-webkit-animation-delay: -1s;
|
||||
animation-delay: -1s;
|
||||
}
|
||||
@-webkit-keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
-webkit-transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.listening-icon {
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: -24px;
|
||||
margin-inline-end: -24px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
transform: scaleX(var(--scale-direction));
|
||||
}
|
||||
|
||||
.listening-icon[active] {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.unsupported {
|
||||
color: var(--error-color);
|
||||
position: absolute;
|
||||
--mdc-icon-size: 16px;
|
||||
right: 5px;
|
||||
inset-inline-end: 5px;
|
||||
inset-inline-start: initial;
|
||||
top: 0px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-assist-chat": HaAssistChat;
|
||||
}
|
||||
}
|
@@ -26,7 +26,7 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean }) public includeLastUsed = false;
|
||||
@property({ attribute: false }) public includeLastUsed = false;
|
||||
|
||||
@state() _pipelines?: AssistPipeline[];
|
||||
|
||||
|
@@ -15,7 +15,7 @@ export class HaAttributeIcon extends LitElement {
|
||||
|
||||
@property() public attribute?: string;
|
||||
|
||||
@property() public attributeValue?: string;
|
||||
@property({ attribute: false }) public attributeValue?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
|
@@ -20,7 +20,7 @@ class HaAttributes extends LitElement {
|
||||
@state() private _expanded = false;
|
||||
|
||||
private get _filteredAttributes() {
|
||||
return this.computeDisplayAttributes(
|
||||
return this._computeDisplayAttributes(
|
||||
STATE_ATTRIBUTES.concat(
|
||||
this.extraFilters ? this.extraFilters.split(",") : []
|
||||
)
|
||||
@@ -53,7 +53,7 @@ class HaAttributes extends LitElement {
|
||||
"ui.components.attributes.expansion_header"
|
||||
)}
|
||||
outlined
|
||||
@expanded-will-change=${this.expandedChanged}
|
||||
@expanded-will-change=${this._expandedChanged}
|
||||
>
|
||||
<div class="attribute-container">
|
||||
${this._expanded
|
||||
@@ -128,7 +128,7 @@ class HaAttributes extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
private computeDisplayAttributes(filtersArray: string[]): string[] {
|
||||
private _computeDisplayAttributes(filtersArray: string[]): string[] {
|
||||
if (!this.stateObj) {
|
||||
return [];
|
||||
}
|
||||
@@ -137,7 +137,7 @@ class HaAttributes extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private expandedChanged(ev) {
|
||||
private _expandedChanged(ev) {
|
||||
this._expanded = ev.detail.expanded;
|
||||
}
|
||||
}
|
||||
|
@@ -98,7 +98,6 @@ export class HaBadge extends LitElement {
|
||||
align-items: flex-start;
|
||||
padding-inline-start: initial;
|
||||
text-align: center;
|
||||
font-family: Roboto;
|
||||
}
|
||||
.label {
|
||||
font-size: 10px;
|
||||
|
@@ -36,7 +36,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
/**
|
||||
* auto validate time inputs
|
||||
*/
|
||||
@property({ type: Boolean }) autoValidate = false;
|
||||
@property({ attribute: "auto-validate", type: Boolean }) autoValidate = false;
|
||||
|
||||
/**
|
||||
* determines if inputs are required
|
||||
@@ -81,52 +81,56 @@ export class HaBaseTimeInput extends LitElement {
|
||||
/**
|
||||
* Label for the day input
|
||||
*/
|
||||
@property() dayLabel = "";
|
||||
@property({ attribute: false }) dayLabel = "";
|
||||
|
||||
/**
|
||||
* Label for the hour input
|
||||
*/
|
||||
@property() hourLabel = "";
|
||||
@property({ attribute: false }) hourLabel = "";
|
||||
|
||||
/**
|
||||
* Label for the min input
|
||||
*/
|
||||
@property() minLabel = "";
|
||||
@property({ attribute: false }) minLabel = "";
|
||||
|
||||
/**
|
||||
* Label for the sec input
|
||||
*/
|
||||
@property() secLabel = "";
|
||||
@property({ attribute: false }) secLabel = "";
|
||||
|
||||
/**
|
||||
* Label for the milli sec input
|
||||
*/
|
||||
@property() millisecLabel = "";
|
||||
@property({ attribute: false }) millisecLabel = "";
|
||||
|
||||
/**
|
||||
* show the sec field
|
||||
*/
|
||||
@property({ type: Boolean }) enableSecond = false;
|
||||
@property({ attribute: "enable-second", type: Boolean })
|
||||
public enableSecond = false;
|
||||
|
||||
/**
|
||||
* show the milli sec field
|
||||
*/
|
||||
@property({ type: Boolean }) enableMillisecond = false;
|
||||
@property({ attribute: "enable-millisecond", type: Boolean })
|
||||
public enableMillisecond = false;
|
||||
|
||||
/**
|
||||
* show the day field
|
||||
*/
|
||||
@property({ type: Boolean }) enableDay = false;
|
||||
@property({ attribute: "enable-day", type: Boolean })
|
||||
public enableDay = false;
|
||||
|
||||
/**
|
||||
* limit hours input
|
||||
*/
|
||||
@property({ type: Boolean }) noHoursLimit = false;
|
||||
@property({ attribute: "no-hours-limit", type: Boolean })
|
||||
public noHoursLimit = false;
|
||||
|
||||
/**
|
||||
* AM or PM
|
||||
*/
|
||||
@property() amPm: "AM" | "PM" = "AM";
|
||||
@property({ attribute: false }) amPm: "AM" | "PM" = "AM";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
|
||||
|
||||
@@ -134,7 +138,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
return html`
|
||||
${this.label
|
||||
? html`<label>${this.label}${this.required ? " *" : ""}</label>`
|
||||
: ""}
|
||||
: nothing}
|
||||
<div class="time-input-wrap-wrap">
|
||||
<div class="time-input-wrap">
|
||||
${this.enableDay
|
||||
@@ -158,7 +162,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
|
||||
<ha-textfield
|
||||
id="hour"
|
||||
@@ -221,7 +225,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
class=${this.enableMillisecond ? "has-suffix" : ""}
|
||||
>
|
||||
</ha-textfield>`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this.enableMillisecond
|
||||
? html`<ha-textfield
|
||||
id="millisec"
|
||||
@@ -240,7 +244,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
</ha-textfield>`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this.clearable && !this.required && !this.disabled
|
||||
? html`<ha-icon-button
|
||||
label="clear"
|
||||
@@ -251,7 +255,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
</div>
|
||||
|
||||
${this.format === 24
|
||||
? ""
|
||||
? nothing
|
||||
: html`<ha-select
|
||||
.required=${this.required}
|
||||
.value=${this.amPm}
|
||||
@@ -265,10 +269,10 @@ export class HaBaseTimeInput extends LitElement {
|
||||
<mwc-list-item value="AM">AM</mwc-list-item>
|
||||
<mwc-list-item value="PM">PM</mwc-list-item>
|
||||
</ha-select>`}
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: ""}
|
||||
</div>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -363,17 +367,17 @@ export class HaBaseTimeInput extends LitElement {
|
||||
width: 85px;
|
||||
}
|
||||
:host([clearable]) .mdc-select__anchor {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 12px);
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 12px);
|
||||
}
|
||||
ha-icon-button {
|
||||
position: relative
|
||||
position: relative;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color:var(--mdc-text-field-fill-color, whitesmoke);
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
@@ -398,6 +402,10 @@ export class HaBaseTimeInput extends LitElement {
|
||||
padding-inline-start: 4px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
ha-input-helper-text {
|
||||
padding-top: 8px;
|
||||
line-height: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -14,7 +14,7 @@ export class HaButtonMenu extends LitElement {
|
||||
|
||||
@property() public corner: Corner = "BOTTOM_START";
|
||||
|
||||
@property() public menuCorner: MenuCorner = "START";
|
||||
@property({ attribute: false }) public menuCorner: MenuCorner = "START";
|
||||
|
||||
@property({ type: Number }) public x: number | null = null;
|
||||
|
||||
|
@@ -14,7 +14,8 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
|
||||
@property() public active?: string;
|
||||
|
||||
@property({ type: Boolean }) public fullWidth = false;
|
||||
@property({ attribute: "full-width", type: Boolean })
|
||||
public fullWidth = false;
|
||||
|
||||
@property({ type: Boolean }) public dense = false;
|
||||
|
||||
|
@@ -7,9 +7,11 @@ import { HaListItem } from "./ha-list-item";
|
||||
export class HaClickableListItem extends HaListItem {
|
||||
@property() public href?: string;
|
||||
|
||||
@property({ type: Boolean }) public disableHref = false;
|
||||
@property({ attribute: "disable-href", type: Boolean })
|
||||
public disableHref = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public openNewTab = false;
|
||||
@property({ attribute: "open-new-tab", type: Boolean, reflect: true })
|
||||
public openNewTab = false;
|
||||
|
||||
@query("a") private _anchor!: HTMLAnchorElement;
|
||||
|
||||
@@ -18,7 +20,7 @@ export class HaClickableListItem extends HaListItem {
|
||||
const href = this.href || "";
|
||||
|
||||
return html`${this.disableHref
|
||||
? html`<a>${r}</a>`
|
||||
? html`<a href="#" class="disabled">${r}</a>`
|
||||
: html`<a target=${this.openNewTab ? "_blank" : ""} href=${href}
|
||||
>${r}</a
|
||||
>`}`;
|
||||
@@ -44,6 +46,9 @@ export class HaClickableListItem extends HaListItem {
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -44,9 +44,10 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: Boolean }) public readOnly = false;
|
||||
@property({ attribute: "read-only", type: Boolean }) public readOnly = false;
|
||||
|
||||
@property({ type: Boolean }) public linewrap = false;
|
||||
|
||||
|
@@ -82,7 +82,7 @@ export class HaColorPicker extends LitElement {
|
||||
`
|
||||
: value === "state"
|
||||
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
|
||||
: this.renderColorCircle(value || "grey")}
|
||||
: this._renderColorCircle(value || "grey")}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
@@ -123,7 +123,7 @@ export class HaColorPicker extends LitElement {
|
||||
${this.defaultColor === color
|
||||
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
||||
: nothing}
|
||||
<span slot="graphic">${this.renderColorCircle(color)}</span>
|
||||
<span slot="graphic">${this._renderColorCircle(color)}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
@@ -131,7 +131,7 @@ export class HaColorPicker extends LitElement {
|
||||
? html`
|
||||
<ha-list-item .value=${value} graphic="icon">
|
||||
${value}
|
||||
<span slot="graphic">${this.renderColorCircle(value)}</span>
|
||||
<span slot="graphic">${this._renderColorCircle(value)}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
@@ -139,7 +139,7 @@ export class HaColorPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderColorCircle(color: string) {
|
||||
private _renderColorCircle(color: string) {
|
||||
return html`
|
||||
<span
|
||||
class="circle-color"
|
||||
|
@@ -71,7 +71,7 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public validationMessage?: string;
|
||||
@property({ attribute: false }) public validationMessage?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
|
@@ -435,7 +435,7 @@ export class HaControlCircularSlider extends LitElement {
|
||||
this._activeSlider = undefined;
|
||||
}
|
||||
|
||||
_handleKeyUp(e: KeyboardEvent) {
|
||||
private _handleKeyUp(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
this._activeSlider = (e.currentTarget as any).id as ActiveSlider;
|
||||
e.preventDefault();
|
||||
|
@@ -52,7 +52,7 @@ export class HaControlNumberButton extends LitElement {
|
||||
},
|
||||
});
|
||||
|
||||
private boundedValue(value: number) {
|
||||
private _boundedValue(value: number) {
|
||||
const clamped = conditionalClamp(value, this.min, this.max);
|
||||
return Math.round(clamped / this._step) * this._step;
|
||||
}
|
||||
@@ -86,14 +86,14 @@ export class HaControlNumberButton extends LitElement {
|
||||
}
|
||||
|
||||
private _increment() {
|
||||
this.value = this.boundedValue(this._value + this._step);
|
||||
this.value = this._boundedValue(this._value + this._step);
|
||||
}
|
||||
|
||||
private _decrement() {
|
||||
this.value = this.boundedValue(this._value - this._step);
|
||||
this.value = this._boundedValue(this._value - this._step);
|
||||
}
|
||||
|
||||
_handleKeyDown(e: KeyboardEvent) {
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
if (this.disabled) return;
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
@@ -107,10 +107,10 @@ export class HaControlNumberButton extends LitElement {
|
||||
this._decrement();
|
||||
break;
|
||||
case "PageUp":
|
||||
this.value = this.boundedValue(this._value + this._tenPercentStep);
|
||||
this.value = this._boundedValue(this._value + this._tenPercentStep);
|
||||
break;
|
||||
case "PageDown":
|
||||
this.value = this.boundedValue(this._value - this._tenPercentStep);
|
||||
this.value = this._boundedValue(this._value - this._tenPercentStep);
|
||||
break;
|
||||
case "Home":
|
||||
if (this.min != null) {
|
||||
|
@@ -57,12 +57,13 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
aria-labelledby=${ifDefined(labelledby)}
|
||||
aria-label=${ifDefined(labelAttribute)}
|
||||
aria-required=${this.required}
|
||||
aria-controls="listbox"
|
||||
@focus=${this.onFocus}
|
||||
@blur=${this.onBlur}
|
||||
@click=${this.onClick}
|
||||
@keydown=${this.onKeydown}
|
||||
>
|
||||
${this.renderIcon()}
|
||||
${this._renderIcon()}
|
||||
<div class="content">
|
||||
${this.hideLabel
|
||||
? nothing
|
||||
@@ -71,7 +72,7 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
? html`<p class="value">${this.selectedText}</p>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.renderArrow()}
|
||||
${this._renderArrow()}
|
||||
<ha-ripple .disabled=${this.disabled}></ha-ripple>
|
||||
</div>
|
||||
${this.renderMenu()}
|
||||
@@ -79,7 +80,7 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderArrow() {
|
||||
private _renderArrow() {
|
||||
if (!this.showArrow) return nothing;
|
||||
|
||||
return html`
|
||||
@@ -89,7 +90,7 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderIcon() {
|
||||
private _renderIcon() {
|
||||
const index = this.mdcFoundation?.getSelectedIndex();
|
||||
const items = this.menuElement?.items ?? [];
|
||||
const item = index != null ? items[index] : undefined;
|
||||
|
@@ -215,12 +215,12 @@ export class HaControlSlider extends LitElement {
|
||||
return Math.max(this.step, (this.max - this.min) / 10);
|
||||
}
|
||||
|
||||
_showTooltip() {
|
||||
private _showTooltip() {
|
||||
if (this._tooltipTimeout != null) window.clearTimeout(this._tooltipTimeout);
|
||||
this.tooltipVisible = true;
|
||||
}
|
||||
|
||||
_hideTooltip(delay?: number) {
|
||||
private _hideTooltip(delay?: number) {
|
||||
if (!delay) {
|
||||
this.tooltipVisible = false;
|
||||
return;
|
||||
@@ -230,7 +230,7 @@ export class HaControlSlider extends LitElement {
|
||||
}, delay);
|
||||
}
|
||||
|
||||
_handleKeyDown(e: KeyboardEvent) {
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
switch (e.code) {
|
||||
@@ -265,7 +265,7 @@ export class HaControlSlider extends LitElement {
|
||||
|
||||
private _tooltipTimeout?: number;
|
||||
|
||||
_handleKeyUp(e: KeyboardEvent) {
|
||||
private _handleKeyUp(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
this._hideTooltip(500);
|
||||
|
@@ -22,10 +22,10 @@ export class HaControlSwitch extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) public checked = false;
|
||||
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided on icon slot to pass an <ha-icon slot="icon-on"> in)
|
||||
@property({ type: String }) pathOn?: string;
|
||||
@property({ attribute: false, type: String }) pathOn?: string;
|
||||
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
|
||||
@property({ type: String }) pathOff?: string;
|
||||
@property({ attribute: false, type: String }) pathOff?: string;
|
||||
|
||||
@property({ attribute: "touch-action" })
|
||||
public touchAction?: string;
|
||||
|
@@ -277,7 +277,7 @@ export class HaCountryPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public noSort = false;
|
||||
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
|
||||
|
||||
private _getOptions = memoizeOne(
|
||||
(language?: string, countries?: string[]) => {
|
||||
|
@@ -13,7 +13,7 @@ import "./ha-textfield";
|
||||
|
||||
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
|
||||
|
||||
export interface datePickerDialogParams {
|
||||
export interface DatePickerDialogParams {
|
||||
value?: string;
|
||||
min?: string;
|
||||
max?: string;
|
||||
@@ -25,7 +25,7 @@ export interface datePickerDialogParams {
|
||||
|
||||
const showDatePickerDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: datePickerDialogParams
|
||||
dialogParams: DatePickerDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-dialog-date-picker",
|
||||
@@ -51,7 +51,7 @@ export class HaDateInput extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public canClear = false;
|
||||
@property({ attribute: "can-clear", type: Boolean }) public canClear = false;
|
||||
|
||||
render() {
|
||||
return html`<ha-textfield
|
||||
|
@@ -53,9 +53,11 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
@state() private _ranges?: DateRangePickerRanges;
|
||||
|
||||
@property({ type: Boolean }) public autoApply = false;
|
||||
@property({ attribute: "auto-apply", type: Boolean })
|
||||
public autoApply = false;
|
||||
|
||||
@property({ type: Boolean }) public timePicker = true;
|
||||
@property({ attribute: "time-picker", type: Boolean })
|
||||
public timePicker = true;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@@ -63,9 +65,14 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
@state() private _hour24format = false;
|
||||
|
||||
@property({ type: Boolean }) public extendedPresets = false;
|
||||
@property({ attribute: "extended-presets", type: Boolean })
|
||||
public extendedPresets = false;
|
||||
|
||||
@property() public openingDirection?: "right" | "left" | "center" | "inline";
|
||||
@property({ attribute: false }) public openingDirection?:
|
||||
| "right"
|
||||
| "left"
|
||||
| "center"
|
||||
| "inline";
|
||||
|
||||
@state() private _calcedOpeningDirection?:
|
||||
| "right"
|
||||
|
@@ -7,7 +7,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { haStyleDialog } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { datePickerDialogParams } from "./ha-date-input";
|
||||
import type { DatePickerDialogParams } from "./ha-date-input";
|
||||
import "./ha-dialog";
|
||||
|
||||
@customElement("ha-dialog-date-picker")
|
||||
@@ -20,11 +20,11 @@ export class HaDialogDatePicker extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@state() private _params?: datePickerDialogParams;
|
||||
@state() private _params?: DatePickerDialogParams;
|
||||
|
||||
@state() private _value?: string;
|
||||
|
||||
public async showDialog(params: datePickerDialogParams): Promise<void> {
|
||||
public async showDialog(params: DatePickerDialogParams): Promise<void> {
|
||||
// app-datepicker has a bug, that it removes its handlers when disconnected, but doesn't add them back when reconnected.
|
||||
// So we need to wait for the next render to make sure the element is removed and re-created so the handlers are added.
|
||||
await nextRender();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user