Compare commits

..

2 Commits

Author SHA1 Message Date
Paul Bottein
f3f2059634 Reset history when changing edit mode 2023-08-24 08:48:44 +02:00
Paul Bottein
497484d419 Add undo/redo action for dashboard edition 2023-08-23 21:31:25 +02:00
223 changed files with 5777 additions and 12758 deletions

View File

@@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
with:
ref: dev
@@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
with:
ref: master

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.8.1
with:
@@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.8.1
with:
@@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.8.1
with:
@@ -91,7 +91,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.8.1
with:

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
with:
ref: dev
@@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
with:
ref: master

View File

@@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.8.1

View File

@@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.8.1

View File

@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3.6.0
uses: actions/checkout@v3.5.3
- name: Upload Translations
run: |

File diff suppressed because one or more lines are too long

View File

@@ -8,4 +8,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.6.3.cjs
yarnPath: .yarn/releases/yarn-3.6.2.cjs

View File

@@ -1,14 +1,10 @@
import fs from "fs/promises";
import gulp from "gulp";
import path from "path";
import mapStream from "map-stream";
import transform from "gulp-json-transform";
import { LokaliseApi } from "@lokalise/node-api";
import JSZip from "jszip";
const inDir = "translations";
const inDirFrontend = `${inDir}/frontend`;
const inDirBackend = `${inDir}/backend`;
const inDirFrontend = "translations/frontend";
const inDirBackend = "translations/backend";
const srcMeta = "src/translations/translationMetadata.json";
const encoding = "utf8";
@@ -72,9 +68,8 @@ gulp.task("convert-backend-translations", function () {
});
gulp.task("check-translations-html", function () {
return gulp
.src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
.pipe(checkHtml());
// We exclude backend translations because they are not compliant with the HTML rule for now
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
});
gulp.task("check-all-files-exist", async function () {
@@ -94,83 +89,7 @@ gulp.task("check-all-files-exist", async function () {
await Promise.allSettled(writings);
});
const lokaliseProjects = {
backend: "130246255a974bd3b5e8a1.51616605",
frontend: "3420425759f6d6d241f598.13594006",
};
gulp.task("fetch-lokalise", async function () {
let apiKey;
try {
apiKey =
process.env.LOKALISE_TOKEN ||
(await fs.readFile(".lokalise_token", { encoding }));
} catch {
throw new Error(
"An Administrator Lokalise API token is required to download the latest set of translations. Place your token in a new file `.lokalise_token` in the repo root directory."
);
}
const lokaliseApi = new LokaliseApi({ apiKey });
const mkdirPromise = Promise.all([
fs.mkdir(inDirFrontend, { recursive: true }),
fs.mkdir(inDirBackend, { recursive: true }),
]);
await Promise.all(
Object.entries(lokaliseProjects).map(([project, projectId]) =>
lokaliseApi
.files()
.download(projectId, {
format: "json",
original_filenames: false,
replace_breaks: false,
json_unescaped_slashes: true,
export_empty_as: "skip",
})
.then((download) => fetch(download.bundle_url))
.then((response) => {
if (response.status === 200 || response.status === 0) {
return response.arrayBuffer();
}
throw new Error(response.statusText);
})
.then(JSZip.loadAsync)
.then(async (contents) => {
await mkdirPromise;
return Promise.all(
Object.keys(contents.files).map(async (filename) => {
const file = contents.file(filename);
if (!file) {
// no file, probably a directory
return Promise.resolve();
}
return file
.async("nodebuffer")
.then((content) =>
fs.writeFile(
path.join(
inDir,
project,
filename.split("/").splice(-1)[0]
),
content,
{ flag: "w", encoding }
)
);
})
);
})
)
);
});
gulp.task(
"download-translations",
gulp.series(
"fetch-lokalise",
"convert-backend-translations",
"check-translations-html",
"check-all-files-exist"
)
"check-downloaded-translations",
gulp.series("check-translations-html", "check-all-files-exist")
);

View File

@@ -1,3 +0,0 @@
---
title: Control Number Buttons
---

View File

@@ -1,100 +0,0 @@
import { LitElement, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-number-buttons";
import { repeat } from "lit/directives/repeat";
import { ifDefined } from "lit/directives/if-defined";
const buttons: {
id: string;
label: string;
min?: number;
max?: number;
step?: number;
class?: string;
}[] = [
{
id: "basic",
label: "Basic",
},
{
id: "min_max_step",
label: "With min/max and step",
min: 5,
max: 25,
step: 0.5,
},
{
id: "custom",
label: "Custom",
class: "custom",
},
];
@customElement("demo-components-ha-control-number-buttons")
export class DemoHarControlNumberButtons extends LitElement {
@state() value = 5;
private _valueChanged(ev) {
this.value = ev.detail.value;
}
protected render(): TemplateResult {
return html`
${repeat(buttons, (button) => {
const { id, label, ...config } = button;
return html`
<ha-card>
<div class="card-content">
<label id=${id}>${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-control-number-buttons
.value=${this.value}
.min=${config.min}
.max=${config.max}
.step=${config.step}
class=${ifDefined(config.class)}
@value-changed=${this._valueChanged}
.label=${label}
>
</ha-control-number-buttons>
</div>
</ha-card>
`;
})}
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
pre {
margin-top: 0;
margin-bottom: 8px;
}
p {
margin: 0;
}
label {
font-weight: 600;
}
.custom {
color: #2196f3;
--control-number-buttons-color: #2196f3;
--control-number-buttons-background-color: #2196f3;
--control-number-buttons-background-opacity: 0.1;
--control-number-buttons-thickness: 100px;
--control-number-buttons-border-radius: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-control-number-buttons": DemoHarControlNumberButtons;
}
}

View File

@@ -0,0 +1,3 @@
---
title: Temp Color Picker
---

View File

@@ -0,0 +1,117 @@
import "../../../../src/components/ha-temp-color-picker";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-slider";
@customElement("demo-components-ha-temp-color-picker")
export class DemoHaTempColorPicker extends LitElement {
@state()
min = 3000;
@state()
max = 7000;
@state()
value = 4000;
@state()
liveValue?: number;
private _minChanged(ev) {
this.min = Number(ev.target.value);
}
private _maxChanged(ev) {
this.max = Number(ev.target.value);
}
private _valueChanged(ev) {
this.value = Number(ev.target.value);
}
private _tempColorCursor(ev) {
this.liveValue = ev.detail.value;
}
private _tempColorChanged(ev) {
this.value = ev.detail.value;
}
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
<p class="value">${this.liveValue ?? this.value} K</p>
<ha-temp-color-picker
.min=${this.min}
.max=${this.max}
.value=${this.value}
@value-changed=${this._tempColorChanged}
@cursor-moved=${this._tempColorCursor}
></ha-temp-color-picker>
<p>Min temp : ${this.min} K</p>
<ha-slider
step="1"
pin
min="2000"
max="10000"
.value=${this.min}
@change=${this._minChanged}
>
</ha-slider>
<p>Max temp : ${this.max} K</p>
<ha-slider
step="1"
pin
min="2000"
max="10000"
.value=${this.max}
@change=${this._maxChanged}
>
</ha-slider>
<p>Value : ${this.value} K</p>
<ha-slider
step="1"
pin
min=${this.min}
max=${this.max}
.value=${this.value}
@change=${this._valueChanged}
>
</ha-slider>
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.card-content {
display: flex;
align-items: center;
flex-direction: column;
}
ha-temp-color-picker {
width: 400px;
}
.value {
font-size: 22px;
font-weight: bold;
margin: 0 0 12px 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-temp-color-picker": DemoHaTempColorPicker;
}
}

View File

@@ -31,8 +31,8 @@ export class HassioUploadBackup extends LitElement {
.icon=${mdiFolderUpload}
accept="application/x-tar"
label="Upload backup"
supports="Supports .TAR files"
@file-picked=${this._uploadFile}
auto-open-file-dialog
></ha-file-upload>
`;
}

View File

@@ -173,7 +173,6 @@ class HassioBackupDialog
private async _restoreClicked() {
const backupDetails = this._backupContent.backupDetails();
this._restoringBackup = true;
this._dialogParams?.onRestoring?.();
if (this._backupContent.backupType === "full") {
await this._fullRestoreClicked(backupDetails);
} else {
@@ -220,7 +219,7 @@ class HassioBackupDialog
this._error = error.body.message;
}
} else {
this._dialogParams?.onRestoring?.();
fireEvent(this, "restoring");
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
method: "POST",
body: JSON.stringify(backupDetails),
@@ -269,7 +268,7 @@ class HassioBackupDialog
}
);
} else {
this._dialogParams?.onRestoring?.();
fireEvent(this, "restoring");
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
method: "POST",
body: JSON.stringify(backupDetails),

View File

@@ -5,7 +5,6 @@ import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupDialogParams {
slug: string;
onDelete?: () => void;
onRestoring?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
localize?: LocalizeFunc;

View File

@@ -25,15 +25,15 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.22.11",
"@babel/runtime": "7.22.10",
"@braintree/sanitize-url": "6.0.4",
"@codemirror/autocomplete": "6.9.0",
"@codemirror/commands": "6.2.5",
"@codemirror/commands": "6.2.4",
"@codemirror/language": "6.9.0",
"@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.2",
"@codemirror/search": "6.5.1",
"@codemirror/state": "6.2.1",
"@codemirror/view": "6.17.1",
"@codemirror/view": "6.16.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.10.0",
"@formatjs/intl-displaynames": "6.5.0",
@@ -52,7 +52,7 @@
"@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.4.0",
"@lit-labs/motion": "1.0.4",
"@lit-labs/virtualizer": "2.0.7",
"@lit-labs/virtualizer": "2.0.5",
"@lrnwebcomponents/simple-tooltip": "7.0.16",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
@@ -79,9 +79,10 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.0-pre.16",
"@material/web": "=1.0.0-pre.15",
"@mdi/js": "7.2.96",
"@mdi/svg": "7.2.96",
"@polymer/app-layout": "3.1.0",
"@polymer/iron-flex-layout": "3.0.1",
"@polymer/iron-input": "3.0.1",
"@polymer/iron-resizable-behavior": "3.0.1",
@@ -93,8 +94,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.1.6",
"@vaadin/vaadin-themable-mixin": "24.1.6",
"@vaadin/combo-box": "24.1.5",
"@vaadin/vaadin-themable-mixin": "24.1.5",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -102,17 +103,17 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"chart.js": "4.3.3",
"chart.js": "3.3.2",
"comlink": "4.4.1",
"core-js": "3.32.1",
"cropperjs": "1.6.0",
"cropperjs": "1.5.13",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"fuse.js": "6.6.2",
"google-timezones-json": "1.2.0",
"hls.js": "1.4.12",
"hls.js": "1.4.10",
"home-assistant-js-websocket": "8.2.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.0",
@@ -120,8 +121,8 @@
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.4.2",
"marked": "7.0.5",
"luxon": "3.4.0",
"marked": "7.0.4",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -153,13 +154,12 @@
"xss": "1.0.14"
},
"devDependencies": {
"@babel/core": "7.22.11",
"@babel/core": "7.22.10",
"@babel/plugin-proposal-decorators": "7.22.10",
"@babel/plugin-transform-runtime": "7.22.10",
"@babel/preset-env": "7.22.14",
"@babel/preset-typescript": "7.22.11",
"@babel/preset-env": "7.22.10",
"@babel/preset-typescript": "7.22.5",
"@koa/cors": "4.0.0",
"@lokalise/node-api": "11.0.1",
"@octokit/auth-oauth-device": "6.0.0",
"@octokit/plugin-retry": "6.0.0",
"@octokit/rest": "20.0.1",
@@ -167,7 +167,7 @@
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "25.0.4",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.2.1",
"@rollup/plugin-node-resolve": "15.2.0",
"@rollup/plugin-replace": "5.0.2",
"@types/babel__plugin-transform-runtime": "7.9.2",
"@types/chromecast-caf-receiver": "6.0.9",
@@ -176,25 +176,25 @@
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.0",
"@types/js-yaml": "4.0.5",
"@types/leaflet": "1.9.4",
"@types/leaflet-draw": "1.0.8",
"@types/luxon": "3.3.2",
"@types/leaflet": "1.9.3",
"@types/leaflet-draw": "1.0.7",
"@types/luxon": "3.3.1",
"@types/mocha": "10.0.1",
"@types/qrcode": "1.5.2",
"@types/qrcode": "1.5.1",
"@types/serve-handler": "6.1.1",
"@types/sortablejs": "1.15.2",
"@types/sortablejs": "1.15.1",
"@types/tar": "6.1.5",
"@types/ua-parser-js": "0.7.37",
"@types/ua-parser-js": "0.7.36",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "6.5.0",
"@typescript-eslint/eslint-plugin": "6.4.0",
"@typescript-eslint/parser": "6.4.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.8",
"del": "7.1.0",
"eslint": "8.48.0",
"chai": "4.3.7",
"del": "7.0.0",
"eslint": "8.47.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.0.0",
@@ -208,7 +208,7 @@
"esprima": "4.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.1.1",
"glob": "10.3.4",
"glob": "10.3.3",
"gulp": "4.0.2",
"gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8",
@@ -219,16 +219,16 @@
"husky": "8.0.3",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "14.0.1",
"lint-staged": "14.0.0",
"lit-analyzer": "2.0.0-pre.3",
"lodash.template": "4.5.0",
"magic-string": "0.30.3",
"magic-string": "0.30.2",
"map-stream": "0.0.7",
"mocha": "10.2.0",
"object-hash": "3.0.0",
"open": "9.1.0",
"pinst": "3.0.0",
"prettier": "3.0.3",
"prettier": "3.0.2",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
@@ -240,7 +240,7 @@
"tar": "6.1.15",
"terser-webpack-plugin": "5.3.9",
"ts-lit-plugin": "2.0.0-pre.1",
"typescript": "5.2.2",
"typescript": "5.1.6",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0",
"webpack": "5.88.2",
@@ -257,5 +257,5 @@
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},
"packageManager": "yarn@3.6.3"
"packageManager": "yarn@3.6.2"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

@@ -8,4 +8,40 @@ set -eu -o pipefail
cd "$(dirname "$0")/.."
./node_modules/.bin/gulp download-translations
if [ -z "${LOKALISE_TOKEN-}" ] && [ ! -f .lokalise_token ] ; then
echo "Lokalise API token is required to download the latest set of" \
"translations. Please create an account by using the following link:" \
"https://lokalise.co/signup/3420425759f6d6d241f598.13594006/all/" \
"Place your token in a new file \".lokalise_token\" in the repo" \
"root directory."
exit 1
fi
# Load token from file if not already in the environment
[ -z "${LOKALISE_TOKEN-}" ] && LOKALISE_TOKEN="$(<.lokalise_token)"
declare -A PROJECT_ID=( \
[frontend]="3420425759f6d6d241f598.13594006" \
[backend]="130246255a974bd3b5e8a1.51616605" \
)
for project in ${!PROJECT_ID[*]}; do
LOCAL_DIR=`pwd`/translations/${project}
rm -f ${LOCAL_DIR}/* || mkdir -p ${LOCAL_DIR}
docker run \
-v ${LOCAL_DIR}:/opt/dest/locale \
--rm \
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d \
lokalise2 \
--token ${LOKALISE_TOKEN} \
--project-id ${PROJECT_ID[${project}]} \
file download \
--export-empty-as skip \
--format json \
--json-unescaped-slashes=true \
--replace-breaks=false \
--original-filenames=false \
--unzip-to /opt/dest
done
./node_modules/.bin/gulp check-downloaded-translations

View File

@@ -35,8 +35,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public oauth2State?: string;
@property() public translationFragment = "page-authorize";
@state() private _authProvider?: AuthProvider;
@state() private _authProviders?: AuthProvider[];
@@ -47,6 +45,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
constructor() {
super();
this.translationFragment = "page-authorize";
const query = extractSearchParamsObject() as AuthUrlSearchParams;
if (query.client_id) {
this.clientId = query.client_id;
@@ -103,6 +102,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
: nothing}
<ha-auth-flow
.resources=${this.resources}
.clientId=${this.clientId}
.redirectUri=${this.redirectUri}
.oauth2State=${this.oauth2State}

View File

@@ -107,7 +107,6 @@ export class HaPasswordManagerPolyfill extends LitElement {
.value=${this.stepData[schema.name] || ""}
.autocomplete=${schema.autocomplete}
@input=${this._valueChanged}
@change=${this._valueChanged}
/>
`;
}

View File

@@ -4,7 +4,7 @@ export const clamp = (value: number, min: number, max: number) =>
// Variant that only applies the clamping to a border if the border is defined
export const conditionalClamp = (value: number, min?: number, max?: number) => {
let result: number;
result = min != null ? Math.max(value, min) : value;
result = max != null ? Math.min(result, max) : result;
result = min ? Math.max(value, min) : value;
result = max ? Math.min(result, max) : result;
return result;
};

View File

@@ -108,7 +108,7 @@ export const formatNumber = (
* @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined`
*/
export const getNumberFormatOptions = (
entityState?: HassEntity,
entityState: HassEntity,
entity?: EntityRegistryDisplayEntry
): Intl.NumberFormatOptions | undefined => {
const precision = entity?.display_precision;
@@ -119,8 +119,8 @@ export const getNumberFormatOptions = (
};
}
if (
Number.isInteger(Number(entityState?.attributes?.step)) &&
Number.isInteger(Number(entityState?.state))
Number.isInteger(Number(entityState.attributes?.step)) &&
Number.isInteger(Number(entityState.state))
) {
return { maximumFractionDigits: 0 };
}

View File

@@ -22,7 +22,14 @@ export type LocalizeKeys =
| `ui.dialogs.unhealthy.reason.${string}`
| `ui.dialogs.unsupported.reason.${string}`
| `ui.panel.config.${string}.${"caption" | "description"}`
| `ui.panel.config.automation.${string}`
| `ui.panel.config.dashboard.${string}`
| `ui.panel.config.devices.${string}`
| `ui.panel.config.energy.${string}`
| `ui.panel.config.info.${string}`
| `ui.panel.config.lovelace.${string}`
| `ui.panel.config.network.${string}`
| `ui.panel.config.scene.${string}`
| `ui.panel.config.zha.${string}`
| `ui.panel.config.zwave_js.${string}`
| `ui.panel.lovelace.card.${string}`

View File

@@ -15,20 +15,13 @@ import { HomeAssistant } from "../../types";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
export interface ChartResizeOptions {
aspectRatio?: number;
height?: number;
width?: number;
}
interface Tooltip
extends Omit<TooltipModel<any>, "tooltipPosition" | "hasValue" | "getProps"> {
interface Tooltip extends TooltipModel<any> {
top: string;
left: string;
}
@customElement("ha-chart-base")
export class HaChartBase extends LitElement {
export default class HaChartBase extends LitElement {
public chart?: Chart;
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -52,6 +45,14 @@ export class HaChartBase extends LitElement {
@state() private _hiddenDatasets: Set<number> = new Set();
private _releaseCanvas() {
// release the canvas memory to prevent
// safari from running out of memory.
if (this.chart) {
this.chart.destroy();
}
}
public disconnectedCallback() {
this._releaseCanvas();
super.disconnectedCallback();
@@ -64,36 +65,6 @@ export class HaChartBase extends LitElement {
}
}
public updateChart = (
mode:
| "resize"
| "reset"
| "none"
| "hide"
| "show"
| "default"
| "active"
| undefined
): void => {
this.chart?.update(mode);
};
public resize = (options?: ChartResizeOptions): void => {
if (options?.aspectRatio && !options.height) {
options.height = Math.round(
(options.width ?? this.clientWidth) / options.aspectRatio
);
} else if (options?.aspectRatio && !options.width) {
options.width = Math.round(
(options.height ?? this.clientHeight) * options.aspectRatio
);
}
this.chart?.resize(
options?.width ?? this.clientWidth,
options?.height ?? this.clientHeight
);
};
protected firstUpdated() {
this._setupChart();
this.data.datasets.forEach((dataset, index) => {
@@ -109,11 +80,14 @@ export class HaChartBase extends LitElement {
if (!this.hasUpdated || !this.chart) {
return;
}
if (changedProps.has("plugins") || changedProps.has("chartType")) {
if (changedProps.has("plugins")) {
this.chart.destroy();
this._setupChart();
return;
}
if (changedProps.has("chartType")) {
this.chart.config.type = this.chartType;
}
if (changedProps.has("data")) {
if (this._hiddenDatasets.size) {
this.data.datasets.forEach((dataset, index) => {
@@ -157,70 +131,55 @@ export class HaChartBase extends LitElement {
</div>`
: ""}
<div
class="animationContainer"
class="chartContainer"
style=${styleMap({
height: `${this.height || this._chartHeight || 0}px`,
height: `${this.height ?? this._chartHeight}px`,
overflow: this._chartHeight ? "initial" : "hidden",
"padding-left": `${computeRTL(this.hass) ? 0 : this.paddingYAxis}px`,
"padding-right": `${computeRTL(this.hass) ? this.paddingYAxis : 0}px`,
})}
>
<div
class="chartContainer"
style=${styleMap({
height: `${
this.height ?? this._chartHeight ?? this.clientWidth / 2
}px`,
"padding-left": `${
computeRTL(this.hass) ? 0 : this.paddingYAxis
}px`,
"padding-right": `${
computeRTL(this.hass) ? this.paddingYAxis : 0
}px`,
})}
>
<canvas></canvas>
${this._tooltip
? html`<div
class="chartTooltip ${classMap({
[this._tooltip.yAlign]: true,
})}"
style=${styleMap({
top: this._tooltip.top,
left: this._tooltip.left,
})}
>
<div class="title">${this._tooltip.title}</div>
${this._tooltip.beforeBody
? html`<div class="beforeBody">
${this._tooltip.beforeBody}
</div>`
: ""}
<div>
<ul>
${this._tooltip.body.map(
(item, i) =>
html`<li>
<div
class="bullet"
style=${styleMap({
backgroundColor: this._tooltip!.labelColors[i]
.backgroundColor as string,
borderColor: this._tooltip!.labelColors[i]
.borderColor as string,
})}
></div>
${item.lines.join("\n")}
</li>`
)}
</ul>
</div>
${this._tooltip.footer.length
? html`<div class="footer">
${this._tooltip.footer.map((item) => html`${item}<br />`)}
</div>`
: ""}
</div>`
: ""}
</div>
<canvas></canvas>
${this._tooltip
? html`<div
class="chartTooltip ${classMap({ [this._tooltip.yAlign]: true })}"
style=${styleMap({
top: this._tooltip.top,
left: this._tooltip.left,
})}
>
<div class="title">${this._tooltip.title}</div>
${this._tooltip.beforeBody
? html`<div class="beforeBody">
${this._tooltip.beforeBody}
</div>`
: ""}
<div>
<ul>
${this._tooltip.body.map(
(item, i) =>
html`<li>
<div
class="bullet"
style=${styleMap({
backgroundColor: this._tooltip!.labelColors[i]
.backgroundColor as string,
borderColor: this._tooltip!.labelColors[i]
.borderColor as string,
})}
></div>
${item.lines.join("\n")}
</li>`
)}
</ul>
</div>
${this._tooltip.footer.length
? html`<div class="footer">
${this._tooltip.footer.map((item) => html`${item}<br />`)}
</div>`
: ""}
</div>`
: ""}
</div>
`;
}
@@ -254,7 +213,6 @@ export class HaChartBase extends LitElement {
private _createOptions() {
return {
maintainAspectRatio: false,
...this.options,
plugins: {
...this.options?.plugins,
@@ -275,10 +233,10 @@ export class HaChartBase extends LitElement {
return [
...(this.plugins || []),
{
id: "resizeHook",
resize: (chart) => {
id: "afterRenderHook",
afterRender: (chart) => {
const change = chart.height - (this._chartHeight ?? 0);
if (!this._chartHeight || change > 12 || change < -12) {
if (!this._chartHeight || change > 0 || change < -12) {
// hysteresis to prevent infinite render loops
this._chartHeight = chart.height;
}
@@ -330,13 +288,21 @@ export class HaChartBase extends LitElement {
};
}
private _releaseCanvas() {
// release the canvas memory to prevent
// safari from running out of memory.
public updateChart = (
mode:
| "resize"
| "reset"
| "none"
| "hide"
| "show"
| "normal"
| "active"
| undefined
): void => {
if (this.chart) {
this.chart.destroy();
this.chart.update(mode);
}
}
};
static get styles(): CSSResultGroup {
return css`
@@ -344,14 +310,11 @@ export class HaChartBase extends LitElement {
display: block;
position: var(--chart-base-position, relative);
}
.animationContainer {
.chartContainer {
overflow: hidden;
height: 0;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
.chartContainer {
position: relative;
}
canvas {
max-height: var(--chart-max-height, 400px);
}

View File

@@ -1,6 +1,6 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { html, LitElement, PropertyValues } from "lit";
import { property, query, state } from "lit/decorators";
import { property, state } from "lit/decorators";
import { getGraphColorByIndex } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
@@ -11,18 +11,14 @@ import {
} from "../../common/number/format_number";
import { LineChartEntity, LineChartState } from "../../data/history";
import { HomeAssistant } from "../../types";
import {
ChartResizeOptions,
HaChartBase,
MIN_TIME_BETWEEN_UPDATES,
} from "./ha-chart-base";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
const safeParseFloat = (value) => {
const parsed = parseFloat(value);
return isFinite(parsed) ? parsed : null;
};
export class StateHistoryChartLine extends LitElement {
class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data: LineChartEntity[] = [];
@@ -51,12 +47,6 @@ export class StateHistoryChartLine extends LitElement {
private _chartTime: Date = new Date();
@query("ha-chart-base") private _chart?: HaChartBase;
public resize = (options?: ChartResizeOptions): void => {
this._chart?.resize(options);
};
protected render() {
return html`
<ha-chart-base
@@ -137,16 +127,12 @@ export class StateHistoryChartLine extends LitElement {
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale,
this.data[context.datasetIndex]?.entity_id
? getNumberFormatOptions(
this.hass.states[
this.data[context.datasetIndex].entity_id
],
this.hass.entities[
this.data[context.datasetIndex].entity_id
]
)
: undefined
getNumberFormatOptions(
this.hass.states[this.data[context.datasetIndex].entity_id],
this.hass.entities[
this.data[context.datasetIndex].entity_id
]
)
)} ${this.unit}`,
},
},

View File

@@ -1,6 +1,6 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
import { fireEvent } from "../../common/dom/fire_event";
@@ -8,11 +8,7 @@ import { numberFormatToLocale } from "../../common/number/format_number";
import { computeRTL } from "../../common/util/compute_rtl";
import { TimelineEntity } from "../../data/history";
import { HomeAssistant } from "../../types";
import {
ChartResizeOptions,
HaChartBase,
MIN_TIME_BETWEEN_UPDATES,
} from "./ha-chart-base";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const";
import { computeTimelineColor } from "./timeline-chart/timeline-color";
@@ -50,12 +46,6 @@ export class StateHistoryChartTimeline extends LitElement {
private _chartTime: Date = new Date();
@query("ha-chart-base") private _chart?: HaChartBase;
public resize = (options?: ChartResizeOptions): void => {
this._chart?.resize(options);
};
protected render() {
return html`
<ha-chart-base

View File

@@ -6,13 +6,7 @@ import {
nothing,
PropertyValues,
} from "lit";
import {
customElement,
eventOptions,
property,
queryAll,
state,
} from "lit/decorators";
import { customElement, eventOptions, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import {
@@ -24,9 +18,6 @@ import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import "./state-history-chart-line";
import "./state-history-chart-timeline";
import type { StateHistoryChartLine } from "./state-history-chart-line";
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
import { ChartResizeOptions } from "./ha-chart-base";
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
@@ -84,16 +75,6 @@ export class StateHistoryCharts extends LitElement {
// @ts-ignore
@restoreScroll(".container") private _savedScrollPos?: number;
@queryAll("state-history-chart-line")
private _charts?: StateHistoryChartLine[];
public resize = (options?: ChartResizeOptions): void => {
this._charts?.forEach(
(chart: StateHistoryChartLine | StateHistoryChartTimeline) =>
chart.resize(options)
);
};
protected render() {
if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info">

View File

@@ -12,7 +12,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
@@ -31,7 +31,6 @@ import {
} from "../../data/recorder";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
import type { ChartResizeOptions, HaChartBase } from "./ha-chart-base";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean",
@@ -43,7 +42,7 @@ export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
};
@customElement("statistics-chart")
export class StatisticsChart extends LitElement {
class StatisticsChart extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public statisticsData?: Statistics;
@@ -76,14 +75,8 @@ export class StatisticsChart extends LitElement {
@state() private _chartOptions?: ChartOptions;
@query("ha-chart-base") private _chart?: HaChartBase;
private _computedStyle?: CSSStyleDeclaration;
public resize = (options?: ChartResizeOptions): void => {
this._chart?.resize(options);
};
protected shouldUpdate(changedProps: PropertyValues): boolean {
return changedProps.size > 1 || !changedProps.has("hass");
}

View File

@@ -1,8 +1,3 @@
import type {
BarControllerChartOptions,
BarControllerDatasetOptions,
} from "chart.js";
export interface TimeLineData {
start: Date;
end: Date;

View File

@@ -16,7 +16,7 @@ export interface TextBaroptions extends BarOptions {
export class TextBarElement extends BarElement {
static id = "textbar";
draw(ctx: CanvasRenderingContext2D) {
draw(ctx) {
super.draw(ctx);
const options = this.options as TextBaroptions;
const { x, y, base, width, text } = (

View File

@@ -2,95 +2,6 @@ import { BarController, BarElement } from "chart.js";
import { TimeLineData } from "./const";
import { TextBarProps } from "./textbar-element";
function borderProps(properties) {
let reverse;
let start;
let end;
let top;
let bottom;
if (properties.horizontal) {
reverse = properties.base > properties.x;
start = "left";
end = "right";
} else {
reverse = properties.base < properties.y;
start = "bottom";
end = "top";
}
if (reverse) {
top = "end";
bottom = "start";
} else {
top = "start";
bottom = "end";
}
return { start, end, reverse, top, bottom };
}
function setBorderSkipped(properties, options, stack, index) {
let edge = options.borderSkipped;
const res = {};
if (!edge) {
properties.borderSkipped = res;
return;
}
if (edge === true) {
properties.borderSkipped = {
top: true,
right: true,
bottom: true,
left: true,
};
return;
}
const { start, end, reverse, top, bottom } = borderProps(properties);
if (edge === "middle" && stack) {
properties.enableBorderRadius = true;
if ((stack._top || 0) === index) {
edge = top;
} else if ((stack._bottom || 0) === index) {
edge = bottom;
} else {
res[parseEdge(bottom, start, end, reverse)] = true;
edge = top;
}
}
res[parseEdge(edge, start, end, reverse)] = true;
properties.borderSkipped = res;
}
function parseEdge(edge, a, b, reverse) {
if (reverse) {
edge = swap(edge, a, b);
edge = startEnd(edge, b, a);
} else {
edge = startEnd(edge, a, b);
}
return edge;
}
function swap(orig, v1, v2) {
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
}
function startEnd(v, start, end) {
return v === "start" ? start : v === "end" ? end : v;
}
function setInflateAmount(
properties,
{ inflateAmount }: { inflateAmount?: string | number },
ratio
) {
properties.inflateAmount =
inflateAmount === "auto" ? (ratio === 1 ? 0.33 : 0) : inflateAmount;
}
function parseValue(entry, item, vScale, i) {
const startValue = vScale.parse(entry.start, i);
const endValue = vScale.parse(entry.end, i);
@@ -186,7 +97,7 @@ export class TimelineController extends BarController {
bars: BarElement[],
start: number,
count: number,
mode: "reset" | "resize" | "none" | "hide" | "show" | "default" | "active"
mode: "reset" | "resize" | "none" | "hide" | "show" | "normal" | "active"
) {
const vScale = this._cachedMeta.vScale!;
const iScale = this._cachedMeta.iScale!;
@@ -203,15 +114,15 @@ export class TimelineController extends BarController {
for (let index = start; index < start + count; index++) {
const data = dataset.data[index] as TimeLineData;
// @ts-ignore
const y = vScale.getPixelForValue(this.index);
// @ts-ignore
const xStart = iScale.getPixelForValue(data.start.getTime());
// @ts-ignore
const xEnd = iScale.getPixelForValue(data.end.getTime());
const width = xEnd - xStart;
const parsed = this.getParsed(index);
const stack = (parsed._stacks || {})[vScale.axis];
const height = 10;
const properties: TextBarProps = {
@@ -234,10 +145,7 @@ export class TimelineController extends BarController {
backgroundColor: data.color,
};
}
const options = properties.options || bars[index].options;
setBorderSkipped(properties, options, stack, index);
setInflateAmount(properties, options, 1);
this.updateElement(bars[index], index, properties as any, mode);
}
}

View File

@@ -458,8 +458,7 @@ export class HaDataTable extends LitElement {
filteredData,
this._sortColumns[this._sortColumn],
this._sortDirection,
this._sortColumn,
this.hass.locale.language
this._sortColumn
)
: filteredData;

View File

@@ -1,7 +1,6 @@
// To use comlink under ES5
import { expose } from "comlink";
import "proxy-polyfill";
import { stringCompare } from "../../common/string/compare";
import { expose } from "comlink";
import type {
ClonedDataTableColumnData,
DataTableRowData,
@@ -40,8 +39,7 @@ const sortData = (
data: DataTableRowData[],
column: ClonedDataTableColumnData,
direction: SortingDirection,
sortColumn: string,
language?: string
sortColumn: string
) =>
data.sort((a, b) => {
let sort = 1;
@@ -60,8 +58,13 @@ const sortData = (
if (column.type === "numeric") {
valA = isNaN(valA) ? undefined : Number(valA);
valB = isNaN(valB) ? undefined : Number(valB);
} else if (typeof valA === "string" && typeof valB === "string") {
return sort * stringCompare(valA, valB, language);
} else {
if (typeof valA === "string") {
valA = valA.toUpperCase();
}
if (typeof valB === "string") {
valB = valB.toUpperCase();
}
}
// Ensure "undefined" and "null" are always sorted to the bottom

View File

@@ -27,12 +27,10 @@ export const filterData = (
filter: FilterDataParamTypes[2]
): Promise<ReturnType<FilterDataType>> =>
getWorker().filterData(data, columns, filter);
export const sortData = (
data: SortDataParamTypes[0],
columns: SortDataParamTypes[1],
direction: SortDataParamTypes[2],
sortColumn: SortDataParamTypes[3],
language?: SortDataParamTypes[4]
sortColumn: SortDataParamTypes[3]
): Promise<ReturnType<SortDataType>> =>
getWorker().sortData(data, columns, direction, sortColumn, language);
getWorker().sortData(data, columns, direction, sortColumn);

View File

@@ -324,7 +324,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
.renderer=${rowRenderer}
.disabled=${this.disabled}
.required=${this.required}
item-id-path="id"
item-value-path="id"
item-label-path="name"
@opened-changed=${this._openedChanged}

View File

@@ -1,9 +1,11 @@
import { HassEntity } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, html, nothing } from "lit";
import { html, LitElement, PropertyValues, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { getStates } from "../../common/entity/get_states";
import { HomeAssistant, ValueChangedEvent } from "../../types";
import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display";
import { ValueChangedEvent, HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
@@ -56,9 +58,20 @@ class HaEntityStatePicker extends LitElement {
? getStates(state, this.attribute).map((key) => ({
value: key,
label: !this.attribute
? this.hass.formatEntityState(state, key)
: this.hass.formatEntityAttributeValue(
? computeStateDisplay(
this.hass.localize,
state,
this.hass.locale,
this.hass.config,
this.hass.entities,
key
)
: computeAttributeValueDisplay(
this.hass.localize,
state,
this.hass.locale,
this.hass.config,
this.hass.entities,
this.attribute,
key
),

View File

@@ -12,6 +12,7 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
@@ -191,7 +192,13 @@ export class HaStateLabelBadge extends LitElement {
this.hass!.locale,
getNumberFormatOptions(entityState, entry)
)
: this.hass!.formatEntityState(entityState);
: computeStateDisplay(
this.hass!.localize,
entityState,
this.hass!.locale,
this.hass!.config,
this.hass!.entities
);
}
}

View File

@@ -1,19 +1,12 @@
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-attribute-value";
import "./ha-expansion-panel";
import "./ha-attribute-value";
@customElement("ha-attributes")
class HaAttributes extends LitElement {
@@ -25,30 +18,16 @@ class HaAttributes extends LitElement {
@state() private _expanded = false;
private get _filteredAttributes() {
return this.computeDisplayAttributes(
STATE_ATTRIBUTES.concat(
this.extraFilters ? this.extraFilters.split(",") : []
)
);
}
protected willUpdate(changedProperties: PropertyValues): void {
if (
changedProperties.has("extraFilters") ||
changedProperties.has("stateObj")
) {
this.toggleAttribute("empty", this._filteredAttributes.length === 0);
}
}
protected render() {
if (!this.stateObj) {
return nothing;
}
const attributes = this._filteredAttributes;
const attributes = this.computeDisplayAttributes(
STATE_ATTRIBUTES.concat(
this.extraFilters ? this.extraFilters.split(",") : []
)
);
if (attributes.length === 0) {
return nothing;
}

View File

@@ -1,14 +1,9 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { CLIMATE_PRESET_NONE, ClimateEntity } from "../data/climate";
import { isUnavailableState, OFF } from "../data/entity";
import { isUnavailableState } from "../data/entity";
import type { HomeAssistant } from "../types";
@customElement("ha-climate-state")
@@ -27,24 +22,26 @@ class HaClimateState extends LitElement {
${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`-
${this.hass.formatEntityAttributeValue(
${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"preset_mode"
)}`
: nothing}
: ""}
</span>
<div class="unit">${this._computeTarget()}</div>`
: this._localizeState()}
</div>
${currentStatus && !isUnavailableState(this.stateObj.state)
? html`
<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
</div>
`
: nothing}`;
? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
</div>`
: ""}`;
}
private _computeCurrentStatus(): string | undefined {
@@ -128,17 +125,24 @@ class HaClimateState extends LitElement {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = this.hass.formatEntityState(this.stateObj);
const stateString = computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
);
if (this.stateObj.attributes.hvac_action && this.stateObj.state !== OFF) {
const actionString = this.hass.formatEntityAttributeValue(
this.stateObj,
"hvac_action"
);
return `${actionString} (${stateString})`;
}
return stateString;
return this.stateObj.attributes.hvac_action
? `${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"hvac_action"
)} (${stateString})`
: stateString;
}
static get styles(): CSSResultGroup {

View File

@@ -244,6 +244,7 @@ export class HaComboBox extends LitElement {
);
if (overlay) {
overlay.setAttribute("required-vertical-space", "0");
this._removeInert(overlay);
}
this._observeBody();
@@ -311,10 +312,6 @@ export class HaComboBox extends LitElement {
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
ev.stopPropagation();
if (!this.allowCustomValue) {
// @ts-ignore
this._comboBox._closeOnBlurIsPrevented = true;
}
const newValue = ev.detail.value;
if (newValue !== this.value) {
@@ -330,7 +327,7 @@ export class HaComboBox extends LitElement {
}
vaadin-combo-box-light {
position: relative;
--vaadin-combo-box-overlay-max-height: calc(45vh - 56px);
--vaadin-combo-box-overlay-max-height: calc(45vh);
}
ha-textfield {
width: 100%;

View File

@@ -1,260 +0,0 @@
import { mdiMinus, mdiPlus } from "@mdi/js";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { conditionalClamp } from "../common/number/clamp";
import { formatNumber } from "../common/number/format_number";
import { FrontendLocaleData } from "../data/translation";
import { fireEvent } from "../common/dom/fire_event";
const A11Y_KEY_CODES = new Set([
"ArrowRight",
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"PageUp",
"PageDown",
"Home",
"End",
]);
@customElement("ha-control-number-buttons")
export class HaControlNumberButton extends LitElement {
@property({ attribute: false }) public locale?: FrontendLocaleData;
@property({ type: Boolean, reflect: true }) disabled = false;
@property() public label?: string;
@property({ type: Number }) public step?: number;
@property({ type: Number }) public value?: number;
@property({ type: Number }) public min?: number;
@property({ type: Number }) public max?: number;
@property({ attribute: "false" })
public formatOptions: Intl.NumberFormatOptions = {};
@query("#input") _input!: HTMLDivElement;
private boundedValue(value: number) {
const clamped = conditionalClamp(value, this.min, this.max);
return Math.round(clamped / this._step) * this._step;
}
private get _step() {
return this.step ?? 1;
}
private get _value() {
return this.value ?? 0;
}
private get _tenPercentStep() {
if (this.max == null || this.min == null) return this._step;
const range = this.max - this.min / 10;
if (range <= this._step) return this._step;
return Math.max(range / 10);
}
private _handlePlusButton() {
this._increment();
fireEvent(this, "value-changed", { value: this.value });
this._input.focus();
}
private _handleMinusButton() {
this._decrement();
fireEvent(this, "value-changed", { value: this.value });
this._input.focus();
}
private _increment() {
this.value = this.boundedValue(this._value + this._step);
}
private _decrement() {
this.value = this.boundedValue(this._value - this._step);
}
_handleKeyDown(e: KeyboardEvent) {
if (this.disabled) return;
if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault();
switch (e.code) {
case "ArrowRight":
case "ArrowUp":
this._increment();
break;
case "ArrowLeft":
case "ArrowDown":
this._decrement();
break;
case "PageUp":
this.value = this.boundedValue(this._value + this._tenPercentStep);
break;
case "PageDown":
this.value = this.boundedValue(this._value - this._tenPercentStep);
break;
case "Home":
if (this.min != null) {
this.value = this.min;
}
break;
case "End":
if (this.max != null) {
this.value = this.max;
}
break;
}
fireEvent(this, "value-changed", { value: this.value });
}
protected render(): TemplateResult {
const displayedValue =
this.value != null
? formatNumber(this.value, this.locale, this.formatOptions)
: "";
return html`
<div class="container">
<div
id="input"
class="value"
role="number-button"
.tabIndex=${this.disabled ? "-1" : "0"}
aria-valuenow=${this.value}
aria-valuemin=${this.min}
aria-valuemax=${this.max}
aria-label=${ifDefined(this.label)}
?disabled=${this.disabled}
@keydown=${this._handleKeyDown}
>
${displayedValue}
</div>
<button
class="button minus"
type="button"
tabindex="-1"
aria-label="decrement"
@click=${this._handleMinusButton}
.disabled=${this.disabled ||
(this.min != null && this._value <= this.min)}
>
<ha-svg-icon aria-hidden .path=${mdiMinus}></ha-svg-icon>
</button>
<button
class="button plus"
type="button"
tabindex="-1"
aria-label="increment"
@click=${this._handlePlusButton}
.disabled=${this.disabled ||
(this.max != null && this._value >= this.max)}
>
<ha-svg-icon aria-hidden .path=${mdiPlus}></ha-svg-icon>
</button>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
--control-number-buttons-focus-color: var(--primary-color);
--control-number-buttons-background-color: var(--disabled-color);
--control-number-buttons-background-opacity: 0.2;
--control-number-buttons-border-radius: 10px;
--mdc-icon-size: 16px;
height: 40px;
width: 200px;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
font-style: normal;
font-weight: 500;
transition: color 180ms ease-in-out;
}
:host([disabled]) {
color: var(--disabled-color);
}
.container {
position: relative;
width: 100%;
height: 100%;
}
.value {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
padding: 0 44px;
border-radius: var(--control-number-buttons-border-radius);
padding: 0;
margin: 0;
box-sizing: border-box;
line-height: 0;
overflow: hidden;
/* For safari border-radius overflow */
z-index: 0;
font-size: inherit;
color: inherit;
user-select: none;
-webkit-tap-highlight-color: transparent;
outline: none;
}
.value::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: var(--control-number-buttons-background-color);
transition:
background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
opacity: var(--control-number-buttons-background-opacity);
}
.value:focus-visible {
box-shadow: 0 0 0 2px var(--control-number-buttons-focus-color);
}
.button {
color: inherit;
position: absolute;
top: 0;
bottom: 0;
padding: 0;
width: 35px;
height: 40px;
border: none;
background: none;
cursor: pointer;
outline: none;
}
.button[disabled] {
opacity: 0.4;
pointer-events: none;
cursor: not-allowed;
}
.button.minus {
left: 0;
}
.button.plus {
right: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-control-number-buttons": HaControlNumberButton;
}
}

View File

@@ -171,12 +171,15 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-background-color: var(--disabled-color);
--control-select-menu-background-opacity: 0.2;
--control-select-menu-border-radius: 14px;
--control-select-menu-min-width: 120px;
--control-select-menu-max-width: 200px;
--control-select-menu-width: 100%;
--mdc-icon-size: 20px;
width: auto;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
color: var(--control-select-menu-text-color);
height: 48px;
padding: 6px 10px;
overflow: hidden;
@@ -195,8 +198,11 @@ export class HaControlSelectMenu extends SelectBase {
z-index: 0;
font-size: inherit;
transition: color 180ms ease-in-out;
color: var(--control-text-icon-color);
gap: 10px;
width: 100%;
min-width: var(--control-select-menu-min-width);
max-width: var(--control-select-menu-max-width);
width: var(--control-select-menu-width);
user-select: none;
font-size: 14px;
font-style: normal;

View File

@@ -217,7 +217,6 @@ export class HaControlSelect extends LitElement {
transition: box-shadow 180ms ease-in-out;
font-style: normal;
font-weight: 500;
color: var(--primary-text-color);
user-select: none;
-webkit-tap-highlight-color: transparent;
}
@@ -268,6 +267,7 @@ export class HaControlSelect extends LitElement {
justify-content: center;
border-radius: var(--control-select-button-border-radius);
overflow: hidden;
color: var(--primary-text-color);
/* For safari border-radius overflow */
z-index: 0;
}
@@ -331,7 +331,6 @@ export class HaControlSelect extends LitElement {
:host([disabled]) {
--control-select-color: var(--disabled-color);
--control-select-focused-opacity: 0;
color: var(--disabled-color);
}
:host([disabled]) .option {
cursor: not-allowed;

View File

@@ -10,12 +10,12 @@ import "./ha-icon-button";
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
export const createCloseHeading = (
hass: HomeAssistant | undefined,
hass: HomeAssistant,
title: string | TemplateResult
) => html`
<div class="header_title">${title}</div>
<ha-icon-button
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
.label=${hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close"
class="header_button"

View File

@@ -1,19 +1,16 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiDelete, mdiFileUpload } from "@mdi/js";
import { LitElement, PropertyValues, TemplateResult, css, html } from "lit";
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import "./ha-button";
import "./ha-circular-progress";
import "./ha-icon-button";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ensureArray } from "../common/array/ensure-array";
import { bytesToString } from "../util/bytes-to-string";
declare global {
interface HASSDomEvents {
"file-picked": { files: File[] };
"file-picked": { files: FileList };
}
}
@@ -25,22 +22,12 @@ export class HaFileUpload extends LitElement {
@property() public icon?: string;
@property() public label?: string;
@property() public label!: string;
@property() public secondary?: string;
@property() public supports?: string;
@property() public value?: File | File[] | FileList | string;
@property({ type: Boolean }) private multiple = false;
@property({ type: Boolean, reflect: true }) public disabled: boolean = false;
@property() public value: string | TemplateResult | null = null;
@property({ type: Boolean }) private uploading = false;
@property({ type: Number }) private progress?: number;
@property({ type: Boolean, attribute: "auto-open-file-dialog" })
private autoOpenFileDialog = false;
@@ -58,102 +45,72 @@ export class HaFileUpload extends LitElement {
public render(): TemplateResult {
return html`
${this.uploading
? html`<div class="container">
<div class="row">
<span class="header"
>${this.value
? this.hass?.localize(
"ui.components.file-upload.uploading_name",
{ name: this.value }
)
: this.hass?.localize(
"ui.components.file-upload.uploading"
)}</span
? html`<ha-circular-progress
alt="Uploading"
size="large"
active
></ha-circular-progress>`
: html`
<label
for="input"
class="mdc-text-field mdc-text-field--filled ${classMap({
"mdc-text-field--focused": this._drag,
"mdc-text-field--with-leading-icon": Boolean(this.icon),
"mdc-text-field--with-trailing-icon": Boolean(this.value),
})}"
@drop=${this._handleDrop}
@dragenter=${this._handleDragStart}
@dragover=${this._handleDragStart}
@dragleave=${this._handleDragEnd}
@dragend=${this._handleDragEnd}
>
<span class="mdc-text-field__ripple"></span>
<span
class="mdc-floating-label ${this.value || this._drag
? "mdc-floating-label--float-above"
: ""}"
id="label"
>${this.label}</span
>
${this.progress
? html`<span class="progress"
>${this.progress}${blankBeforePercent(
this.hass!.locale
)}%</span
>`
: ""}
</div>
<mwc-linear-progress
.indeterminate=${!this.progress}
.progress=${this.progress ? this.progress / 100 : undefined}
></mwc-linear-progress>
</div>`
: html`<label
for=${this.value ? "" : "input"}
class="container ${classMap({
dragged: this._drag,
multiple: this.multiple,
value: Boolean(this.value),
})}"
@drop=${this._handleDrop}
@dragenter=${this._handleDragStart}
@dragover=${this._handleDragStart}
@dragleave=${this._handleDragEnd}
@dragend=${this._handleDragEnd}
>${!this.value
? html`<ha-svg-icon
class="big-icon"
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
<ha-button unelevated @click=${this._openFilePicker}>
${this.label ||
this.hass?.localize("ui.components.file-upload.label")}
</ha-button>
<span class="secondary"
>${this.secondary ||
this.hass?.localize(
"ui.components.file-upload.secondary"
)}</span
${this.icon
? html`<span
class="mdc-text-field__icon mdc-text-field__icon--leading"
>
<span class="supports">${this.supports}</span>`
: typeof this.value === "string"
? html`<div class="row">
<div class="value" @click=${this._openFilePicker}>
<ha-svg-icon
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
${this.value}
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.path=${mdiDelete}
></ha-icon-button>
</div>`
: (this.value instanceof FileList
? Array.from(this.value)
: ensureArray(this.value)
).map(
(file) =>
html`<div class="row">
<div class="value" @click=${this._openFilePicker}>
<ha-svg-icon
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
${file.name} - ${bytesToString(file.size)}
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.path=${mdiDelete}
></ha-icon-button>
</div>`
)}
<input
id="input"
type="file"
class="file"
.accept=${this.accept}
.multiple=${this.multiple}
@change=${this._handleFilePicked}
/></label>`}
<ha-icon-button
@click=${this._openFilePicker}
.path=${this.icon}
></ha-icon-button>
</span>`
: ""}
<div class="value">${this.value}</div>
<input
id="input"
type="file"
class="mdc-text-field__input file"
accept=${this.accept}
@change=${this._handleFilePicked}
aria-labelledby="label"
/>
${this.value
? html`<span
class="mdc-text-field__icon mdc-text-field__icon--trailing"
>
<ha-icon-button
slot="suffix"
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.close") ||
"close"}
.path=${mdiClose}
></ha-icon-button>
</span>`
: ""}
<span
class="mdc-line-ripple ${this._drag
? "mdc-line-ripple--active"
: ""}"
></span>
</label>
`}
`;
}
@@ -165,12 +122,7 @@ export class HaFileUpload extends LitElement {
ev.preventDefault();
ev.stopPropagation();
if (ev.dataTransfer?.files) {
fireEvent(this, "file-picked", {
files:
this.multiple || ev.dataTransfer.files.length === 1
? Array.from(ev.dataTransfer.files)
: [ev.dataTransfer.files[0]],
});
fireEvent(this, "file-picked", { files: ev.dataTransfer.files });
}
this._drag = false;
}
@@ -188,121 +140,93 @@ export class HaFileUpload extends LitElement {
}
private _handleFilePicked(ev) {
if (ev.target.files.length === 0) {
return;
}
this.value = ev.target.files;
fireEvent(this, "file-picked", { files: ev.target.files });
}
private _clearValue(ev: Event) {
ev.preventDefault();
this.value = null;
this._input!.value = "";
this.value = undefined;
fireEvent(this, "change");
}
static get styles() {
return css`
:host {
display: block;
height: 240px;
}
:host([disabled]) {
pointer-events: none;
color: var(--disabled-text-color);
}
.container {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: solid 1px
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
border-radius: var(--mdc-shape-small, 4px);
height: 100%;
}
label.container {
border: dashed 1px
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
cursor: pointer;
}
:host([disabled]) .container {
border-color: var(--disabled-color);
}
label.dragged {
border-color: var(--primary-color);
}
.dragged:before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: var(--primary-color);
content: "";
opacity: var(--dark-divider-opacity);
pointer-events: none;
border-radius: var(--mdc-shape-small, 4px);
}
label.value {
cursor: default;
}
label.value.multiple {
justify-content: unset;
overflow: auto;
}
.highlight {
color: var(--primary-color);
}
.row {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
padding: 0 16px;
box-sizing: border-box;
}
ha-button {
margin-bottom: 4px;
}
.supports {
color: var(--secondary-text-color);
font-size: 12px;
}
:host([disabled]) .secondary {
color: var(--disabled-text-color);
}
input.file {
display: none;
}
.value {
cursor: pointer;
}
.value ha-svg-icon {
margin-right: 8px;
}
.big-icon {
--mdc-icon-size: 48px;
margin-bottom: 8px;
}
ha-button {
--mdc-button-outline-color: var(--primary-color);
--mdc-icon-button-size: 24px;
}
mwc-linear-progress {
width: 100%;
padding: 16px;
box-sizing: border-box;
}
.header {
font-weight: 500;
}
.progress {
color: var(--secondary-text-color);
}
`;
return [
styles,
css`
:host {
display: block;
}
.mdc-text-field--filled {
height: auto;
padding-top: 16px;
cursor: pointer;
}
.mdc-text-field--filled.mdc-text-field--with-trailing-icon {
padding-top: 28px;
}
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
color: var(--secondary-text-color);
}
.mdc-text-field--filled.mdc-text-field--with-trailing-icon
.mdc-text-field__icon {
align-self: flex-end;
}
.mdc-text-field__icon--leading {
margin-bottom: 12px;
inset-inline-start: initial;
inset-inline-end: 0px;
direction: var(--direction);
}
.mdc-text-field--filled .mdc-floating-label--float-above {
transform: scale(0.75);
top: 8px;
}
.mdc-floating-label {
inset-inline-start: 16px !important;
inset-inline-end: initial !important;
direction: var(--direction);
}
.mdc-text-field--filled .mdc-floating-label {
inset-inline-start: 48px !important;
inset-inline-end: initial !important;
direction: var(--direction);
}
.mdc-text-field__icon--trailing {
pointer-events: auto !important;
}
.dragged:before {
position: var(--layout-fit_-_position);
top: var(--layout-fit_-_top);
right: var(--layout-fit_-_right);
bottom: var(--layout-fit_-_bottom);
left: var(--layout-fit_-_left);
background: currentColor;
content: "";
opacity: var(--dark-divider-opacity);
pointer-events: none;
border-radius: 4px;
}
.value {
width: 100%;
}
input.file {
display: none;
}
img {
max-width: 100%;
max-height: 125px;
}
ha-icon-button {
--mdc-icon-button-size: 24px;
--mdc-icon-size: 20px;
}
ha-circular-progress {
display: block;
text-align-last: center;
}
`,
];
}
}

View File

@@ -27,8 +27,7 @@ export const computeInitialHaFormData = (
data[field.name] = 0.0;
} else if (field.type === "select") {
if (field.options.length) {
const val = field.options[0];
data[field.name] = Array.isArray(val) ? val[0] : val;
data[field.name] = field.options[0][0];
}
} else if (field.type === "positive_time_period_dict") {
data[field.name] = {
@@ -62,7 +61,7 @@ export const computeInitialHaFormData = (
} else if ("select" in selector) {
if (selector.select?.options.length) {
const val = selector.select.options[0];
data[field.name] = typeof val === "string" ? val : val.value;
data[field.name] = Array.isArray(val) ? val[0] : val;
}
} else if ("duration" in selector) {
data[field.name] = {

View File

@@ -68,7 +68,6 @@ export class HaFormString extends LitElement implements HaFormElement {
: this.schema.description?.suffix}
.validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged}
@change=${this._valueChanged}
></ha-textfield>
${isPassword
? html`<ha-icon-button

View File

@@ -7,12 +7,6 @@ import { hsv2rgb, rgb2hex } from "../common/color/convert-color";
import { rgbw2rgb, rgbww2rgb } from "../common/color/convert-light-color";
import { fireEvent } from "../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"cursor-moved": { value?: any };
}
}
function xy2polar(x: number, y: number) {
const r = Math.sqrt(x * x + y * y);
const phi = Math.atan2(y, x);

View File

@@ -1,5 +1,7 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { isUnavailableState, OFF } from "../data/entity";
import { HumidifierEntity } from "../data/humidifier";
import type { HomeAssistant } from "../types";
@@ -19,8 +21,12 @@ class HaHumidifierState extends LitElement {
${this._localizeState()}
${this.stateObj.attributes.mode
? html`-
${this.hass.formatEntityAttributeValue(
${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"mode"
)}`
: ""}
@@ -72,17 +78,24 @@ class HaHumidifierState extends LitElement {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = this.hass.formatEntityState(this.stateObj);
const stateString = computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
);
if (this.stateObj.attributes.action && this.stateObj.state !== OFF) {
const actionString = this.hass.formatEntityAttributeValue(
this.stateObj,
"action"
);
return `${actionString} (${stateString})`;
}
return stateString;
return this.stateObj.attributes.action && this.stateObj.state !== OFF
? `${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"action"
)} (${stateString})`
: stateString;
}
static get styles(): CSSResultGroup {

View File

@@ -7,7 +7,6 @@ import { formatLanguageCode } from "../common/language/format_language";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { FrontendLocaleData } from "../data/translation";
import "../resources/intl-polyfill";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
@@ -21,7 +20,7 @@ export class HaLanguagePicker extends LitElement {
@property() public languages?: string[];
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@@ -42,18 +41,7 @@ export class HaLanguagePicker extends LitElement {
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
const localeChanged =
changedProperties.has("hass") &&
this.hass &&
changedProperties.get("hass") &&
changedProperties.get("hass").locale.language !==
this.hass.locale.language;
if (
changedProperties.has("languages") ||
changedProperties.has("value") ||
localeChanged
) {
if (changedProperties.has("languages") || changedProperties.has("value")) {
this._select.layoutOptions();
if (this._select.value !== this.value) {
fireEvent(this, "value-changed", { value: this._select.value });
@@ -63,27 +51,24 @@ export class HaLanguagePicker extends LitElement {
}
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.nativeName,
this.hass?.locale
this.hass.locale,
this.nativeName
);
const selectedItemIndex = languageOptions.findIndex(
const selectedItem = languageOptions.find(
(option) => option.value === this.value
);
if (selectedItemIndex === -1) {
if (!selectedItem) {
this.value = undefined;
}
if (localeChanged) {
this._select.select(selectedItemIndex);
}
}
}
private _getLanguagesOptions = memoizeOne(
(languages: string[], nativeName: boolean, locale?: FrontendLocaleData) => {
(languages: string[], locale: FrontendLocaleData, nativeName: boolean) => {
let options: { label: string; value: string }[] = [];
if (nativeName) {
const translations = translationMetadata.translations;
const translations = this.hass.translationMetadata.translations;
options = languages.map((lang) => {
let label = translations[lang]?.nativeName;
if (!label) {
@@ -102,14 +87,14 @@ export class HaLanguagePicker extends LitElement {
label,
};
});
} else if (locale) {
} else {
options = languages.map((lang) => ({
value: lang,
label: formatLanguageCode(lang, locale),
}));
}
if (!this.noSort && locale) {
if (!this.noSort) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, locale.language)
);
@@ -119,14 +104,20 @@ export class HaLanguagePicker extends LitElement {
);
private _computeDefaultLanguageOptions() {
this._defaultLanguages = Object.keys(translationMetadata.translations);
if (!this.hass.translationMetadata?.translations) {
return;
}
this._defaultLanguages = Object.keys(
this.hass.translationMetadata.translations
);
}
protected render() {
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.nativeName,
this.hass?.locale
this.hass.locale,
this.nativeName
);
const value =
@@ -134,10 +125,9 @@ export class HaLanguagePicker extends LitElement {
return html`
<ha-select
.label=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language")}
.value=${value || ""}
.label=${this.label ||
this.hass.localize("ui.components.language-picker.language")}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@@ -147,9 +137,9 @@ export class HaLanguagePicker extends LitElement {
>
${languageOptions.length === 0
? html`<ha-list-item value=""
>${this.hass?.localize(
>${this.hass.localize(
"ui.components.language-picker.no_languages"
) || "No languages"}</ha-list-item
)}</ha-list-item
>`
: languageOptions.map(
(option) => html`
@@ -172,7 +162,7 @@ export class HaLanguagePicker extends LitElement {
private _changed(ev): void {
const target = ev.target as HaSelect;
if (target.value === "" || target.value === this.value) {
if (!this.hass || target.value === "" || target.value === this.value) {
return;
}
this.value = target.value;

View File

@@ -1,5 +1,5 @@
import { mdiImagePlus } from "@mdi/js";
import { LitElement, TemplateResult, css, html } from "lit";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
@@ -9,7 +9,6 @@ import {
showImageCropperDialog,
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { HomeAssistant } from "../types";
import "./ha-button";
import "./ha-circular-progress";
import "./ha-file-upload";
@@ -21,12 +20,6 @@ export class HaPictureUpload extends LitElement {
@property() public label?: string;
@property() public secondary?: string;
@property() public supports?: string;
@property() public currentImageAltText?: string;
@property({ type: Boolean }) public crop = false;
@property({ attribute: false }) public cropOptions?: CropOptions;
@@ -36,44 +29,19 @@ export class HaPictureUpload extends LitElement {
@state() private _uploading = false;
public render(): TemplateResult {
if (!this.value) {
return html`
<ha-file-upload
.hass=${this.hass}
.icon=${mdiImagePlus}
.label=${this.label ||
this.hass.localize("ui.components.picture-upload.label")}
.secondary=${this.secondary}
.supports=${this.supports ||
this.hass.localize("ui.components.picture-upload.supported_formats")}
.uploading=${this._uploading}
@file-picked=${this._handleFilePicked}
@change=${this._handleFileCleared}
accept="image/png, image/jpeg, image/gif"
></ha-file-upload>
`;
}
return html`<div class="center-vertical">
<div class="value">
<img
.src=${this.value}
alt=${this.currentImageAltText ||
this.hass.localize("ui.components.picture-upload.current_image_alt")}
/>
<ha-button
@click=${this._handleChangeClick}
.label=${this.hass.localize(
"ui.components.picture-upload.change_picture"
)}
>
</ha-button>
</div>
</div>`;
}
private _handleChangeClick() {
this.value = null;
fireEvent(this, "change");
return html`
<ha-file-upload
.hass=${this.hass}
.icon=${mdiImagePlus}
.label=${this.label ||
this.hass.localize("ui.components.picture-upload.label")}
.uploading=${this._uploading}
.value=${this.value ? html`<img .src=${this.value} />` : ""}
@file-picked=${this._handleFilePicked}
@change=${this._handleFileCleared}
accept="image/png, image/jpeg, image/gif"
></ha-file-upload>
`;
}
private async _handleFilePicked(ev) {
@@ -132,35 +100,6 @@ export class HaPictureUpload extends LitElement {
this._uploading = false;
}
}
static get styles() {
return css`
:host {
display: block;
height: 240px;
}
ha-file-upload {
height: 100%;
}
.center-vertical {
display: flex;
align-items: center;
height: 100%;
}
.value {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
img {
max-width: 100%;
max-height: 200px;
margin-bottom: 4px;
border-radius: var(--file-upload-image-border-radius);
}
`;
}
}
declare global {

View File

@@ -47,9 +47,6 @@ export class HaSelect extends SelectBase {
.mdc-select__anchor {
width: var(--ha-select-min-width, 200px);
}
.mdc-select--filled .mdc-select__anchor {
height: var(--ha-select-height, 56px);
}
.mdc-select--filled .mdc-floating-label {
inset-inline-start: 12px;
inset-inline-end: initial;

View File

@@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { Action } from "../../data/script";
import { ActionSelector } from "../../data/selector";
@@ -19,13 +19,10 @@ export class HaActionSelector extends LitElement {
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-action
.disabled=${this.disabled}
.actions=${this.value || []}
.hass=${this.hass}
.nested=${this.selector.action?.nested}
.reOrderMode=${this.selector.action?.reorder_mode}
></ha-automation-action>
`;
}
@@ -40,11 +37,6 @@ export class HaActionSelector extends LitElement {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
`;
}
}

View File

@@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { Condition } from "../../data/automation";
import { ConditionSelector } from "../../data/selector";
@@ -19,13 +19,10 @@ export class HaConditionSelector extends LitElement {
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-condition
.disabled=${this.disabled}
.conditions=${this.value || []}
.hass=${this.hass}
.nested=${this.selector.condition?.nested}
.reOrderMode=${this.selector.condition?.reorder_mode}
></ha-automation-condition>
`;
}
@@ -40,11 +37,6 @@ export class HaConditionSelector extends LitElement {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
`;
}
}

View File

@@ -37,12 +37,9 @@ export class HaFileSelector extends LitElement {
.label=${this.label}
.required=${this.required}
.disabled=${this.disabled}
.supports=${this.helper}
.helper=${this.helper}
.uploading=${this._busy}
.value=${this.value
? this._filename?.name ||
this.hass.localize("ui.components.selectors.file.unknown_file")
: undefined}
.value=${this.value ? this._filename?.name || "Unknown file" : ""}
@file-picked=${this._uploadFile}
@change=${this._removeFile}
></ha-file-upload>

View File

@@ -0,0 +1,440 @@
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
import { LitElement, PropertyValues, css, html, svg } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { rgb2hex } from "../common/color/convert-color";
import {
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
temperature2rgb,
} from "../common/color/convert-light-color";
import { fireEvent } from "../common/dom/fire_event";
const SAFE_ZONE_FACTOR = 0.9;
declare global {
interface HASSDomEvents {
"cursor-moved": { value?: any };
}
}
const A11Y_KEY_CODES = new Set([
"ArrowRight",
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"PageUp",
"PageDown",
"Home",
"End",
]);
function xy2polar(x: number, y: number) {
const r = Math.sqrt(x * x + y * y);
const phi = Math.atan2(y, x);
return [r, phi];
}
function polar2xy(r: number, phi: number) {
const x = Math.cos(phi) * r;
const y = Math.sin(phi) * r;
return [x, y];
}
function drawColorWheel(
ctx: CanvasRenderingContext2D,
minTemp: number,
maxTemp: number
) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const radius = ctx.canvas.width / 2;
const min = Math.max(minTemp, 2000);
const max = Math.min(maxTemp, 40000);
for (let y = -radius; y < radius; y += 1) {
const x = radius * Math.sqrt(1 - (y / radius) ** 2);
const fraction = (y / (radius * SAFE_ZONE_FACTOR) + 1) / 2;
const temperature = Math.max(
Math.min(min + fraction * (max - min), max),
min
);
const color = rgb2hex(temperature2rgb(temperature));
ctx.fillStyle = color;
ctx.fillRect(radius - x, radius + y - 0.5, 2 * x, 2);
ctx.fill();
}
}
@customElement("ha-temp-color-picker")
class HaTempColorPicker extends LitElement {
@property({ type: Boolean, reflect: true })
public disabled = false;
@property({ type: Number, attribute: false })
public renderSize?: number;
@property({ type: Number })
public value?: number;
@property({ type: Number })
public min = DEFAULT_MIN_KELVIN;
@property({ type: Number })
public max = DEFAULT_MAX_KELVIN;
@query("#canvas") private _canvas!: HTMLCanvasElement;
private _mc?: HammerManager;
@state()
private _pressed?: string;
@state()
private _cursorPosition?: [number, number];
@state()
private _localValue?: number;
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._setupListeners();
this._generateColorWheel();
this.setAttribute("role", "slider");
this.setAttribute("aria-orientation", "vertical");
if (!this.hasAttribute("tabindex")) {
this.setAttribute("tabindex", "0");
}
}
private _generateColorWheel() {
const ctx = this._canvas.getContext("2d")!;
drawColorWheel(ctx, this.min, this.max);
}
connectedCallback(): void {
super.connectedCallback();
this._setupListeners();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._destroyListeners();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("_localValue")) {
this.setAttribute("aria-valuenow", this._localValue?.toString() ?? "");
}
if (changedProps.has("min") || changedProps.has("max")) {
this._generateColorWheel();
this._resetPosition();
}
if (changedProps.has("min")) {
this.setAttribute("aria-valuemin", this.min.toString());
}
if (changedProps.has("max")) {
this.setAttribute("aria-valuemax", this.max.toString());
}
if (changedProps.has("value")) {
if (this._localValue !== this.value) {
this._resetPosition();
}
}
}
private _setupListeners() {
if (this._canvas && !this._mc) {
this._mc = new Manager(this._canvas);
this._mc.add(
new Pan({
direction: DIRECTION_ALL,
enable: true,
threshold: 0,
})
);
this._mc.add(new Tap({ event: "singletap" }));
let savedPosition;
this._mc.on("panstart", (e) => {
if (this.disabled) return;
this._pressed = e.pointerType;
savedPosition = this._cursorPosition;
});
this._mc.on("pancancel", () => {
if (this.disabled) return;
this._pressed = undefined;
this._cursorPosition = savedPosition;
});
this._mc.on("panmove", (e) => {
if (this.disabled) return;
this._cursorPosition = this._getPositionFromEvent(e);
this._localValue = this._getValueFromCoord(...this._cursorPosition);
fireEvent(this, "cursor-moved", { value: this._localValue });
});
this._mc.on("panend", (e) => {
if (this.disabled) return;
this._pressed = undefined;
this._cursorPosition = this._getPositionFromEvent(e);
this._localValue = this._getValueFromCoord(...this._cursorPosition);
fireEvent(this, "cursor-moved", { value: undefined });
fireEvent(this, "value-changed", { value: this._localValue });
});
this._mc.on("singletap", (e) => {
if (this.disabled) return;
this._cursorPosition = this._getPositionFromEvent(e);
this._localValue = this._getValueFromCoord(...this._cursorPosition);
fireEvent(this, "value-changed", { value: this._localValue });
});
this.addEventListener("keydown", this._handleKeyDown);
this.addEventListener("keyup", this._handleKeyUp);
}
}
private _resetPosition() {
if (this.value === undefined) {
this._cursorPosition = undefined;
this._localValue = undefined;
return;
}
const [, y] = this._getCoordsFromValue(this.value);
const currentX = this._cursorPosition?.[0] ?? 0;
const x =
Math.sign(currentX) * Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX));
this._cursorPosition = [x, y];
this._localValue = this.value;
}
private _getCoordsFromValue = (temperature: number): [number, number] => {
if (this.value === this.min) {
return [0, -1];
}
if (this.value === this.max) {
return [0, 1];
}
const fraction = (temperature - this.min) / (this.max - this.min);
const y = (2 * fraction - 1) * SAFE_ZONE_FACTOR;
return [0, y];
};
private _getValueFromCoord = (_x: number, y: number): number => {
const fraction = (y / SAFE_ZONE_FACTOR + 1) / 2;
const temperature = Math.max(
Math.min(this.min + fraction * (this.max - this.min), this.max),
this.min
);
return Math.round(temperature);
};
private _getPositionFromEvent = (e: HammerInput): [number, number] => {
const x = e.center.x;
const y = e.center.y;
const boundingRect = e.target.getBoundingClientRect();
const offsetX = boundingRect.left;
const offsetY = boundingRect.top;
const maxX = e.target.clientWidth;
const maxY = e.target.clientHeight;
const _x = (2 * (x - offsetX)) / maxX - 1;
const _y = (2 * (y - offsetY)) / maxY - 1;
const [r, phi] = xy2polar(_x, _y);
const [__x, __y] = polar2xy(Math.min(1, r), phi);
return [__x, __y];
};
private _destroyListeners() {
if (this._mc) {
this._mc.destroy();
this._mc = undefined;
}
this.removeEventListener("keydown", this._handleKeyDown);
this.removeEventListener("keyup", this._handleKeyDown);
}
_handleKeyDown(e: KeyboardEvent) {
if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault();
const step = 1;
const tenPercentStep = Math.max(step, (this.max - this.min) / 10);
const currentValue =
this._localValue ?? Math.round((this.max + this.min) / 2);
switch (e.code) {
case "ArrowRight":
case "ArrowUp":
this._localValue = Math.round(Math.min(currentValue + step, this.max));
break;
case "ArrowLeft":
case "ArrowDown":
this._localValue = Math.round(Math.max(currentValue - step, this.min));
break;
case "PageUp":
this._localValue = Math.round(
Math.min(currentValue + tenPercentStep, this.max)
);
break;
case "PageDown":
this._localValue = Math.round(
Math.max(currentValue - tenPercentStep, this.min)
);
break;
case "Home":
this._localValue = this.min;
break;
case "End":
this._localValue = this.max;
break;
}
if (this._localValue != null) {
const [_, y] = this._getCoordsFromValue(this._localValue);
const currentX = this._cursorPosition?.[0] ?? 0;
const x =
Math.sign(currentX) *
Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX));
this._cursorPosition = [x, y];
fireEvent(this, "cursor-moved", { value: this._localValue });
}
}
_handleKeyUp(e: KeyboardEvent) {
if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault();
this.value = this._localValue;
fireEvent(this, "value-changed", { value: this._localValue });
}
render() {
const size = this.renderSize || 400;
const canvasSize = size * window.devicePixelRatio;
const rgb = temperature2rgb(
this._localValue ?? Math.round((this.max + this.min) / 2)
);
const [x, y] = this._cursorPosition ?? [0, 0];
const cx = ((x + 1) * size) / 2;
const cy = ((y + 1) * size) / 2;
const markerPosition = `${cx}px, ${cy}px`;
const markerScale = this._pressed
? this._pressed === "touch"
? "2.5"
: "1.5"
: "1";
const markerOffset =
this._pressed === "touch" ? `0px, -${size / 16}px` : "0px, 0px";
return html`
<div class="container ${classMap({ pressed: Boolean(this._pressed) })}">
<canvas id="canvas" .width=${canvasSize} .height=${canvasSize}></canvas>
<svg
id="interaction"
viewBox="0 0 ${size} ${size}"
overflow="visible"
aria-hidden="true"
>
<defs>${this.renderSVGFilter()}</defs>
<g
style=${styleMap({
fill: rgb2hex(rgb),
transform: `translate(${markerPosition})`,
})}
class="cursor"
>
<circle
cx="0"
cy="0"
r="16"
style=${styleMap({
fill: rgb2hex(rgb),
transform: `translate(${markerOffset}) scale(${markerScale})`,
visibility: this._cursorPosition ? undefined : "hidden",
})}
></circle>
</g>
</svg>
</div>
`;
}
renderSVGFilter() {
return svg`
<filter
id="marker-shadow"
x="-50%"
y="-50%"
width="200%"
height="200%"
filterUnits="objectBoundingBox"
>
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.3" flood-color="rgba(0, 0, 0, 1)"/>
<feDropShadow dx="0" dy="1" stdDeviation="3" flood-opacity="0.15" flood-color="rgba(0, 0, 0, 1)"/>
</filter>
`;
}
static get styles() {
return css`
:host {
display: block;
outline: none;
}
.container {
position: relative;
width: 100%;
height: 100%;
display: flex;
}
canvas {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 50%;
transition: box-shadow 180ms ease-in-out;
cursor: pointer;
}
:host(:focus-visible) canvas {
box-shadow: 0 0 0 2px rgb(255, 160, 0);
}
svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
circle {
fill: black;
stroke: white;
stroke-width: 2;
filter: url(#marker-shadow);
}
.container:not(.pressed) circle {
transition:
transform 100ms ease-in-out,
fill 100ms ease-in-out;
}
.container:not(.pressed) .cursor {
transition: transform 200ms ease-in-out;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-temp-color-picker": HaTempColorPicker;
}
}

View File

@@ -1,6 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/number/format_number";
import LocalizeMixin from "../mixins/localize-mixin";
@@ -83,7 +84,12 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
}
_localizeState(stateObj) {
return this.hass.formatEntityState(stateObj);
return computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.entities
);
}
}
customElements.define("ha-water_heater-state", HaWaterHeaterState);

View File

@@ -0,0 +1,15 @@
import { HomeAssistant } from "../types";
export const createLanguageListEl = (hass: HomeAssistant) => {
const list = document.createElement("datalist");
list.id = "languages";
for (const [language, metadata] of Object.entries(
hass.translationMetadata.translations
)) {
const option = document.createElement("option");
option.value = language;
option.innerText = metadata.nativeName;
list.appendChild(option);
}
return list;
};

View File

@@ -61,18 +61,7 @@ export const createAuthForUser = async (
password,
});
export const changePassword = (
hass: HomeAssistant,
current_password: string,
new_password: string
) =>
hass.callWS({
type: "config/auth_provider/homeassistant/change_password",
current_password,
new_password,
});
export const adminChangePassword = (
export const adminChangePassword = async (
hass: HomeAssistant,
userId: string,
password: string
@@ -82,8 +71,3 @@ export const adminChangePassword = (
user_id: userId,
password,
});
export const deleteAllRefreshTokens = (hass: HomeAssistant) =>
hass.callWS({
type: "auth/delete_all_refresh_tokens",
});

View File

@@ -238,13 +238,11 @@ export interface ZoneCondition extends BaseCondition {
zone: string;
}
type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
export interface TimeCondition extends BaseCondition {
condition: "time";
after?: string;
before?: string;
weekday?: Weekday | Weekday[];
weekday?: string | string[];
}
export interface TemplateCondition extends BaseCondition {

View File

@@ -6,7 +6,11 @@ import {
formatTimeWithSeconds,
} from "../common/datetime/format_time";
import secondsToDuration from "../common/datetime/seconds_to_duration";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import {
computeAttributeNameDisplay,
computeAttributeValueDisplay,
} from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateName } from "../common/entity/compute_state_name";
import "../resources/intl-polyfill";
import type { HomeAssistant } from "../types";
@@ -231,14 +235,23 @@ const tryDescribeTrigger = (
for (const state of trigger.from.values()) {
from.push(
trigger.attribute
? hass
.formatEntityAttributeValue(
stateObj,
trigger.attribute,
state
)
.toString()
: hass.formatEntityState(stateObj, state)
? computeAttributeValueDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
trigger.attribute,
state
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
state
)
);
}
if (from.length !== 0) {
@@ -248,16 +261,23 @@ const tryDescribeTrigger = (
} else {
base += ` from ${
trigger.attribute
? hass
.formatEntityAttributeValue(
stateObj,
trigger.attribute,
trigger.from
)
.toString()
: hass
.formatEntityState(stateObj, trigger.from.toString())
.toString()
? computeAttributeValueDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
trigger.attribute,
trigger.from
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
trigger.from.toString()
).toString()
}`;
}
}
@@ -272,14 +292,23 @@ const tryDescribeTrigger = (
for (const state of trigger.to.values()) {
to.push(
trigger.attribute
? hass
.formatEntityAttributeValue(
stateObj,
trigger.attribute,
state
)
.toString()
: hass.formatEntityState(stateObj, state).toString()
? computeAttributeValueDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
trigger.attribute,
state
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
state
).toString()
);
}
if (to.length !== 0) {
@@ -289,14 +318,23 @@ const tryDescribeTrigger = (
} else {
base += ` to ${
trigger.attribute
? hass
.formatEntityAttributeValue(
stateObj,
trigger.attribute,
trigger.to
)
.toString()
: hass.formatEntityState(stateObj, trigger.to.toString())
? computeAttributeValueDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
trigger.attribute,
trigger.to
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
trigger.to.toString()
)
}`;
}
}
@@ -784,27 +822,45 @@ const tryDescribeCondition = (
for (const state of condition.state.values()) {
states.push(
condition.attribute
? hass
.formatEntityAttributeValue(
stateObj,
condition.attribute,
state
)
.toString()
: hass.formatEntityState(stateObj, state)
? computeAttributeValueDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
condition.attribute,
state
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
state
)
);
}
} else if (condition.state !== "") {
states.push(
condition.attribute
? hass
.formatEntityAttributeValue(
stateObj,
condition.attribute,
condition.state
)
.toString()
: hass.formatEntityState(stateObj, condition.state.toString())
? computeAttributeValueDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
condition.attribute,
condition.state
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
condition.state.toString()
)
);
}

View File

@@ -1,5 +1,7 @@
import { EntityFilter } from "../common/entity/entity_filter";
import { PlaceholderContainer } from "../panels/config/automation/thingtalk/dialog-thingtalk";
import { HomeAssistant } from "../types";
import { AutomationConfig } from "./automation";
interface CloudStatusNotLoggedIn {
logged_in: false;
@@ -64,6 +66,11 @@ export interface CloudWebhook {
managed?: boolean;
}
export interface ThingTalkConversion {
config: Partial<AutomationConfig>;
placeholders: PlaceholderContainer;
}
export const cloudLogin = (
hass: HomeAssistant,
email: string,
@@ -129,6 +136,9 @@ export const disconnectCloudRemote = (hass: HomeAssistant) =>
export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) =>
hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" });
export const convertThingTalk = (hass: HomeAssistant, query: string) =>
hass.callWS<ThingTalkConversion>({ type: "cloud/thingtalk/convert", query });
export const updateCloudPref = (
hass: HomeAssistant,
prefs: {

View File

@@ -67,7 +67,7 @@ export const DOMAIN_ATTRIBUTES_UNITS: Record<string, Record<string, string>> = {
sun: {
elevation: "°",
},
vacuum: {
vaccum: {
battery_level: "%",
},
};

View File

@@ -6,7 +6,6 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types";
import { LightColor } from "./light";
import { computeDomain } from "../common/entity/compute_domain";
type entityCategory = "config" | "diagnostic";
@@ -130,29 +129,15 @@ export interface EntityRegistryEntryUpdateParams {
aliases?: string[];
}
const batteryPriorities = ["sensor", "binary_sensor"];
export const findBatteryEntity = <T extends { entity_id: string }>(
hass: HomeAssistant,
entities: T[]
): T | undefined => {
const batteryEntities = entities
.filter(
(entity) =>
hass.states[entity.entity_id] &&
hass.states[entity.entity_id].attributes.device_class === "battery" &&
batteryPriorities.includes(computeDomain(entity.entity_id))
)
.sort(
(a, b) =>
batteryPriorities.indexOf(computeDomain(a.entity_id)) -
batteryPriorities.indexOf(computeDomain(b.entity_id))
);
if (batteryEntities.length > 0) {
return batteryEntities[0];
}
return undefined;
};
): T | undefined =>
entities.find(
(entity) =>
hass.states[entity.entity_id] &&
hass.states[entity.entity_id].attributes.device_class === "battery"
);
export const findBatteryChargingEntity = <T extends { entity_id: string }>(
hass: HomeAssistant,

View File

@@ -1,25 +1,46 @@
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
import { HomeAssistant } from "../types";
interface EntitySource {
interface EntitySourceConfigEntry {
source: "config_entry";
domain: string;
custom_component: boolean;
config_entry: string;
}
export type EntitySources = Record<string, EntitySource>;
interface EntitySourcePlatformConfig {
source: "platform_config";
domain: string;
custom_component: boolean;
}
const fetchEntitySources = (hass: HomeAssistant): Promise<EntitySources> =>
hass.callWS({ type: "entity/source" });
export type EntitySources = Record<
string,
EntitySourceConfigEntry | EntitySourcePlatformConfig
>;
const fetchEntitySources = (
hass: HomeAssistant,
entity_id?: string
): Promise<EntitySources> =>
hass.callWS({
type: "entity/source",
entity_id,
});
export const fetchEntitySourcesWithCache = (
hass: HomeAssistant
hass: HomeAssistant,
entity_id?: string
): Promise<EntitySources> =>
timeCachePromiseFunc(
"_entitySources",
// cache for 30 seconds
30000,
fetchEntitySources,
// We base the cache on number of states. If number of states
// changes we force a refresh
(hass2) => Object.keys(hass2.states).length,
hass
);
entity_id
? fetchEntitySources(hass, entity_id)
: timeCachePromiseFunc(
"_entitySources",
// cache for 30 seconds
30000,
fetchEntitySources,
// We base the cache on number of states. If number of states
// changes we force a refresh
(hass2) => Object.keys(hass2.states).length,
hass
);

View File

@@ -17,11 +17,6 @@ export interface GroupEntity extends HassEntityBase {
attributes: GroupEntityAttributes;
}
export interface GroupPreview {
state: string;
attributes: Record<string, any>;
}
export const computeGroupDomain = (
stateObj: GroupEntity
): string | undefined => {
@@ -32,15 +27,35 @@ export const computeGroupDomain = (
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
};
export const subscribePreviewGroup = (
export const subscribePreviewGroupSensor = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: GroupPreview) => void
callback: (preview: {
state: string;
attributes: Record<string, any>;
}) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "group/start_preview",
type: "group/sensor/start_preview",
flow_id,
flow_type,
user_input,
});
export const subscribePreviewGroupBinarySensor = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: {
state: string;
attributes: Record<string, any>;
}) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "group/binary_sensor/start_preview",
flow_id,
flow_type,
user_input,

View File

@@ -5,12 +5,14 @@ import {
DOMAINS_WITH_DYNAMIC_PICTURE,
} from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { autoCaseNoun } from "../common/translations/auto_case_noun";
import { LocalizeFunc } from "../common/translations/localize";
import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
import { HomeAssistant } from "../types";
import { UNAVAILABLE, UNKNOWN } from "./entity";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"];
@@ -337,9 +339,14 @@ export const localizeStateMessage = (
// TODO: This is not working yet, as we don't get historic attribute values
const event_type = hass
.formatEntityAttributeValue(stateObj, "event_type")
?.toString();
const event_type = computeAttributeValueDisplay(
hass!.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
"event_type"
)?.toString();
if (!event_type) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`);
@@ -385,7 +392,16 @@ export const localizeStateMessage = (
return hass.localize(
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
"state",
stateObj ? hass.formatEntityState(stateObj, state) : state
stateObj
? computeStateDisplay(
localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
state
)
: state
);
};

View File

@@ -54,10 +54,8 @@ export type Selector =
| UiColorSelector;
export interface ActionSelector {
action: {
reorder_mode?: boolean;
nested?: boolean;
} | null;
// eslint-disable-next-line @typescript-eslint/ban-types
action: {} | null;
}
export interface AddonSelector {
@@ -100,10 +98,8 @@ export interface ColorTempSelector {
}
export interface ConditionSelector {
condition: {
reorder_mode?: boolean;
nested?: boolean;
} | null;
// eslint-disable-next-line @typescript-eslint/ban-types
condition: {} | null;
}
export interface ConversationAgentSelector {

View File

@@ -5,6 +5,7 @@ import {
} from "home-assistant-js-websocket";
import durationToSeconds from "../common/datetime/duration_to_seconds";
import secondsToDuration from "../common/datetime/seconds_to_duration";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { HomeAssistant } from "../types";
export type TimerEntity = HassEntityBase & {
@@ -89,13 +90,25 @@ export const computeDisplayTimer = (
}
if (stateObj.state === "idle" || timeRemaining === 0) {
return hass.formatEntityState(stateObj);
return computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities
);
}
let display = secondsToDuration(timeRemaining || 0);
if (stateObj.state === "paused") {
display = `${display} (${hass.formatEntityState(stateObj)})`;
display = `${display} (${computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities
)})`;
}
return display;

View File

@@ -36,9 +36,7 @@ export const enum WeatherEntityFeature {
FORECAST_TWICE_DAILY = 4,
}
export type ModernForecastType = "hourly" | "daily" | "twice_daily";
export type ForecastType = ModernForecastType | "legacy";
export type ForecastType = "legacy" | "hourly" | "daily" | "twice_daily";
interface ForecastAttribute {
temperature: number;
@@ -638,7 +636,7 @@ export const getForecast = (
export const subscribeForecast = (
hass: HomeAssistant,
entity_id: string,
forecast_type: ModernForecastType,
forecast_type: "daily" | "hourly" | "twice_daily",
callback: (forecastevent: ForecastEvent) => void
) =>
hass.connection.subscribeMessage<ForecastEvent>(callback, {
@@ -647,31 +645,15 @@ export const subscribeForecast = (
entity_id,
});
export const getSupportedForecastTypes = (
stateObj: HassEntityBase
): ModernForecastType[] => {
const supported: ModernForecastType[] = [];
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
supported.push("daily");
}
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
supported.push("twice_daily");
}
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
supported.push("hourly");
}
return supported;
};
export const getDefaultForecastType = (stateObj: HassEntityBase) => {
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
return "daily";
}
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
return "twice_daily";
}
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
return "hourly";
}
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
return "twice_daily";
}
return undefined;
};

View File

@@ -1,65 +1,29 @@
import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
export interface RenderTemplateResult {
result: string;
listeners: TemplateListeners;
}
export interface RenderTemplateError {
error: string;
level: "ERROR" | "WARNING";
}
export interface TemplateListeners {
interface TemplateListeners {
all: boolean;
domains: string[];
entities: string[];
time: boolean;
}
export type TemplatePreview = TemplatePreviewState | TemplatePreviewError;
interface TemplatePreviewState {
state: string;
attributes: Record<string, any>;
listeners: TemplateListeners;
}
interface TemplatePreviewError {
error: string;
}
export const subscribeRenderTemplate = (
conn: Connection,
onChange: (result: RenderTemplateResult | RenderTemplateError) => void,
onChange: (result: RenderTemplateResult) => void,
params: {
template: string;
entity_ids?: string | string[];
variables?: Record<string, unknown>;
timeout?: number;
strict?: boolean;
report_errors?: boolean;
}
): Promise<UnsubscribeFunc> =>
conn.subscribeMessage(
(msg: RenderTemplateResult | RenderTemplateError) => onChange(msg),
{
type: "render_template",
...params,
}
);
export const subscribePreviewTemplate = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: TemplatePreview) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "template/start_preview",
flow_id,
flow_type,
user_input,
conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), {
type: "render_template",
...params,
});

View File

@@ -404,6 +404,8 @@ export interface RequestedGrant {
clientSideAuth: boolean;
}
export const nodeStatus = ["unknown", "asleep", "awake", "dead", "alive"];
export const fetchZwaveNetworkStatus = (
hass: HomeAssistant,
device_or_entry_id: {

View File

@@ -1,6 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { isUnavailableState } from "../../../data/entity";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
@@ -20,7 +21,6 @@ class EntityPreviewRow extends LitElement {
return html`<state-badge
.hass=${this.hass}
.stateObj=${stateObj}
stateColor
></state-badge>
<div class="name" .title=${computeStateName(stateObj)}>
${computeStateName(stateObj)}
@@ -35,7 +35,13 @@ class EntityPreviewRow extends LitElement {
capitalize
></hui-timestamp-display>
`
: this.hass.formatEntityState(stateObj)}
: computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
)}
</div>`;
}

View File

@@ -2,20 +2,19 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import { GroupPreview, subscribePreviewGroup } from "../../../data/group";
import { subscribePreviewGroupBinarySensor } from "../../../data/group";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
import { debounce } from "../../../common/util/debounce";
@customElement("flow-preview-group")
class FlowPreviewGroup extends LitElement {
@customElement("flow-preview-group_binary_sensor")
class FlowPreviewGroupBinarySensor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
@property() public stepId!: string;
public stepId!: string;
@property() public flowId!: string;
@@ -35,7 +34,7 @@ class FlowPreviewGroup extends LitElement {
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._debouncedSubscribePreview();
this._subscribePreview();
}
}
@@ -46,10 +45,13 @@ class FlowPreviewGroup extends LitElement {
></entity-preview-row>`;
}
private _setPreview = (preview: GroupPreview) => {
private _setPreview = (preview: {
state: string;
attributes: Record<string, any>;
}) => {
const now = new Date().toISOString();
this._preview = {
entity_id: `${this.stepId}.flow_preview`,
entity_id: "binary_sensor.flow_preview",
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
@@ -57,10 +59,6 @@ class FlowPreviewGroup extends LitElement {
};
};
private _debouncedSubscribePreview = debounce(() => {
this._subscribePreview();
}, 250);
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
@@ -70,7 +68,7 @@ class FlowPreviewGroup extends LitElement {
return;
}
try {
this._unsub = subscribePreviewGroup(
this._unsub = subscribePreviewGroupBinarySensor(
this.hass,
this.flowId,
this.flowType,
@@ -85,6 +83,6 @@ class FlowPreviewGroup extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"flow-preview-group": FlowPreviewGroup;
"flow-preview-group_binary_sensor": FlowPreviewGroupBinarySensor;
}
}

View File

@@ -0,0 +1,92 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import { subscribePreviewGroupSensor } from "../../../data/group";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
@customElement("flow-preview-group_sensor")
class FlowPreviewGroupSensor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
public stepId!: string;
@property() public flowId!: string;
@property() public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
private _unsub?: Promise<UnsubscribeFunc>;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._subscribePreview();
}
}
protected render() {
return html`<entity-preview-row
.hass=${this.hass}
.stateObj=${this._preview}
></entity-preview-row>`;
}
private _setPreview = (preview: {
state: string;
attributes: Record<string, any>;
}) => {
const now = new Date().toISOString();
this._preview = {
entity_id: "sensor.flow_preview",
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
...preview,
};
};
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
return;
}
if (!this.stepData.type) {
this._preview = undefined;
return;
}
try {
this._unsub = subscribePreviewGroupSensor(
this.hass,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
} catch (err) {
this._preview = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-group_sensor": FlowPreviewGroupSensor;
}
}

View File

@@ -1,179 +0,0 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { debounce } from "../../../common/util/debounce";
import { FlowType } from "../../../data/data_entry_flow";
import {
TemplateListeners,
TemplatePreview,
subscribePreviewTemplate,
} from "../../../data/ws-templates";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
import { fireEvent } from "../../../common/dom/fire_event";
@customElement("flow-preview-template")
class FlowPreviewTemplate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
@property() public stepId!: string;
@property() public flowId!: string;
@property() public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
@state() private _listeners?: TemplateListeners;
@state() private _error?: string;
private _unsub?: Promise<UnsubscribeFunc>;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._debouncedSubscribePreview();
}
}
protected render() {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html`<entity-preview-row
.hass=${this.hass}
.stateObj=${this._preview}
></entity-preview-row>
${this._listeners?.time
? html`
<p>
${this.hass.localize("ui.dialogs.helper_settings.template.time")}
</p>
`
: nothing}
${!this._listeners
? nothing
: this._listeners.all
? html`
<p class="all_listeners">
${this.hass.localize(
"ui.dialogs.helper_settings.template.all_listeners"
)}
</p>
`
: this._listeners.domains.length || this._listeners.entities.length
? html`
<p>
${this.hass.localize(
"ui.dialogs.helper_settings.template.listeners"
)}
</p>
<ul>
${this._listeners.domains
.sort()
.map(
(domain) => html`
<li>
<b
>${this.hass.localize(
"ui.dialogs.helper_settings.template.domain"
)}</b
>: ${domain}
</li>
`
)}
${this._listeners.entities
.sort()
.map(
(entity_id) => html`
<li>
<b
>${this.hass.localize(
"ui.dialogs.helper_settings.template.entity"
)}</b
>: ${entity_id}
</li>
`
)}
</ul>
`
: !this._listeners.time
? html`<span class="all_listeners">
${this.hass.localize(
"ui.dialogs.helper_settings.template.no_listeners"
)}
</span>`
: nothing} `;
}
private _setPreview = (preview: TemplatePreview) => {
if ("error" in preview) {
this._error = preview.error;
this._preview = undefined;
return;
}
this._error = undefined;
this._listeners = preview.listeners;
const now = new Date().toISOString();
this._preview = {
entity_id: `${this.stepId}.flow_preview`,
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
attributes: preview.attributes,
state: preview.state,
};
};
private _debouncedSubscribePreview = debounce(() => {
this._subscribePreview();
}, 250);
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
return;
}
try {
this._unsub = subscribePreviewTemplate(
this.hass,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
await this._unsub;
fireEvent(this, "set-flow-errors", { errors: {} });
} catch (err: any) {
if (typeof err.message === "string") {
this._error = err.message;
} else {
this._error = undefined;
fireEvent(this, "set-flow-errors", err.message);
}
this._unsub = undefined;
this._preview = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-template": FlowPreviewTemplate;
}
}

View File

@@ -70,7 +70,7 @@ class StepFlowForm extends LitElement {
></ha-form>
</div>
${step.preview
? html`<div class="preview" @set-flow-errors=${this._setError}>
? html`<div class="preview">
<h3>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.preview"
@@ -107,10 +107,6 @@ class StepFlowForm extends LitElement {
`;
}
private _setError(ev: CustomEvent) {
this.step = { ...this.step, errors: ev.detail };
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0);
@@ -257,9 +253,6 @@ class StepFlowForm extends LitElement {
}
declare global {
interface HASSDomEvents {
"set-flow-errors": { errors: DataEntryFlowStepForm["errors"] };
}
interface HTMLElementTagNameMap {
"step-flow-form": StepFlowForm;
}

View File

@@ -10,7 +10,7 @@ import {
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../../../common/const";
import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color";
import { supportsFeature } from "../../../../common/entity/supports-feature";
@@ -67,7 +67,7 @@ export class HaMoreInfoClimateTemperature extends LitElement {
private get _step() {
return (
this.stateObj.attributes.target_temp_step ||
(this.hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
(this.hass.config.unit_system.temperature.indexOf("F") === -1 ? 0.5 : 1)
);
}
@@ -161,10 +161,14 @@ export class HaMoreInfoClimateTemperature extends LitElement {
const action = this.stateObj.attributes.hvac_action;
const actionLabel = this.hass.formatEntityAttributeValue(
const actionLabel = computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"hvac_action"
);
) as string;
return html`
<p class="label">
@@ -275,21 +279,15 @@ export class HaMoreInfoClimateTemperature extends LitElement {
);
}
const activeModes = this.stateObj.attributes.hvac_modes.filter(
(m) => m !== "off"
);
if (
supportsTargetTemperature &&
this._targetTemperature.value != null &&
this.stateObj.state !== UNAVAILABLE
) {
const heatCoolModes = this.stateObj.attributes.hvac_modes.filter((m) =>
["heat", "cool", "heat_cool"].includes(m)
);
const sliderMode =
SLIDER_MODES[
heatCoolModes.length === 1 && ["off", "auto"].includes(mode)
? heatCoolModes[0]
: mode
];
return html`
<div
class="container"
@@ -300,7 +298,9 @@ export class HaMoreInfoClimateTemperature extends LitElement {
>
<ha-control-circular-slider
.inactive=${!active}
.mode=${sliderMode}
.mode=${mode === "off" && activeModes.length === 1
? SLIDER_MODES[activeModes[0]]
: SLIDER_MODES[mode]}
.value=${this._targetTemperature.value}
.min=${this._min}
.max=${this._max}

View File

@@ -35,9 +35,7 @@ export class HaMoreInfoCoverPosition extends LitElement {
}
protected render(): TemplateResult {
const forcedState = this.stateObj.state === "closed" ? "open" : undefined;
const color = stateColorCss(this.stateObj, forcedState);
const color = stateColorCss(this.stateObj);
return html`
<ha-control-slider
@@ -70,6 +68,8 @@ export class HaMoreInfoCoverPosition extends LitElement {
height: 45vh;
max-height: 320px;
min-height: 200px;
/* Force inactive state to be colored for the slider */
--state-cover-inactive-color: var(--state-cover-active-color);
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-color: var(--primary-color);

View File

@@ -72,9 +72,8 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
}
protected render(): TemplateResult {
const forcedState = this.stateObj.state === "closed" ? "open" : undefined;
const color = stateColorCss(this.stateObj, forcedState);
const color = stateColorCss(this.stateObj);
const isUnavailable = this.stateObj.state === UNAVAILABLE;
return html`
<ha-control-slider
@@ -94,7 +93,7 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
"--control-slider-color": color,
"--control-slider-background": color,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
.disabled=${isUnavailable}
>
<div slot="background" class="gradient"></div>
</ha-control-slider>
@@ -107,6 +106,8 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
height: 45vh;
max-height: 320px;
min-height: 200px;
/* Force inactive state to be colored for the slider */
--state-cover-inactive-color: var(--state-cover-active-color);
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-color: var(--primary-color);

View File

@@ -2,6 +2,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../../common/entity/compute_state_display";
import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color";
import "../../../../components/ha-control-select";
@@ -11,12 +12,12 @@ import { UNAVAILABLE } from "../../../../data/entity";
import {
computeFanSpeedCount,
computeFanSpeedIcon,
FAN_SPEED_COUNT_MAX_FOR_BUTTONS,
FAN_SPEEDS,
FanEntity,
fanPercentageToSpeed,
FanSpeed,
fanSpeedToPercentage,
FAN_SPEEDS,
FAN_SPEED_COUNT_MAX_FOR_BUTTONS,
} from "../../../../data/fan";
import { HomeAssistant } from "../../../../types";
@@ -67,7 +68,14 @@ export class HaMoreInfoFanSpeed extends LitElement {
private _localizeSpeed(speed: FanSpeed) {
if (speed === "on" || speed === "off") {
return this.hass.formatEntityState(this.stateObj, speed);
return computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
speed
);
}
return (
this.hass.localize(`ui.dialogs.more_info_control.fan.speed.${speed}`) ||

View File

@@ -1,79 +0,0 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@customElement("ha-more-info-control-select-container")
export class HaMoreInfoControlSelectContainer extends LitElement {
protected render(): TemplateResult {
const classname = `items-${this.childElementCount}`;
return html`
<div class="controls">
<div
class="controls-scroll ${classMap({
[classname]: true,
multiline: this.childElementCount >= 4,
})}"
>
<slot></slot>
</div>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
.controls {
display: flex;
flex-direction: row;
justify-content: center;
}
.controls-scroll {
display: flex;
flex-direction: row;
justify-content: flex-start;
gap: 12px;
margin: auto;
overflow: auto;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
margin: 0 -24px;
padding: 0 24px;
}
.controls-scroll::-webkit-scrollbar {
display: none;
}
::slotted(*) {
min-width: 120px;
max-width: 160px;
flex: none;
}
@media all and (hover: hover),
all and (min-width: 600px) and (min-height: 501px) {
.controls-scroll {
justify-content: center;
flex-wrap: wrap;
width: 100%;
max-width: 450px;
}
.controls-scroll.items-4 {
max-width: 300px;
}
.controls-scroll.items-3 ::slotted(*) {
max-width: 140px;
}
.multiline ::slotted(*) {
width: 140px;
}
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-control-select-container": HaMoreInfoControlSelectContainer;
}
}

View File

@@ -22,6 +22,35 @@ export const moreInfoControlStyle = css`
margin-bottom: 24px;
}
.secondary-controls {
display: flex;
flex-direction: row;
justify-content: center;
}
.secondary-controls-scroll {
display: flex;
flex-direction: row;
justify-content: flex-start;
gap: 12px;
margin: auto;
overflow: auto;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
margin: 0 -24px;
padding: 0 24px;
}
.secondary-controls-scroll::-webkit-scrollbar {
display: none;
}
/* Don't use scroll on device without touch support */
@media (hover: hover) {
.secondary-controls-scroll {
justify-content: center;
flex-wrap: wrap;
}
}
.buttons {
display: flex;
align-items: center;
@@ -34,10 +63,6 @@ export const moreInfoControlStyle = css`
}
ha-attributes {
display: block;
width: 100%;
}
ha-more-info-control-select-container + ha-attributes:not([empty]) {
margin-top: 16px;
}
`;

View File

@@ -1,5 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import "../../../components/ha-absolute-time";
import "../../../components/ha-relative-time";
import { isUnavailableState } from "../../../data/entity";
@@ -18,22 +20,30 @@ export class HaMoreInfoStateHeader extends LitElement {
@state() private _absoluteTime = false;
private _localizeState(): TemplateResult | string {
private _computeStateDisplay(stateObj: HassEntity): TemplateResult | string {
if (
this.stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
!isUnavailableState(this.stateObj.state)
stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
!isUnavailableState(stateObj.state)
) {
return html`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(this.stateObj.state)}
.ts=${new Date(stateObj.state)}
format="relative"
capitalize
></hui-timestamp-display>
`;
}
return this.hass.formatEntityState(this.stateObj);
const stateDisplay = computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass!.locale,
this.hass!.config,
this.hass!.entities
);
return stateDisplay;
}
private _toggleAbsolute() {
@@ -41,7 +51,8 @@ export class HaMoreInfoStateHeader extends LitElement {
}
protected render(): TemplateResult {
const stateDisplay = this.stateOverride ?? this._localizeState();
const stateDisplay =
this.stateOverride ?? this._computeStateDisplay(this.stateObj);
return html`
<p class="state">${stateDisplay}</p>

View File

@@ -2,6 +2,7 @@ import { mdiMinus, mdiPlus } from "@mdi/js";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color";
import { clamp } from "../../../../common/number/clamp";
@@ -91,10 +92,14 @@ export class HaMoreInfoHumidifierHumidity extends LitElement {
const action = this.stateObj.attributes.action;
const actionLabel = this.hass.formatEntityAttributeValue(
const actionLabel = computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"action"
);
) as string;
return html`
<p class="label">

View File

@@ -1,4 +1,6 @@
import "@material/mwc-button";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import { mdiEyedropper } from "@mdi/js";
import {
css,
@@ -24,6 +26,7 @@ import "../../../../components/ha-hs-color-picker";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-labeled-slider";
import "../../../../components/ha-temp-color-picker";
import {
getLightCurrentModeRgbColor,
LightColor,

View File

@@ -1,31 +1,25 @@
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { rgb2hex } from "../../../../common/color/convert-color";
import {
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
temperature2rgb,
} from "../../../../common/color/convert-light-color";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stateColorCss } from "../../../../common/entity/state_color";
import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-control-slider";
import { UNAVAILABLE } from "../../../../data/entity";
import "../../../../components/ha-temp-color-picker";
import {
LightColor,
LightColorMode,
LightEntity,
} from "../../../../data/light";
import { HomeAssistant } from "../../../../types";
import {
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
} from "../../../../common/color/convert-light-color";
declare global {
interface HASSDomEvents {
@@ -34,26 +28,6 @@ declare global {
}
}
export const generateColorTemperatureGradient = (min: number, max: number) => {
const count = 10;
const gradient: [number, string][] = [];
const step = (max - min) / count;
const percentageStep = 1 / count;
for (let i = 0; i < count + 1; i++) {
const value = min + step * i;
const hex = rgb2hex(temperature2rgb(value));
gradient.push([percentageStep * i, hex]);
}
return gradient
.map(([stop, color]) => `${color} ${(stop as number) * 100}%`)
.join(", ");
};
@customElement("light-color-temp-picker")
class LightColorTempPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -72,36 +46,18 @@ class LightColorTempPicker extends LitElement {
const maxKelvin =
this.stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN;
const gradient = this._generateTemperatureGradient(minKelvin!, maxKelvin);
const color = stateColorCss(this.stateObj);
return html`
<ha-control-slider
inverted
vertical
.value=${this._ctPickerValue}
<ha-temp-color-picker
@value-changed=${this._ctColorChanged}
@cursor-moved=${this._ctColorCursorMoved}
.min=${minKelvin}
.max=${maxKelvin}
mode="cursor"
@value-changed=${this._ctColorChanged}
@slider-moved=${this._ctColorCursorMoved}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.light.color_temp"
)}
style=${styleMap({
"--control-slider-color": color,
"--gradient": gradient,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
.value=${this._ctPickerValue}
>
</ha-control-slider>
</ha-temp-color-picker>
`;
}
private _generateTemperatureGradient = memoizeOne(
(min: number, max: number) => generateColorTemperatureGradient(min, max)
);
public _updateSliderValues() {
const stateObj = this.stateObj;
@@ -182,18 +138,10 @@ class LightColorTempPicker extends LitElement {
flex-direction: column;
}
ha-control-slider {
ha-temp-color-picker {
height: 45vh;
max-height: 320px;
min-height: 200px;
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-color: var(--primary-color);
--control-slider-background: -webkit-linear-gradient(
top,
var(--gradient)
);
--control-slider-background-opacity: 1;
}
`,
];

View File

@@ -9,7 +9,6 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../../../common/const";
import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color";
import { supportsFeature } from "../../../../common/entity/supports-feature";
@@ -45,7 +44,7 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
private get _step() {
return (
this.stateObj.attributes.target_temp_step ||
(this.hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
(this.hass.config.unit_system.temperature.indexOf("F") === -1 ? 0.5 : 1)
);
}

View File

@@ -38,7 +38,6 @@ import { haOscillating } from "../../../data/icons/haOscillating";
import { HomeAssistant } from "../../../types";
import "../components/climate/ha-more-info-climate-humidity";
import "../components/climate/ha-more-info-climate-temperature";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
type MainControl = "temperature" | "humidity";
@@ -142,7 +141,7 @@ class MoreInfoClimate extends LitElement {
.selected=${this._mainControl === "temperature"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.climate.temperature"
"ui.dialogs.more_info_control.light.color"
)}
.control=${"temperature"}
@click=${this._setMainControl}
@@ -153,7 +152,7 @@ class MoreInfoClimate extends LitElement {
.selected=${this._mainControl === "humidity"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.climate.humidity"
"ui.dialogs.more_info_control.light.color_temp"
)}
.control=${"humidity"}
@click=${this._setMainControl}
@@ -164,132 +163,140 @@ class MoreInfoClimate extends LitElement {
`
: nothing}
</div>
<ha-more-info-control-select-container>
<ha-control-select-menu
.label=${this.hass.localize("ui.card.climate.mode")}
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiThermostat}></ha-svg-icon>
${stateObj.attributes.hvac_modes
.concat()
.sort(compareClimateHvacModes)
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeHvacModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
<div class="secondary-controls">
<div class="secondary-controls-scroll">
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"hvac_mode"
)}
</ha-control-select-menu>
${supportPresetMode && stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"preset_mode"
)}
.value=${stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
${stateObj.attributes.preset_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computePresetModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportFanMode && stateObj.attributes.fan_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"fan_mode"
)}
.value=${stateObj.attributes.fan_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleFanModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
${stateObj.attributes.fan_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeFanModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportSwingMode && stateObj.attributes.swing_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"swing_mode"
)}
.value=${stateObj.attributes.swing_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleSwingmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${haOscillating}></ha-svg-icon>
${stateObj.attributes.swing_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeSwingModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</ha-more-info-control-select-container>
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiThermostat}></ha-svg-icon>
${stateObj.attributes.hvac_modes
.concat()
.sort(compareClimateHvacModes)
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeHvacModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
)}
</ha-control-select-menu>
${supportPresetMode && stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"preset_mode"
)}
.value=${stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
${stateObj.attributes.preset_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computePresetModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportFanMode && stateObj.attributes.fan_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"fan_mode"
)}
.value=${stateObj.attributes.fan_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleFanModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
${stateObj.attributes.fan_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeFanModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportSwingMode && stateObj.attributes.swing_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"swing_mode"
)}
.value=${stateObj.attributes.swing_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleSwingmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${haOscillating}></ha-svg-icon>
${stateObj.attributes.swing_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeSwingModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</div>
</div>
`;
}

View File

@@ -1,21 +1,22 @@
import { mdiMenu, mdiSwapVertical } from "@mdi/js";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attributes";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
import {
computeCoverPositionStateDisplay,
CoverEntity,
CoverEntityFeature,
computeCoverPositionStateDisplay,
} from "../../../data/cover";
import type { HomeAssistant } from "../../../types";
import "../components/cover/ha-more-info-cover-buttons";
@@ -82,8 +83,12 @@ class MoreInfoCover extends LitElement {
const forcedState =
liveValue != null ? (liveValue ? "open" : "closed") : undefined;
const stateDisplay = this.hass.formatEntityState(
const stateDisplay = computeStateDisplay(
this.hass.localize,
this.stateObj!,
this.hass.locale,
this.hass.config,
this.hass.entities,
forcedState
);

View File

@@ -27,7 +27,6 @@ import { haOscillating } from "../../../data/icons/haOscillating";
import { haOscillatingOff } from "../../../data/icons/haOscillatingOff";
import type { HomeAssistant } from "../../../types";
import "../components/fan/ha-more-info-fan-speed";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import "../components/ha-more-info-state-header";
import "../components/ha-more-info-toggle";
@@ -86,7 +85,7 @@ class MoreInfoFan extends LitElement {
}
_handleOscillating(ev) {
const newVal = ev.target.value === "true";
const newVal = ev.target.value === "on";
this.hass.callService("fan", "oscillate", {
entity_id: this.stateObj!.entity_id,
@@ -192,122 +191,117 @@ class MoreInfoFan extends LitElement {
`
: nothing}
</div>
<ha-more-info-control-select-container>
${supportsPresetMode && this.stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"preset_mode"
)}
.value=${this.stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetMode}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
${this.stateObj.attributes.preset_modes?.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportsDirection
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"direction"
)}
.value=${this.stateObj.attributes.direction}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleDirection}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiRotateLeft}></ha-svg-icon>
<ha-list-item value="forward" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiRotateRight}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
<div class="secondary-controls">
<div class="secondary-controls-scroll">
${supportsPresetMode && this.stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"direction",
"forward"
"preset_mode"
)}
</ha-list-item>
<ha-list-item value="reverse" graphic="icon">
.value=${this.stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetMode}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="graphic"
.path=${mdiRotateLeft}
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
"reverse"
${this.stateObj.attributes.preset_modes?.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
${supportsOscillate
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"oscillating"
)}
.value=${this.stateObj.attributes.oscillating
? "true"
: "false"}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOscillating}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="icon"
.path=${haOscillatingOff}
></ha-svg-icon>
<ha-list-item value="true" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${haOscillating}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
</ha-control-select-menu>
`
: nothing}
${supportsDirection
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"oscillating",
true
"direction"
)}
</ha-list-item>
<ha-list-item value="false" graphic="icon">
.value=${this.stateObj.attributes.direction}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleDirection}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiRotateLeft}></ha-svg-icon>
<ha-list-item value="forward" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiRotateRight}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
"forward"
)}
</ha-list-item>
<ha-list-item value="reverse" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiRotateLeft}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
"reverse"
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
${supportsOscillate
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"oscillating"
)}
.value=${this.stateObj.attributes.oscillating ? "on" : "off"}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOscillating}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="graphic"
slot="icon"
.path=${haOscillatingOff}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"oscillating",
false
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
</ha-more-info-control-select-container>
<ha-list-item value="on" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${haOscillating}
></ha-svg-icon>
${this.hass.localize("state.default.on")}
</ha-list-item>
<ha-list-item value="off" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${haOscillatingOff}
></ha-svg-icon>
${this.hass.localize("state.default.off")}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
</div>
</div>
`;
}

View File

@@ -10,6 +10,11 @@ import {
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import {
computeAttributeNameDisplay,
computeAttributeValueDisplay,
} from "../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-list-item";
@@ -20,7 +25,6 @@ import {
computeHumidiferModeIcon,
} from "../../../data/humidifier";
import { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import "../components/humidifier/ha-more-info-humidifier-humidity";
@@ -53,21 +57,26 @@ class MoreInfoHumidifier extends LitElement {
HumidifierEntityFeature.MODES
);
const currentHumidity = this.stateObj.attributes.current_humidity as number;
return html`
<div class="current">
${this.stateObj.attributes.current_humidity != null
${currentHumidity != null
? html`
<div>
<p class="label">
${this.hass.formatEntityAttributeName(
${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"current_humidity"
)}
</p>
<p class="value">
${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
"current_humidity",
currentHumidity
)}
</p>
</div>
@@ -82,56 +91,79 @@ class MoreInfoHumidifier extends LitElement {
></ha-more-info-humidifier-humidity>
</div>
<ha-more-info-control-select-container>
<ha-control-select-menu
.label=${this.hass.localize("ui.card.humidifier.state")}
.value=${this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleStateChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
<ha-list-item value="off">
${this.hass.formatEntityState(this.stateObj, "off")}
</ha-list-item>
<ha-list-item value="on">
${this.hass.formatEntityState(this.stateObj, "on")}
</ha-list-item>
</ha-control-select-menu>
<div class="secondary-controls">
<div class="secondary-controls-scroll">
<ha-control-select-menu
.label=${this.hass.localize("ui.card.humidifier.state")}
.value=${this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleStateChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
<ha-list-item value="off">
${computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"off"
)}
</ha-list-item>
<ha-list-item value="on">
${computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"on"
)}
</ha-list-item>
</ha-control-select-menu>
${supportModes
? html`
<ha-control-select-menu
.label=${hass.localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
${stateObj.attributes.available_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeHumidiferModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj!,
"mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</ha-more-info-control-select-container>
${supportModes
? html`
<ha-control-select-menu
.label=${hass.localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
${stateObj.attributes.available_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeHumidiferModeIcon(mode)}
></ha-svg-icon>
${computeAttributeValueDisplay(
hass.localize,
stateObj!,
hass.locale,
hass.config,
hass.entities,
"mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</div>
</div>
`;
}

View File

@@ -2,6 +2,7 @@ import { mdiHomeImportOutline, mdiPause, mdiPlay } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
@@ -73,7 +74,15 @@ class MoreInfoLawnMower extends LitElement {
)}:
</span>
<span>
<strong>${this.hass.formatEntityState(stateObj)}</strong>
<strong>
${computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
)}
</strong>
</span>
</div>
${this._renderBattery()}

View File

@@ -36,7 +36,6 @@ import {
lightSupportsFavoriteColors,
} from "../../../data/light";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import "../components/ha-more-info-state-header";
import "../components/ha-more-info-toggle";
@@ -287,37 +286,39 @@ class MoreInfoLight extends LitElement {
: nothing}
</div>
<div>
<ha-more-info-control-select-container>
${supportsEffects && this.stateObj.attributes.effect_list
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"effect"
)}
.value=${this.stateObj.attributes.effect}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleEffect}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
${this.stateObj.attributes.effect_list?.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"effect",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</ha-more-info-control-select-container>
<div class="secondary-controls">
<div class="secondary-controls-scroll">
${supportsEffects && this.stateObj.attributes.effect_list
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"effect"
)}
.value=${this.stateObj.attributes.effect}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleEffect}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
${this.stateObj.attributes.effect_list?.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"effect",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</div>
</div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}

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