Compare commits

...

66 Commits

Author SHA1 Message Date
Paulus Schoutsen 3b69f9cc8d Update Z-Wave data type
Data added in https://github.com/home-assistant/core/pull/117288/files
2024-05-12 22:24:11 -04:00
renovate[bot] 6d3940db1e Update dependency glob to v10.3.14 (#20784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-12 19:08:52 +02:00
renovate[bot] 20d174431d Update dependency chai to v5.1.1 (#20781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-12 13:06:07 +02:00
renovate[bot] 1900710e06 Update Yarn to v4.2.2 (#20778) 2024-05-11 15:41:07 -04:00
renovate[bot] ed86a48e1c Update dependency sinon to v17.0.2 (#20772)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-10 15:08:14 -04:00
renovate[bot] d2bdb52926 Update vaadinWebComponents monorepo to v24.3.12 (#20761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-09 14:21:35 +02:00
G Johansson 9c57c9f151 Support open / opening state in LockEntity (#19944) 2024-05-08 21:01:57 +02:00
karwosts 9e9cb15a42 Minor improvements to service call descriptions. (#20733)
* Minor improvements to service call descriptions.
2024-05-08 18:04:38 +02:00
renovate[bot] 6421a9443d Update dependency intl-messageformat to v10.5.12 (#20755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-08 12:50:05 +02:00
Paulus Schoutsen f2b43ddad8 Allow adding card from history panel (#19582)
* Allow adding card from history panel

* Better empty entities check
2024-05-07 17:11:27 +02:00
Yosi Levy e55b59d9b7 Logical property style fixes (#20752)
logical prop fixes
2024-05-07 15:35:34 +02:00
Paul Bottein 4a77359a06 Use Material 3 ripple (#20751)
* Use material web ripple component

* Improve button style

* Use css animation instead of ripple for action

* Use ha ripple in all components

* Remove unused label
2024-05-07 15:30:45 +02:00
renovate[bot] 505d7b6ddb Update dependency tar to v7.1.0 (#20748) 2024-05-07 08:23:16 +02:00
Steve Repsher 79cdc43699 Enhance webpack transform async plugin to use babel runtime (with fix) (#20745) 2024-05-06 18:06:21 -04:00
renovate[bot] 8ff9823cd7 Update dependency @octokit/rest to v20.1.1 (#20746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-06 20:42:37 +02:00
Paul Bottein 3488c60818 Fix tile card margin on old devices (#20742) 2024-05-06 19:49:52 +02:00
Yosi Levy 43a422cdca Font updates in new filters (#20482)
* Style changes

* Fixes
2024-05-06 15:39:36 +02:00
Douwe 11f2bef05c Add header text align theme variable to stack cards (#20563)
* Update hui-stack-card.ts

Added variable

* Update hui-stack-card.ts

Updated the variable, so that it would not be in line with the rest of the variables. In this way, the variable only works for hui-stack titles.

* Update hui-stack-card.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-05-06 13:35:36 +00:00
karwosts ff9f331287 Expand createDomains to more selectors (#20714)
Expand createDomains to more pickers
2024-05-06 15:26:13 +02:00
Steve Repsher cdf64ccdaa Refactor translation merges to use native transform stream (#20666) 2024-05-06 15:17:01 +02:00
Simon Lamon 8b220acca2 Show ungrouped group when there are results (#20716) 2024-05-06 15:07:22 +02:00
Paul Bottein 8fdb7fa1d5 Update newsletter link (#20740)
* Update newsletter link

* Update src/panels/config/dashboard/ha-config-dashboard.ts

* Update src/onboarding/dialogs/community-dialog.ts
2024-05-06 14:57:51 +02:00
Paulus Schoutsen 008c842431 Fix showing options button on conversation agent picker (#20736) 2024-05-06 12:24:22 +02:00
Paul Bottein bc41de0d9c Revert usage of babel runtime for legacy bundle (#20741)
Revert usage of babel runtine for legacy bundle
2024-05-06 12:12:19 +02:00
renovate[bot] 7310c9cf6d Update Yarn to v4.2.1 (#20735) 2024-05-05 21:49:14 -04:00
Steve Repsher 84b436c08e Fix self-injection for custom polyfills (#20718)
* Fix self-injection for custom polyfills

* Update build-scripts/bundle.cjs

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-05-03 16:37:40 -04:00
renovate[bot] 1925a47bdc Update dependency eslint-plugin-unused-imports to v3.2.0 (#20715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-03 16:05:13 +00:00
renovate[bot] 438a426458 Update babel monorepo to v7.24.5 (#20707) 2024-05-02 21:25:29 -04:00
karwosts f923deb71d Energy CSV download should not require admin (#20704) 2024-05-02 21:08:54 +02:00
renovate[bot] e79bc71ab7 Update typescript-eslint monorepo to v7.8.0 (#20703)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-02 21:04:03 +02:00
karwosts 11b0990d2b Add spacer for FAB under the zone list (#20706) 2024-05-02 21:02:57 +02:00
Simon Lamon 870cb0c65f Always save custom display name in energy dashboard when hitting Enter (#20702)
Change to Input event
2024-05-02 20:03:36 +02:00
Paul Bottein deda2009f8 Remove alarm modes list when adding a alarm modes card feature (#20688) 2024-05-02 19:22:43 +02:00
renovate[bot] b2797ab8da Update dependency gulp to v5 (#20601)
* Update dependency gulp to v5

* Fix premature cloasing of hash stream

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2024-05-01 15:13:28 +02:00
renovate[bot] 644dcb0381 Update dependency systemjs to v6.15.1 (#20682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-01 13:12:18 +02:00
Bram Kragten c65f4f7a6e Revert "Remove strict connections" (#20685)
Revert "Remove strict connections (#20662)"

This reverts commit 1df92fa863.
2024-05-01 12:53:01 +02:00
Bram Kragten 68a79490dc Bumped version to 20240501.0 2024-05-01 12:03:26 +02:00
Paul Bottein 6febe8552e Allow to reorder alarm modes in card feature (#20684) 2024-05-01 11:55:06 +02:00
Bram Kragten f611f23f6f Make sure lovelace theme background is set on it's container (#20683) 2024-05-01 11:24:40 +02:00
Bram Kragten 627e06663b Bumped version to 20240430.0 2024-04-30 23:44:32 +02:00
Paul Bottein ab01633069 Fix ha settings row display in more info settings (#20680) 2024-04-30 21:12:53 +00:00
Bram Kragten 17dcc90638 Update entity status filter and grouping (#20679) 2024-04-30 23:04:48 +02:00
Paul Bottein d0df029ff1 Update check update icon and add toast when checking update (#20677)
* Update check update icon

* Add toast when checking for update
2024-04-30 19:21:30 +00:00
Paul Bottein 86a7e69812 Allow to reorder and filter options in select options card feature (#20675) 2024-04-30 21:14:49 +02:00
Adam Kapos af9417f2a6 Add theme support for dialog surface background (#20653)
* Add theme support for dialog surface background

* Change from review

* Change from review

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Run prettier

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-04-30 21:12:36 +02:00
Paul Bottein 7120ad99b9 Add customize mode option to card features with modes (#20670)
* Add customize mode options to card features with modes

* Better type

* Fix water heater and humidifier

* Clean schema
2024-04-30 18:38:51 +02:00
Adam Kapos 334c245b65 Fix visual differences between regular and energy dashboards (#20654)
* Fix visual differences between regular and energy dashboards

* Order padding properties the same way between energy and lovelace

* Change from code review
2024-04-30 15:07:54 +02:00
Nicooow bcb72d83b8 Fix an inconsistency in dark mode (#20671)
* add app-theme-color var

* Fix Prettier format

* Fix regression on default dark theme

* prevent duplicate calculation
2024-04-30 12:03:19 +00:00
karwosts c99e0e846b More config/entities status filters (#20638) 2024-04-30 12:32:32 +02:00
J. Nick Koston ec3f63e8a3 Fallback to raw config entry reason if localize returns an empty string (#20668)
Show config entry reason if localize returns an empty string
2024-04-30 12:25:45 +02:00
karwosts 1bc33a30ec Display version info for custom integrations (#20652)
* Display version info for custom integrations

* no width
2024-04-30 12:23:20 +02:00
krazos 8cca233b7c Update unlock icon for tile card lock features (#20667)
Update unlock icon for tile card lock features so it's easier to see the difference between lock and unlock buttons
2024-04-29 20:53:33 +02:00
karwosts a78608bfb4 Reorderable card-feature modes (#20647)
* Reorderable card-feature modes

* unused var in getStubConfig
2024-04-29 17:48:01 +02:00
Bram Kragten 1a797b3415 Bumped version to 20240429.0 2024-04-29 17:36:46 +02:00
Bram Kragten 2b27a4da2b Show abort reason when no translation (#20664) 2024-04-29 17:35:30 +02:00
Bram Kragten 1df92fa863 Remove strict connections (#20662)
* Remove strict connections

* Update cloud-remote-pref.ts
2024-04-29 16:42:23 +02:00
Bram Kragten cdde85315a fix list items cloud account (#20663) 2024-04-29 14:26:14 +00:00
Paul Bottein dc67f9faf4 Fix cloud page design on mobile (#20661) 2024-04-29 16:03:02 +02:00
dependabot[bot] 3ad1be50a2 Bump actions/upload-artifact from 4.3.2 to 4.3.3 (#20658)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 12:38:02 +02:00
dependabot[bot] 8aadfe7d28 Bump actions/checkout from 4.1.3 to 4.1.4 (#20659)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 12:33:17 +02:00
renovate[bot] cff54b73a4 Update dependency @lokalise/node-api to v12.4.1 (#20643)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-27 20:01:32 +02:00
Philip Allgaier b54cfeb0c0 Hide "Browse Media" button for unavailable media players (#20629)
* Hide "Browse Media" button for unavailable media players
2024-04-27 14:36:42 +02:00
renovate[bot] cefe612b11 Update dependency @octokit/plugin-retry to v7.1.1 (#20641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-27 11:16:55 +02:00
renovate[bot] 4bc874b497 Update workbox monorepo to v7.1.0 (#20642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-27 11:16:43 +02:00
Philip Allgaier f3abaa8e02 Align lawn-mower and vacuum more-info layouts (#20632) 2024-04-26 14:07:38 +02:00
Philip Allgaier 21a563fe98 Add details for offset format to sun trigger (#20625)
Add details for offset to sun trigger
2024-04-26 14:05:04 +02:00
96 changed files with 2657 additions and 3768 deletions
+2 -2
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@v4.1.3
uses: actions/checkout@v4.1.4
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@v4.1.3
uses: actions/checkout@v4.1.4
with:
ref: master
+6 -6
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.3
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.3
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.3
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.2
uses: actions/upload-artifact@v4.3.3
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.3
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.2
uses: actions/upload-artifact@v4.3.3
with:
name: supervisor-bundle-stats
path: build/stats/*.json
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.3
uses: actions/checkout@v4.1.4
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
+2 -2
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@v4.1.3
uses: actions/checkout@v4.1.4
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@v4.1.3
uses: actions/checkout@v4.1.4
with:
ref: master
+1 -1
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@v4.1.3
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.0.2
+1 -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@v4.1.3
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.0.2
+3 -3
View File
@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.3
uses: actions/checkout@v4.1.4
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.3.2
uses: actions/upload-artifact@v4.3.3
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.3.2
uses: actions/upload-artifact@v4.3.3
with:
name: translations
path: translations.tar.gz
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.3
uses: actions/checkout@v4.1.4
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.3
uses: actions/checkout@v4.1.4
- name: Upload Translations
run: |
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.1.1.cjs
yarnPath: .yarn/releases/yarn-4.2.2.cjs
+14 -11
View File
@@ -3,6 +3,8 @@ const env = require("./env.cjs");
const paths = require("./paths.cjs");
const { dependencies } = require("../package.json");
const BABEL_PLUGINS = path.join(__dirname, "babel-plugins");
// GitHub base URL to use for production source maps
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
module.exports.sourceMapURL = () => {
@@ -100,22 +102,12 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
],
plugins: [
[
path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/inline-constants-plugin.cjs"
),
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"),
{
modules: ["@mdi/js"],
ignoreModuleNotFound: true,
},
],
[
path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/custom-polyfill-plugin.js"
),
{ method: "usage-global" },
],
// Minify template literals for production
isProdBuild && [
"template-html-minifier",
@@ -153,6 +145,17 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
],
sourceMaps: !isTestBuild,
overrides: [
{
// Add plugin to inject various polyfills, excluding the polyfills
// themselves to prevent self- injection.
plugins: [
[
path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"),
{ method: "usage-global" },
],
],
exclude: /\/node_modules\/(?:unfetch|proxy-polyfill)\//,
},
{
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
+45 -17
View File
@@ -1,12 +1,14 @@
/* eslint-disable max-classes-per-file */
import { deleteAsync } from "del";
import { glob } from "glob";
import gulp from "gulp";
import merge from "gulp-merge-json";
import rename from "gulp-rename";
import merge from "lodash.merge";
import { createHash } from "node:crypto";
import { mkdir, readFile } from "node:fs/promises";
import { basename, join } from "node:path";
import { Transform } from "node:stream";
import { PassThrough, Transform } from "node:stream";
import { finished } from "node:stream/promises";
import env from "../env.cjs";
import paths from "../paths.cjs";
@@ -54,6 +56,39 @@ class CustomJSON extends Transform {
}
}
// Transform stream to merge Vinyl JSON files (buffer mode only).
class MergeJSON extends Transform {
_objects = [];
constructor(stem, startObj = {}, reviver = null) {
super({ objectMode: true, allowHalfOpen: false });
this._stem = stem;
this._startObj = structuredClone(startObj);
this._reviver = reviver;
}
async _transform(file, _, callback) {
try {
this._objects.push(JSON.parse(file.contents.toString(), this._reviver));
if (!this._outFile) this._outFile = file.clone({ contents: false });
callback(null);
} catch (err) {
callback(err);
}
}
async _flush(callback) {
try {
const mergedObj = merge(this._startObj, ...this._objects);
this._outFile.contents = Buffer.from(JSON.stringify(mergedObj));
this._outFile.stem = this._stem;
callback(null, this._outFile);
} catch (err) {
callback(err);
}
}
}
// Utility to flatten object keys to single level using separator
const flatten = (data, prefix = "", sep = ".") => {
const output = {};
@@ -131,12 +166,7 @@ const createMasterTranslation = () =>
gulp
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
.pipe(new CustomJSON(lokaliseTransform))
.pipe(
merge({
fileName: "en.json",
jsonSpace: undefined,
})
)
.pipe(new MergeJSON("en"))
.pipe(gulp.dest(workDir));
const FRAGMENTS = ["base"];
@@ -213,7 +243,10 @@ const createTranslations = async () => {
// TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated
// than a base translation + region.
gulp.src(`${workDir}/en.json`).pipe(hashStream, { end: false });
gulp
.src(`${workDir}/en.json`)
.pipe(new PassThrough({ objectMode: true }))
.pipe(hashStream, { end: false });
const mergesFinished = [];
for (const translationFile of translationFiles) {
const locale = basename(translationFile, ".json");
@@ -230,14 +263,9 @@ const createTranslations = async () => {
}
}
}
const mergeStream = gulp.src(mergeFiles, { allowEmpty: true }).pipe(
merge({
fileName: `${locale}.json`,
startObj: enMaster,
jsonReviver: emptyReviver,
jsonSpace: undefined,
})
);
const mergeStream = gulp
.src(mergeFiles, { allowEmpty: true })
.pipe(new MergeJSON(locale, enMaster, emptyReviver));
mergesFinished.push(finished(mergeStream));
mergeStream.pipe(hashStream, { end: false });
}
+8 -8
View File
@@ -1,19 +1,19 @@
import { mdiStorePlus, mdiUpdate } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { mdiRefresh, mdiStorePlus } from "@mdi/js";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-fab";
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addons";
import "../../../src/layouts/hass-subpage";
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@@ -43,7 +43,7 @@ class HassioDashboard extends LitElement {
<ha-icon-button
slot="toolbar-icon"
@click=${this._handleCheckUpdates}
.path=${mdiUpdate}
.path=${mdiRefresh}
.label=${this.supervisor.localize("store.check_updates")}
></ha-icon-button>
<hassio-addons
+29 -29
View File
@@ -25,7 +25,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.24.4",
"@babel/runtime": "7.24.5",
"@braintree/sanitize-url": "7.0.1",
"@codemirror/autocomplete": "6.16.0",
"@codemirror/commands": "6.5.0",
@@ -70,7 +70,6 @@
"@material/mwc-list": "0.27.0",
"@material/mwc-menu": "0.27.0",
"@material/mwc-radio": "0.27.0",
"@material/mwc-ripple": "0.27.0",
"@material/mwc-select": "0.27.0",
"@material/mwc-snackbar": "0.27.0",
"@material/mwc-switch": "0.27.0",
@@ -89,8 +88,8 @@
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.3.11",
"@vaadin/vaadin-themable-mixin": "24.3.11",
"@vaadin/combo-box": "24.3.12",
"@vaadin/vaadin-themable-mixin": "24.3.12",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -113,7 +112,7 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.3.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.11",
"intl-messageformat": "10.5.12",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
@@ -141,27 +140,27 @@
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
"workbox-cacheable-response": "7.0.0",
"workbox-core": "7.0.0",
"workbox-expiration": "7.0.0",
"workbox-precaching": "7.0.0",
"workbox-routing": "7.0.0",
"workbox-strategies": "7.0.0",
"workbox-cacheable-response": "7.1.0",
"workbox-core": "7.1.0",
"workbox-expiration": "7.1.0",
"workbox-precaching": "7.1.0",
"workbox-routing": "7.1.0",
"workbox-strategies": "7.1.0",
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.24.4",
"@babel/core": "7.24.5",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.24.1",
"@babel/plugin-transform-runtime": "7.24.3",
"@babel/preset-env": "7.24.4",
"@babel/preset-env": "7.24.5",
"@babel/preset-typescript": "7.24.1",
"@bundle-stats/plugin-webpack-filter": "4.12.2",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.4.0",
"@lokalise/node-api": "12.4.1",
"@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.0",
"@octokit/rest": "20.1.0",
"@octokit/plugin-retry": "7.1.1",
"@octokit/rest": "20.1.1",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "25.0.7",
@@ -177,6 +176,7 @@
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.12",
"@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.6",
"@types/qrcode": "1.5.5",
@@ -185,13 +185,13 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.7.1",
"@typescript-eslint/parser": "7.7.1",
"@typescript-eslint/eslint-plugin": "7.8.0",
"@typescript-eslint/parser": "7.8.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": "5.1.0",
"chai": "5.1.1",
"del": "7.1.0",
"eslint": "8.57.0",
"eslint-config-airbnb-base": "15.0.0",
@@ -202,14 +202,13 @@
"eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.11.0",
"eslint-plugin-lit-a11y": "4.1.2",
"eslint-plugin-unused-imports": "3.1.0",
"eslint-plugin-unused-imports": "3.2.0",
"eslint-plugin-wc": "2.1.0",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
"glob": "10.3.12",
"gulp": "4.0.2",
"glob": "10.3.14",
"gulp": "5.0.0",
"gulp-json-transform": "0.5.0",
"gulp-merge-json": "2.2.1",
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.1",
"html-minifier-terser": "7.2.0",
@@ -218,6 +217,7 @@
"jszip": "3.10.1",
"lint-staged": "15.2.2",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"magic-string": "0.30.10",
"map-stream": "0.0.7",
@@ -231,12 +231,12 @@
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5",
"sinon": "17.0.1",
"sinon": "17.0.2",
"source-map-url": "0.4.1",
"systemjs": "6.14.3",
"tar": "7.0.1",
"systemjs": "6.15.1",
"tar": "7.1.0",
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.0",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.4.5",
"webpack": "5.91.0",
@@ -245,7 +245,7 @@
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.1",
"workbox-build": "7.0.0"
"workbox-build": "7.1.0"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {
@@ -257,5 +257,5 @@
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.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@4.1.1"
"packageManager": "yarn@4.2.2"
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20240426.0"
version = "20240501.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
+2 -5
View File
@@ -61,11 +61,8 @@ export const applyThemesOnElement = (
const accentColor = themeSettings?.accentColor;
if (darkMode && primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
primaryColor,
"#121212",
8
);
themeRules["app-theme-color"] = hexBlend(primaryColor, "#121212", 8);
themeRules["app-header-background-color"] = themeRules["app-theme-color"];
}
if (primaryColor) {
+9 -1
View File
@@ -28,7 +28,15 @@ export const FIXED_DOMAIN_STATES = {
input_button: [],
lawn_mower: ["error", "paused", "mowing", "docked"],
light: ["on", "off"],
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
lock: [
"jammed",
"locked",
"locking",
"unlocked",
"unlocking",
"opening",
"open",
],
media_player: [
"off",
"on",
+1
View File
@@ -20,6 +20,7 @@ export class HaInputChip extends MdInputChip {
0.15
);
--ha-input-chip-selected-container-opacity: 1;
--md-input-chip-label-text-font: Roboto, sans-serif;
}
/** Set the size of mdc icons **/
::slotted([slot="icon"]) {
+20 -25
View File
@@ -565,36 +565,30 @@ export class HaDataTable extends LitElement {
}, {});
const groupedItems: DataTableRowData[] = [];
Object.entries(sorted).forEach(([groupName, rows]) => {
if (
groupName !== UNDEFINED_GROUP_KEY ||
Object.keys(sorted).length > 1
) {
groupedItems.push({
append: true,
content: html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${this._collapseGroup}
groupedItems.push({
append: true,
content: html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${this._collapseGroup}
>
<ha-icon-button
.path=${mdiChevronUp}
class=${this._collapsedGroups.includes(groupName)
? "collapsed"
: ""}
>
<ha-icon-button
.path=${mdiChevronUp}
class=${this._collapsedGroups.includes(groupName)
? "collapsed"
: ""}
>
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
? this.hass.localize("ui.components.data-table.ungrouped")
: groupName || ""}
</div>`,
});
}
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
? this.hass.localize("ui.components.data-table.ungrouped")
: groupName || ""}
</div>`,
});
if (!this._collapsedGroups.includes(groupName)) {
groupedItems.push(...rows);
}
});
items = groupedItems;
}
@@ -990,6 +984,7 @@ export class HaDataTable extends LitElement {
padding-top: 12px;
padding-left: 12px;
padding-inline-start: 12px;
padding-inline-end: initial;
width: 100%;
font-weight: 500;
display: flex;
@@ -76,6 +76,8 @@ class HaEntitiesPickerLight extends LitElement {
@property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Array }) public createDomains?: string[];
protected render() {
if (!this.hass) {
return nothing;
@@ -103,6 +105,7 @@ class HaEntitiesPickerLight extends LitElement {
.value=${entityId}
.label=${this.pickedEntityLabel}
.disabled=${this.disabled}
.createDomains=${this.createDomains}
@value-changed=${this._entityChanged}
></ha-entity-picker>
</div>
@@ -122,6 +125,7 @@ class HaEntitiesPickerLight extends LitElement {
.label=${this.pickEntityLabel}
.helper=${this.helper}
.disabled=${this.disabled}
.createDomains=${this.createDomains}
.required=${this.required && !currentEntities.length}
@value-changed=${this._addEntity}
></ha-entity-picker>
+7 -56
View File
@@ -1,14 +1,7 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "./ha-ripple";
@customElement("ha-control-button")
export class HaControlButton extends LitElement {
@@ -16,10 +9,6 @@ export class HaControlButton extends LitElement {
@property() public label?: string;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult {
return html`
<button
@@ -28,54 +17,13 @@ export class HaControlButton extends LitElement {
aria-label=${ifDefined(this.label)}
title=${ifDefined(this.label)}
.disabled=${Boolean(this.disabled)}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
>
<slot></slot>
${this._shouldRenderRipple && !this.disabled
? html`<mwc-ripple></mwc-ripple>`
: ""}
<ha-ripple .disabled=${this.disabled}></ha-ripple>
</button>
`;
}
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
static get styles(): CSSResultGroup {
return css`
:host {
@@ -86,6 +34,7 @@ export class HaControlButton extends LitElement {
--control-button-border-radius: 10px;
--control-button-padding: 8px;
--mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
color: var(--primary-text-color);
width: 40px;
height: 40px;
@@ -113,12 +62,14 @@ export class HaControlButton extends LitElement {
outline: none;
overflow: hidden;
background: none;
--mdc-ripple-color: var(--control-button-background-color);
/* For safari border-radius overflow */
z-index: 0;
font-size: inherit;
color: inherit;
}
.button:focus-visible {
--control-button-background-opacity: 0.4;
}
.button::before {
content: "";
position: absolute;
+9 -66
View File
@@ -1,22 +1,14 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { mdiMenuDown } from "@mdi/js";
import { css, html, nothing } from "lit";
import {
customElement,
eventOptions,
property,
query,
queryAsync,
state,
} from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import "./ha-icon";
import type { HaIcon } from "./ha-icon";
import "./ha-ripple";
import "./ha-svg-icon";
import type { HaSvgIcon } from "./ha-svg-icon";
@@ -32,10 +24,6 @@ export class HaControlSelectMenu extends SelectBase {
@property({ type: Boolean, attribute: "hide-label" })
public hideLabel = false;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
public override render() {
const classes = {
"select-disabled": this.disabled,
@@ -69,17 +57,10 @@ export class HaControlSelectMenu extends SelectBase {
aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required}
@click=${this.onClick}
@focus=${this.onFocus}
@blur=${this.onBlur}
@click=${this.onClick}
@keydown=${this.onKeydown}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
>
${this.renderIcon()}
<div class="content">
@@ -91,9 +72,7 @@ export class HaControlSelectMenu extends SelectBase {
: nothing}
</div>
${this.renderArrow()}
${this._shouldRenderRipple && !this.disabled
? html` <mwc-ripple></mwc-ripple> `
: nothing}
<ha-ripple .disabled=${this.disabled}></ha-ripple>
</div>
${this.renderMenu()}
</div>
@@ -135,46 +114,6 @@ export class HaControlSelectMenu extends SelectBase {
`;
}
protected onFocus() {
this.handleRippleFocus();
super.onFocus();
}
protected onBlur() {
this.handleRippleBlur();
super.onBlur();
}
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
@@ -204,6 +143,7 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-height: 48px;
--control-select-menu-padding: 6px 10px;
--mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
font-size: 14px;
line-height: 1.4;
width: auto;
@@ -224,7 +164,6 @@ export class HaControlSelectMenu extends SelectBase {
outline: none;
overflow: hidden;
background: none;
--mdc-ripple-color: var(--control-select-menu-background-color);
/* For safari border-radius overflow */
z-index: 0;
transition: color 180ms ease-in-out;
@@ -264,6 +203,10 @@ export class HaControlSelectMenu extends SelectBase {
letter-spacing: inherit;
}
.select-anchor:focus-visible {
--control-select-menu-background-opacity: 0.4;
}
.select-anchor::before {
content: "";
position: absolute;
+13 -2
View File
@@ -19,6 +19,7 @@ import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import { getExtendedEntityRegistryEntry } from "../data/entity_registry";
const NONE = "__NONE_OPTION__";
@@ -107,13 +108,23 @@ export class HaConversationAgentPicker extends LitElement {
}
private async _maybeFetchConfigEntry() {
if (!this.value || this.value === "homeassistant") {
if (!this.value || !(this.value in this.hass.entities)) {
this._configEntry = undefined;
return;
}
try {
const regEntry = await getExtendedEntityRegistryEntry(
this.hass,
this.value
);
if (!regEntry.config_entry_id) {
this._configEntry = undefined;
return;
}
this._configEntry = (
await getConfigEntry(this.hass, this.value)
await getConfigEntry(this.hass, regEntry.config_entry_id)
).config_entry;
} catch (err) {
this._configEntry = undefined;
+4
View File
@@ -127,6 +127,10 @@ export class HaDialog extends DialogBase {
border-radius: var(--ha-dialog-border-radius, 28px);
-webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
background: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
}
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
+2 -2
View File
@@ -163,14 +163,14 @@ export class HaFilterDomains extends LitElement {
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-start: initial;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: 0;
margin-inline-end: initial;
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
-3
View File
@@ -1,6 +1,5 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "@material/web/ripple/ripple";
@customElement("ha-label")
class HaLabel extends LitElement {
@@ -11,7 +10,6 @@ class HaLabel extends LitElement {
<span class="content">
<slot name="icon"></slot>
<slot></slot>
<md-ripple></md-ripple>
</span>
`;
}
@@ -27,7 +25,6 @@ class HaLabel extends LitElement {
0.15
);
--ha-label-background-opacity: 1;
position: relative;
box-sizing: border-box;
display: inline-flex;
+1
View File
@@ -25,6 +25,7 @@ export class HaMenuItem extends MdMenuItem {
--md-sys-color-on-primary-container: var(--primary-text-color);
--md-sys-color-on-secondary-container: var(--primary-text-color);
--md-menu-item-label-text-font: Roboto, sans-serif;
}
:host(.warning) {
--md-menu-item-label-text-color: var(--error-color);
+63
View File
@@ -0,0 +1,63 @@
import { AttachableController } from "@material/web/internal/controller/attachable-controller";
import { MdRipple } from "@material/web/ripple/ripple";
import "element-internals-polyfill";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-ripple")
export class HaRipple extends MdRipple {
private readonly attachableTouchController = new AttachableController(
this,
this.onTouchControlChange.bind(this)
);
attach(control: HTMLElement) {
super.attach(control);
this.attachableTouchController.attach(control);
}
detach() {
super.detach();
this.attachableTouchController.detach();
}
private _handleTouchEnd = () => {
if (!this.disabled) {
// @ts-ignore
super.endPressAnimation();
}
};
private onTouchControlChange(
prev: HTMLElement | null,
next: HTMLElement | null
) {
// Add touchend event to clean ripple on touch devices using action handler
prev?.removeEventListener("touchend", this._handleTouchEnd);
next?.addEventListener("touchend", this._handleTouchEnd);
}
static override styles = [
...super.styles,
css`
:host {
--md-ripple-hover-opacity: var(--ha-ripple-hover-opacity, 0.08);
--md-ripple-pressed-opacity: var(--ha-ripple-pressed-opacity, 0.12);
--md-ripple-hover-color: var(
--ha-ripple-hover-color,
var(--ha-ripple-color, var(--secondary-text-color))
);
--md-ripple-pressed-color: var(
--ha-ripple-pressed-color,
var(--ha-ripple-color, var(--secondary-text-color))
);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-ripple": HaRipple;
}
}
@@ -8,7 +8,10 @@ import {
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { EntitySelector } from "../../data/selector";
import { filterSelectorEntities } from "../../data/selector";
import {
filterSelectorEntities,
computeCreateDomains,
} from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker";
import "../entity/ha-entity-picker";
@@ -31,6 +34,8 @@ export class HaEntitySelector extends LitElement {
@property({ type: Boolean }) public required = true;
@state() private _createDomains: string[] | undefined;
private _hasIntegration(selector: EntitySelector) {
return (
selector.entity?.filter &&
@@ -64,6 +69,7 @@ export class HaEntitySelector extends LitElement {
.includeEntities=${this.selector.entity?.include_entities}
.excludeEntities=${this.selector.entity?.exclude_entities}
.entityFilter=${this._filterEntities}
.createDomains=${this._createDomains}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
@@ -79,6 +85,7 @@ export class HaEntitySelector extends LitElement {
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities}
.createDomains=${this._createDomains}
.disabled=${this.disabled}
.required=${this.required}
></ha-entities-picker>
@@ -96,6 +103,9 @@ export class HaEntitySelector extends LitElement {
this._entitySources = sources;
});
}
if (changedProps.has("selector")) {
this._createDomains = computeCreateDomains(this.selector);
}
}
private _filterEntities = (entity: HassEntity): boolean => {
@@ -22,6 +22,7 @@ import {
filterSelectorDevices,
filterSelectorEntities,
TargetSelector,
computeCreateDomains,
} from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-target-picker";
@@ -42,6 +43,8 @@ export class HaTargetSelector extends LitElement {
@state() private _entitySources?: EntitySources;
@state() private _createDomains: string[] | undefined;
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: TargetSelector) {
@@ -68,6 +71,9 @@ export class HaTargetSelector extends LitElement {
this._entitySources = sources;
});
}
if (changedProperties.has("selector")) {
this._createDomains = computeCreateDomains(this.selector);
}
}
protected render() {
@@ -82,7 +88,7 @@ export class HaTargetSelector extends LitElement {
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
.createDomains=${this.selector.target?.create_domains}
.createDomains=${this._createDomains}
></ha-target-picker>`;
}
+3 -9
View File
@@ -44,7 +44,6 @@ import "./ha-service-picker";
import "./ha-settings-row";
import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor";
import { isHelperDomain } from "../panels/config/helpers/const";
const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") {
@@ -366,12 +365,8 @@ export class HaServiceControl extends LitElement {
}
private _targetSelector = memoizeOne(
(targetSelector: TargetSelector | null | undefined, domain?: string) => {
const create_domains = isHelperDomain(domain) ? [domain] : undefined;
return targetSelector
? { target: { ...targetSelector, create_domains } }
: { target: { create_domains } };
}
(targetSelector: TargetSelector | null | undefined) =>
targetSelector ? { target: { ...targetSelector } } : { target: {} }
);
protected render() {
@@ -462,8 +457,7 @@ export class HaServiceControl extends LitElement {
><ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(
serviceData.target as TargetSelector,
domain
serviceData.target as TargetSelector
)}
.disabled=${this.disabled}
@value-changed=${this._targetChanged}
+4 -1
View File
@@ -8,6 +8,9 @@ export class HaSettingsRow extends LitElement {
@property({ type: Boolean, attribute: "three-line" })
public threeLine = false;
@property({ type: Boolean, attribute: "wrap-heading", reflect: true })
public wrapHeading = false;
protected render(): TemplateResult {
return html`
<div class="prefix-wrap">
@@ -51,7 +54,7 @@ export class HaSettingsRow extends LitElement {
.body[three-line] {
min-height: var(--paper-item-body-three-line-min-height, 88px);
}
.body > * {
:host(:not([wrap-heading])) body > * {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+12 -54
View File
@@ -1,15 +1,7 @@
import type { Ripple } from "@material/mwc-ripple";
import "@material/mwc-ripple/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "./ha-ripple";
@customElement("ha-tab")
export class HaTab extends LitElement {
@@ -19,10 +11,6 @@ export class HaTab extends LitElement {
@property() public name?: string;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult {
return html`
<div
@@ -30,60 +18,21 @@ export class HaTab extends LitElement {
role="tab"
aria-selected=${this.active}
aria-label=${ifDefined(this.name)}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
@keydown=${this._handleKeyDown}
>
${this.narrow ? html`<slot name="icon"></slot>` : ""}
<span class="name">${this.name}</span>
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
<ha-ripple></ha-ripple>
</div>
`;
}
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
private _handleKeyDown(ev: KeyboardEvent): void {
if (ev.key === "Enter") {
(ev.target as HTMLElement).click();
}
}
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
static get styles(): CSSResultGroup {
return css`
div {
@@ -126,6 +75,15 @@ export class HaTab extends LitElement {
:host([narrow]) div {
padding: 0 4px;
}
div:focus-visible:before {
position: absolute;
display: block;
content: "";
inset: 0;
background-color: var(--secondary-text-color);
opacity: 0.08;
}
`;
}
}
+7
View File
@@ -11,6 +11,7 @@ import {
HassEntityBase,
} from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { supportsFeature } from "../common/entity/supports-feature";
export const FORMAT_TEXT = "text";
export const FORMAT_NUMBER = "number";
@@ -96,3 +97,9 @@ export const ALARM_MODES: Record<AlarmMode, AlarmConfig> = {
path: mdiShieldOff,
},
};
export const supportedAlarmModes = (stateObj: AlarmControlPanelEntity) =>
(Object.keys(ALARM_MODES) as AlarmMode[]).filter((mode) => {
const feature = ALARM_MODES[mode].feature;
return !feature || supportsFeature(stateObj, feature);
});
+1
View File
@@ -44,6 +44,7 @@ export interface IntegrationManifest {
| "local_polling"
| "local_push";
single_config_entry?: boolean;
version?: string;
}
export interface IntegrationSetup {
domain: string;
+4
View File
@@ -358,6 +358,10 @@ export const localizeStateMessage = (
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_locking`);
case "unlocking":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_unlocking`);
case "opening":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
case "open":
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opened`);
case "locked":
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
case "jammed":
+16 -9
View File
@@ -89,7 +89,8 @@ const tryDescribeAction = <T extends ActionType>(
const config = action as ActionTypes["service"];
const targets: string[] = [];
if (config.target) {
const targetOrData = config.target || config.data;
if (targetOrData) {
for (const [key, name] of Object.entries({
area_id: "areas",
device_id: "devices",
@@ -97,12 +98,10 @@ const tryDescribeAction = <T extends ActionType>(
floor_id: "floors",
label_id: "labels",
})) {
if (!(key in config.target)) {
if (!(key in targetOrData)) {
continue;
}
const keyConf: string[] = Array.isArray(config.target[key])
? config.target[key]
: [config.target[key]];
const keyConf: string[] = ensureArray(targetOrData[key]) || [];
for (const targetThing of keyConf) {
if (isTemplate(targetThing)) {
@@ -195,8 +194,12 @@ const tryDescribeAction = <T extends ActionType>(
(config.service && isTemplate(config.service))
) {
return hass.localize(
`${actionTranslationBaseKey}.service.description.service_based_on_template`,
{ targets: formatListWithAnds(hass.locale, targets) }
targets.length
? `${actionTranslationBaseKey}.service.description.service_based_on_template`
: `${actionTranslationBaseKey}.service.description.service_based_on_template_no_targets`,
{
targets: formatListWithAnds(hass.locale, targets),
}
);
}
@@ -208,7 +211,9 @@ const tryDescribeAction = <T extends ActionType>(
if (config.metadata) {
return hass.localize(
`${actionTranslationBaseKey}.service.description.service_name`,
targets.length
? `${actionTranslationBaseKey}.service.description.service_name`
: `${actionTranslationBaseKey}.service.description.service_name_no_targets`,
{
domain: domainToName(hass.localize, domain),
name: service || config.service,
@@ -218,7 +223,9 @@ const tryDescribeAction = <T extends ActionType>(
}
return hass.localize(
`${actionTranslationBaseKey}.service.description.service_based_on_name`,
targets.length
? `${actionTranslationBaseKey}.service.description.service_based_on_name`
: `${actionTranslationBaseKey}.service.description.service_based_on_name_no_targets`,
{
name: service
? `${domainToName(hass.localize, domain)}: ${service}`
+32 -1
View File
@@ -13,6 +13,7 @@ import {
EntityRegistryEntry,
} from "./entity_registry";
import { EntitySources } from "./entity_sources";
import { isHelperDomain } from "../panels/config/helpers/const";
export type Selector =
| ActionSelector
@@ -405,7 +406,6 @@ export interface TargetSelector {
target: {
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
create_domains?: string[];
} | null;
}
@@ -822,3 +822,34 @@ export const handleLegacyDeviceSelector = (
device: rest,
};
};
export const computeCreateDomains = (
selector: EntitySelector | TargetSelector
): undefined | string[] => {
let entityFilters: EntitySelectorFilter[] | undefined;
if ("target" in selector) {
entityFilters = ensureArray(selector.target?.entity);
} else if ("entity" in selector) {
if (selector.entity?.include_entities) {
return undefined;
}
entityFilters = ensureArray(selector.entity?.filter);
}
if (!entityFilters) {
return undefined;
}
const createDomains = entityFilters.flatMap((entityFilter) =>
!entityFilter.integration &&
!entityFilter.device_class &&
!entityFilter.supported_features &&
entityFilter.domain
? ensureArray(entityFilter.domain).filter((domain) =>
isHelperDomain(domain)
)
: []
);
return [...new Set(createDomains)];
};
+4
View File
@@ -118,6 +118,10 @@ export const checkForEntityUpdates = async (
return;
}
showToast(element, {
message: hass.localize("ui.panel.config.updates.checking_updates"),
});
let updated = 0;
const unsubscribeEvents = await hass.connection.subscribeEvents<HassEvent>(
+2
View File
@@ -147,6 +147,7 @@ export interface QRProvisioningInformation {
manufacturerId: number;
productType: number;
productId: number;
protocol: string;
applicationVersion: string;
maxInclusionRequestInterval?: number | undefined;
uuid?: string | undefined;
@@ -389,6 +390,7 @@ export interface ZwaveJSProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
security_classes: SecurityClass[];
protocol: string;
additional_properties: {
nodeId?: number;
[prop: string]: any;
@@ -52,7 +52,7 @@ export const showConfigFlowDialog = (
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
: step.reason;
},
renderShowFormStepHeader(hass, step) {
@@ -34,7 +34,7 @@ export interface FlowConfig {
renderAbortDescription(
hass: HomeAssistant,
step: DataEntryFlowStepAbort
): TemplateResult | "";
): TemplateResult | string;
renderShowFormStepHeader(
hass: HomeAssistant,
@@ -65,7 +65,7 @@ export const showOptionsFlowDialog = (
.content=${description}
></ha-markdown>
`
: "";
: step.reason;
},
renderShowFormStepHeader(hass, step) {
@@ -189,6 +189,7 @@ class MoreInfoLawnMower extends LitElement {
.flex-horizontal {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.space-around {
justify-content: space-around;
@@ -19,6 +19,7 @@ import "../../../components/ha-select";
import "../../../components/ha-slider";
import "../../../components/ha-svg-icon";
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
import { isUnavailableState } from "../../../data/entity";
import {
MediaPickedEvent,
MediaPlayerEntity,
@@ -62,7 +63,8 @@ class MoreInfoMediaPlayer extends LitElement {
`
)}
</div>
${supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
${!isUnavailableState(stateObj.state) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
? html`
<mwc-button
.label=${this.hass.localize(
@@ -142,7 +142,7 @@ class MoreInfoVacuum extends LitElement {
"ui.dialogs.more_info_control.vacuum.commands"
)}
</div>
<div class="flex-horizontal">
<div class="flex-horizontal space-around">
${VACUUM_COMMANDS.filter((item) =>
item.isVisible(stateObj)
).map(
@@ -327,6 +327,9 @@ class MoreInfoVacuum extends LitElement {
flex-direction: row;
justify-content: space-between;
}
.space-around {
justify-content: space-around;
}
`;
}
}
+2
View File
@@ -231,6 +231,8 @@ export const ENTITY_COMPONENT_ICONS: Record<string, ComponentIcons> = {
locking: "mdi:lock-clock",
unlocked: "mdi:lock-open",
unlocking: "mdi:lock-clock",
opening: "mdi:lock-clock",
open: "mdi:lock-open-variant",
},
},
},
-1
View File
@@ -1,4 +1,3 @@
import "@material/mwc-ripple";
import {
css,
CSSResultGroup,
+1 -1
View File
@@ -48,7 +48,7 @@ class DialogCommunity extends LitElement {
<a
target="_blank"
rel="noreferrer noopener"
href="https://www.home-assistant.io/newsletter/"
href="https://newsletter.openhomefoundation.org/"
>
<ha-list-item hasMeta graphic="icon">
<img src="/static/icons/favicon-192x192.png" slot="graphic" />
+11 -54
View File
@@ -1,15 +1,7 @@
import "@material/mwc-ripple";
import type { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import {
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { customElement, property } from "lit/decorators";
import "../components/ha-card";
import "../components/ha-ripple";
import "../components/ha-svg-icon";
@customElement("onboarding-welcome-link")
@@ -20,28 +12,15 @@ class OnboardingWelcomeLink extends LitElement {
@property({ type: Boolean }) public noninteractive = false;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult {
return html`
<ha-card
.tabIndex=${this.noninteractive ? "-1" : "0"}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
@keydown=${this._handleKeyDown}
>
<ha-svg-icon .path=${this.iconPath}></ha-svg-icon>
${this.label}
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
<ha-ripple></ha-ripple>
</ha-card>
`;
}
@@ -52,36 +31,6 @@ class OnboardingWelcomeLink extends LitElement {
}
}
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
static get styles(): CSSResultGroup {
return css`
:host {
@@ -104,6 +53,14 @@ class OnboardingWelcomeLink extends LitElement {
padding: 8px;
margin-bottom: 16px;
}
ha-card:focus-visible:before {
position: absolute;
display: block;
content: "";
inset: 0;
background-color: var(--secondary-text-color);
opacity: 0.08;
}
`;
}
}
@@ -80,7 +80,7 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
)}
>
<div class="account-row">
<ha-list-item twoline>
<ha-list-item noninteractive twoline>
${this.cloudStatus.email.replace(
/(\w{3})[\w.-]+@([\w.]+\w)/,
"$1***@$2"
@@ -118,7 +118,7 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
: ""}
<div class="account-row">
<ha-list-item>
<ha-list-item noninteractive>
${this.hass.localize(
"ui.panel.config.cloud.account.connection_status"
)}:
@@ -187,6 +187,7 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
<cloud-remote-pref
.hass=${this.hass}
.narrow=${this.narrow}
.cloudStatus=${this.cloudStatus}
></cloud-remote-pref>
@@ -32,6 +32,8 @@ export class CloudRemotePref extends LitElement {
@property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
@property({ type: Boolean }) public narrow = false;
@state() private _unmaskedUrl = false;
protected render() {
@@ -229,11 +231,15 @@ export class CloudRemotePref extends LitElement {
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page_secondary"
)}
<br /><br />
⚠️
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page_warning"
)}
${strict_connection === "guard_page"
? html`
<br /><br />
⚠️
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page_warning"
)}
`
: nothing}
</div>
</div>
</ha-formfield>
@@ -255,11 +261,15 @@ export class CloudRemotePref extends LitElement {
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection_secondary"
)}
<br /><br />
⚠️
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection_warning"
)}
${strict_connection === "drop_connection"
? html`
<br /><br />
⚠️
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection_warning"
)}
`
: nothing}
</div>
</div>
</ha-formfield>
@@ -267,7 +277,7 @@ export class CloudRemotePref extends LitElement {
${strict_connection !== "disabled"
? html`
<ha-settings-row>
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_link"
@@ -288,7 +298,7 @@ export class CloudRemotePref extends LitElement {
: nothing}
<hr />
<ha-settings-row>
<ha-settings-row wrap-heading>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.external_activation"
@@ -305,7 +315,7 @@ export class CloudRemotePref extends LitElement {
></ha-switch>
</ha-settings-row>
<hr />
<ha-settings-row>
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.certificate_info"
@@ -494,6 +504,7 @@ export class CloudRemotePref extends LitElement {
}
ha-settings-row {
padding: 0;
border-top: none !important;
}
ha-expansion-panel {
--expansion-panel-content-padding: 0 16px;
@@ -530,6 +541,7 @@ export class CloudRemotePref extends LitElement {
ha-formfield {
margin-left: -12px;
margin-inline-start: -12px;
margin-inline-end: initial;
--ha-formfield-align-items: start;
}
.strict-connection-container {
@@ -543,6 +555,7 @@ export class CloudRemotePref extends LitElement {
.strict-connection-container .primary {
font-size: 14px;
margin-top: 12px;
margin-bottom: 4px;
}
.strict-connection-container .secondary {
color: var(--secondary-text-color);
@@ -188,6 +188,7 @@ export class CloudTTSPref extends LitElement {
}
.row > * {
flex: 1;
width: 0;
}
.row > *:first-child {
margin-right: 8px;
@@ -1,7 +1,7 @@
import { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiUpdate } from "@mdi/js";
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
import { HassEntities } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@@ -14,11 +14,11 @@ import "../../../components/ha-check-list-item";
import "../../../components/ha-metric";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
fetchHassioSupervisorInfo,
HassioSupervisorInfo,
SupervisorOptions,
fetchHassioSupervisorInfo,
reloadSupervisor,
setSupervisorOption,
SupervisorOptions,
} from "../../../data/hassio/supervisor";
import {
checkForEntityUpdates,
@@ -66,7 +66,7 @@ class HaConfigSectionUpdates extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.updates.check_updates"
)}
.path=${mdiUpdate}
.path=${mdiRefresh}
@click=${this._checkUpdates}
></ha-icon-button>
<ha-button-menu multi>
@@ -4,7 +4,7 @@ import {
mdiDotsVertical,
mdiMagnify,
mdiPower,
mdiUpdate,
mdiRefresh,
} from "@mdi/js";
import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
@@ -83,7 +83,7 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
>`,
newsletter: html`<span class="keep-together"
><a
href=${documentationUrl(hass, `/newsletter`)}
href="https://newsletter.openhomefoundation.org/"
target="_blank"
rel="noreferrer"
>Newsletter</a
@@ -206,7 +206,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
<ha-list-item graphic="icon">
${this.hass.localize("ui.panel.config.updates.check_updates")}
<ha-svg-icon slot="graphic" .path=${mdiUpdate}></ha-svg-icon>
<ha-svg-icon slot="graphic" .path=${mdiRefresh}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon">
@@ -105,7 +105,7 @@ export class DialogEnergyDeviceSettings
type="text"
.disabled=${!this._device}
.value=${this._device?.name || ""}
@change=${this._nameChanged}
@input=${this._nameChanged}
>
</ha-textfield>
@@ -116,6 +116,9 @@ export interface EntityRow extends StateEntity {
localized_platform: string;
domain: string;
label_entries: LabelRegistryEntry[];
enabled: string;
visible: string;
available: string;
}
@customElement("ha-config-entities")
@@ -198,20 +201,36 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
private _states = memoize((localize: LocalizeFunc) => [
{
value: "disabled",
label: localize("ui.panel.config.entities.picker.status.disabled"),
},
{
value: "hidden",
label: localize("ui.panel.config.entities.picker.status.hidden"),
value: "available",
label: localize("ui.panel.config.entities.picker.status.available"),
},
{
value: "unavailable",
label: localize("ui.panel.config.entities.picker.status.unavailable"),
},
{
value: "enabled",
label: localize("ui.panel.config.entities.picker.status.enabled"),
},
{
value: "disabled",
label: localize("ui.panel.config.entities.picker.status.disabled"),
},
{
value: "visible",
label: localize("ui.panel.config.entities.picker.status.visible"),
},
{
value: "hidden",
label: localize("ui.panel.config.entities.picker.status.hidden"),
},
{
value: "readonly",
label: localize("ui.panel.config.entities.picker.status.readonly"),
label: localize("ui.panel.config.entities.picker.status.unmanageable"),
},
{
value: "restored",
label: localize("ui.panel.config.entities.picker.status.not_provided"),
},
]);
@@ -310,7 +329,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
type: "icon",
sortable: true,
filterable: true,
groupable: true,
width: "68px",
template: (entry) =>
entry.unavailable ||
@@ -339,7 +357,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
<simple-tooltip animation-delay="0" position="left">
${entry.restored
? this.hass.localize(
"ui.panel.config.entities.picker.status.restored"
"ui.panel.config.entities.picker.status.not_provided"
)
: entry.unavailable
? this.hass.localize(
@@ -354,13 +372,31 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
"ui.panel.config.entities.picker.status.hidden"
)
: this.hass.localize(
"ui.panel.config.entities.picker.status.readonly"
"ui.panel.config.entities.picker.status.unmanageable"
)}
</simple-tooltip>
</div>
`
: "—",
},
available: {
title: localize("ui.panel.config.entities.picker.headers.availability"),
sortable: true,
groupable: true,
hidden: true,
},
visible: {
title: localize("ui.panel.config.entities.picker.headers.visibility"),
sortable: true,
groupable: true,
hidden: true,
},
enabled: {
title: localize("ui.panel.config.entities.picker.headers.enabled"),
sortable: true,
groupable: true,
hidden: true,
},
labels: {
title: "",
hidden: true,
@@ -389,18 +425,24 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
const stateFilters = filters["ha-filter-states"]?.value;
const showReadOnly =
!stateFilters?.length || stateFilters.includes("readonly");
const showEnabled =
!stateFilters?.length || stateFilters.includes("enabled");
const showDisabled =
!stateFilters?.length || stateFilters.includes("disabled");
const showVisible =
!stateFilters?.length || stateFilters.includes("visible");
const showHidden =
!stateFilters?.length || stateFilters.includes("hidden");
const showAvailable =
!stateFilters?.length || stateFilters.includes("available");
const showUnavailable =
!stateFilters?.length || stateFilters.includes("unavailable");
const showRestored =
!stateFilters?.length || stateFilters.includes("restored");
const showReadOnly =
!stateFilters?.length || stateFilters.includes("readonly");
let filteredEntities = showReadOnly
? entities.concat(stateEntities)
: entities;
let filteredEntities = entities.concat(stateEntities);
let filteredConfigEntry: ConfigEntry | undefined;
const filteredDomains = new Set<string>();
@@ -459,26 +501,29 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
}
});
if (!showDisabled) {
filteredEntities = filteredEntities.filter(
(entity) => !entity.disabled_by
);
}
if (!showHidden) {
filteredEntities = filteredEntities.filter(
(entity) => !entity.hidden_by
);
}
for (const entry of filteredEntities) {
const entity = this.hass.states[entry.entity_id];
const unavailable = entity?.state === UNAVAILABLE;
const restored = entity?.attributes.restored === true;
const areaId = entry.area_id ?? devices[entry.device_id!]?.area_id;
const area = areaId ? areas[areaId] : undefined;
const hidden = !!entry.hidden_by;
const disabled = !!entry.disabled_by;
const readonly = entry.readonly;
const available = entity?.state && entity.state !== UNAVAILABLE;
if (!showUnavailable && unavailable) {
if (
!(
(showAvailable && available) ||
(showUnavailable && unavailable) ||
(showRestored && restored) ||
(showVisible && !hidden) ||
(showHidden && hidden) ||
(showDisabled && disabled) ||
(showEnabled && !disabled) ||
(showReadOnly && readonly)
)
) {
continue;
}
@@ -500,21 +545,30 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
area: area ? area.name : "—",
domain: domainToName(localize, computeDomain(entry.entity_id)),
status: restored
? localize("ui.panel.config.entities.picker.status.restored")
? localize("ui.panel.config.entities.picker.status.not_provided")
: unavailable
? localize("ui.panel.config.entities.picker.status.unavailable")
: entry.disabled_by
: disabled
? localize("ui.panel.config.entities.picker.status.disabled")
: entry.hidden_by
: hidden
? localize("ui.panel.config.entities.picker.status.hidden")
: entry.readonly
: readonly
? localize(
"ui.panel.config.entities.picker.status.readonly"
"ui.panel.config.entities.picker.status.unmanageable"
)
: localize(
"ui.panel.config.entities.picker.status.available"
),
label_entries: labelsEntries,
available: unavailable
? localize("ui.panel.config.entities.picker.status.unavailable")
: localize("ui.panel.config.entities.picker.status.available"),
enabled: disabled
? localize("ui.panel.config.entities.picker.status.disabled")
: localize("ui.panel.config.entities.picker.status.enabled"),
visible: hidden
? localize("ui.panel.config.entities.picker.status.hidden")
: localize("ui.panel.config.entities.picker.status.visible"),
});
}
@@ -861,7 +915,7 @@ ${
protected firstUpdated() {
this._filters = {
"ha-filter-states": {
value: ["unavailable", "readonly"],
value: ["enabled"],
items: undefined,
},
};
@@ -876,10 +930,7 @@ ${
this._filters = {
...this._filters,
"ha-filter-states": {
value: [
...(this._filters["ha-filter-states"]?.value || []),
"disabled",
],
value: [],
items: undefined,
},
"ha-filter-integrations": {
@@ -892,10 +943,7 @@ ${
this._filters = {
...this._filters,
"ha-filter-states": {
value: [
...(this._filters["ha-filter-states"]?.value || []),
"disabled",
],
value: [],
items: undefined,
},
config_entry: {
@@ -309,7 +309,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
<ha-svg-icon .path=${mdiPencilOff}></ha-svg-icon>
<simple-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.entities.picker.status.readonly"
"ui.panel.config.entities.picker.status.unmanageable"
)}
</simple-tooltip>
</div>
@@ -269,6 +269,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
@error=${this._onImageError}
/>
</div>
${this._manifest?.version != null
? html`<div class="version">${this._manifest.version}</div>`
: nothing}
${this._manifest?.is_built_in === false
? html`<ha-alert alert-type="warning"
><ha-svg-icon
@@ -554,18 +557,22 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
if (item.error_reason_translation_key) {
const lokalisePromExc = this.hass
.loadBackendTranslation("exceptions", item.domain)
.then((localize) =>
localize(
`component.${item.domain}.exceptions.${item.error_reason_translation_key}.message`,
item.error_reason_translation_placeholders ?? undefined
)
.then(
(localize) =>
localize(
`component.${item.domain}.exceptions.${item.error_reason_translation_key}.message`,
item.error_reason_translation_placeholders ?? undefined
) || item.reason
);
stateTextExtra = html`${until(lokalisePromExc)}`;
} else {
const lokalisePromError = this.hass
.loadBackendTranslation("config", item.domain)
.then((localize) =>
localize(`component.${item.domain}.config.error.${item.reason}`)
.then(
(localize) =>
localize(
`component.${item.domain}.config.error.${item.reason}`
) || item.reason
);
stateTextExtra = html`${until(lokalisePromError, item.reason)}`;
}
@@ -1404,6 +1411,12 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
display: flex;
justify-content: center;
}
.version {
padding-top: 8px;
display: flex;
justify-content: center;
color: var(--secondary-text-color);
}
.overview .card-actions {
padding: 0;
}
@@ -1,7 +1,4 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import "@material/mwc-ripple";
import type { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { mdiCloud, mdiPackageVariant } from "@mdi/js";
import {
CSSResultGroup,
@@ -11,18 +8,13 @@ import {
html,
nothing,
} from "lit";
import {
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-card";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-ripple";
import "../../../components/ha-svg-icon";
import { ConfigEntry, ERROR_STATES } from "../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
@@ -54,10 +46,6 @@ export class HaIntegrationCard extends LitElement {
@property({ attribute: false }) public logInfo?: IntegrationLogInfo;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult {
const entryState = this._getState(this.items);
@@ -79,17 +67,8 @@ export class HaIntegrationCard extends LitElement {
<a
href=${`/config/integrations/integration/${this.domain}`}
class="ripple-anchor"
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
>
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
<ha-ripple></ha-ripple>
<ha-integration-header
.hass=${this.hass}
.domain=${this.domain}
@@ -242,36 +221,6 @@ export class HaIntegrationCard extends LitElement {
}
);
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
protected handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
protected handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -289,6 +238,15 @@ export class HaIntegrationCard extends LitElement {
.ripple-anchor {
flex-grow: 1;
position: relative;
outline: none;
}
.ripple-anchor:focus-visible:before {
position: absolute;
display: block;
content: "";
inset: 0;
background-color: var(--secondary-text-color);
opacity: 0.08;
}
ha-integration-header {
height: 100%;
@@ -64,7 +64,7 @@ export const showRepairsFlowDialog = (
.content=${description}
></ha-markdown>
`
: "";
: step.reason;
},
renderShowFormStepHeader(hass, step) {
+3
View File
@@ -573,6 +573,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
flex-grow: 1;
height: 100%;
}
.flex mwc-list {
padding-bottom: 64px;
}
.flex mwc-list,
.flex .empty {
border-left: 1px solid var(--divider-color);
+25 -31
View File
@@ -88,38 +88,34 @@ class PanelEnergy extends LitElement {
collectionKey="energy_dashboard"
>
${this.hass.user?.is_admin
? html`
<ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._navigateConfig}
>
<ha-svg-icon slot="graphic" .path=${mdiPencil}>
</ha-svg-icon>
${this.hass!.localize("ui.panel.energy.configure")}
</ha-list-item>
<ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._dumpCSV}
>
<ha-svg-icon slot="graphic" .path=${mdiDownload}>
</ha-svg-icon>
${this.hass!.localize("ui.panel.energy.download_data")}
</ha-list-item>
`
? html` <ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._navigateConfig}
>
<ha-svg-icon slot="graphic" .path=${mdiPencil}> </ha-svg-icon>
${this.hass!.localize("ui.panel.energy.configure")}
</ha-list-item>`
: nothing}
<ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._dumpCSV}
>
<ha-svg-icon slot="graphic" .path=${mdiDownload}> </ha-svg-icon>
${this.hass!.localize("ui.panel.energy.download_data")}
</ha-list-item>
</hui-energy-period-selector>
</div>
</div>
<hui-view
id="view"
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
@reload-energy-panel=${this._reloadView}
></hui-view>
<div id="view" @reload-energy-panel=${this._reloadView}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
</div>
`;
}
@@ -401,12 +397,10 @@ class PanelEnergy extends LitElement {
min-height: 100vh;
box-sizing: border-box;
padding-left: env(safe-area-inset-left);
padding-inline-start: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-inline-start: env(safe-area-inset-left);
padding-inline-end: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
hui-view {
background: var(
--lovelace-background,
var(--primary-background-color)
+73 -8
View File
@@ -1,4 +1,10 @@
import { mdiDownload, mdiFilterRemove } from "@mdi/js";
import {
mdiDotsVertical,
mdiDownload,
mdiFilterRemove,
mdiImagePlus,
} from "@mdi/js";
import { ActionDetail } from "@material/mwc-list";
import { differenceInHours } from "date-fns";
import {
HassServiceTarget,
@@ -23,6 +29,8 @@ import type { StateHistoryCharts } from "../../components/chart/state-history-ch
import "../../components/ha-circular-progress";
import "../../components/ha-date-range-picker";
import "../../components/ha-icon-button";
import "../../components/ha-button-menu";
import "../../components/ha-list-item";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import "../../components/ha-target-picker";
@@ -49,6 +57,7 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { fileDownload } from "../../util/file_download";
import { addEntitiesToLovelaceView } from "../lovelace/editor/add-entities-to-view";
class HaPanelHistory extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
@@ -144,13 +153,23 @@ class HaPanelHistory extends LitElement {
></ha-icon-button>
`
: ""}
<ha-icon-button
slot="actionItems"
@click=${this._downloadHistory}
.disabled=${this._isLoading}
.path=${mdiDownload}
.label=${this.hass.localize("ui.panel.history.download_data")}
></ha-icon-button>
<ha-button-menu slot="actionItems" @action=${this._handleMenuAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon" .disabled=${this._isLoading}>
${this.hass.localize("ui.panel.history.download_data")}
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon" .disabled=${this._isLoading}>
${this.hass.localize("ui.panel.history.add_card")}
<ha-svg-icon slot="graphic" .path=${mdiImagePlus}></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
<div class="flex content">
<div class="filters">
@@ -633,6 +652,17 @@ class HaPanelHistory extends LitElement {
navigate(`/history?${createSearchParam(params)}`, { replace: true });
}
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._downloadHistory();
break;
case 1:
this._suggestCard();
break;
}
}
private _downloadHistory() {
// Make a copy because getEntityIDs is memoized and sort works in-place
const entities = [...this._getEntityIds()].sort();
@@ -726,6 +756,41 @@ class HaPanelHistory extends LitElement {
fileDownload(url, "history.csv");
}
private _suggestCard() {
const entities = this._getEntityIds();
if (entities.length === 0 || !this._mungedStateHistory) {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.history.add_card_error"),
text: this.hass.localize("ui.panel.history.error_no_data"),
warning: true,
});
return;
}
// If you pick things like "This week", the end date can be in the future
const endDateTime = Math.min(this._endDate.getTime(), Date.now());
const cards = [
{
title: this.hass.localize("panel.history"),
type: "history-graph",
hours_to_show: Math.round(
(endDateTime - this._startDate.getTime()) / 1000 / 60 / 60
),
entities,
},
];
addEntitiesToLovelaceView(
this,
this.hass,
cards,
{
title: this.hass.localize("panel.history"),
cards,
},
entities
);
}
static get styles() {
return [
haStyle,
@@ -0,0 +1,7 @@
export const filterModes = (
supportedModes: string[] | undefined,
selectedModes: string[] | undefined
): string[] =>
selectedModes
? selectedModes.filter((mode) => (supportedModes || []).includes(mode))
: supportedModes || [];
@@ -13,15 +13,17 @@ import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
import "../../../components/ha-control-slider";
import {
ALARM_MODES,
AlarmControlPanelEntity,
AlarmMode,
ALARM_MODES,
supportedAlarmModes,
} from "../../../data/alarm_control_panel";
import { UNAVAILABLE } from "../../../data/entity";
import { showEnterCodeDialog } from "../../../dialogs/enter-code/show-enter-code-dialog";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { filterModes } from "./common/filter-modes";
import { AlarmModesCardFeatureConfig } from "./types";
import { showEnterCodeDialog } from "../../../dialogs/enter-code/show-enter-code-dialog";
export const supportsAlarmModesCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
@@ -41,15 +43,9 @@ class HuiAlarmModeCardFeature
@state() _currentMode?: AlarmMode;
static getStubConfig(_, stateObj?: HassEntity): AlarmModesCardFeatureConfig {
static getStubConfig(): AlarmModesCardFeatureConfig {
return {
type: "alarm-modes",
modes: stateObj
? (Object.keys(ALARM_MODES) as AlarmMode[]).filter((mode) => {
const feature = ALARM_MODES[mode as AlarmMode].feature;
return !feature || supportsFeature(stateObj, feature);
})
: [],
};
}
@@ -164,9 +160,12 @@ class HuiAlarmModeCardFeature
const color = stateColorCss(this.stateObj);
const modes = this._modes(this.stateObj, this._config.modes);
const supportedModes = supportedAlarmModes(this.stateObj);
const options = modes.map<ControlSelectOption>((mode) => ({
const options = filterModes(
supportedModes,
this._config.modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.localize(`ui.card.alarm_control_panel.modes.${mode}`),
path: ALARM_MODES[mode].path,
@@ -196,7 +195,7 @@ class HuiAlarmModeCardFeature
)}
style=${styleMap({
"--control-select-color": color,
"--modes-count": modes.length.toString(),
"--modes-count": options.length.toString(),
})}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
@@ -15,6 +15,7 @@ import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { ClimateFanModesCardFeatureConfig } from "./types";
import { filterModes } from "./common/filter-modes";
export const supportsClimateFanModesCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
@@ -40,14 +41,10 @@ class HuiClimateFanModesCardFeature
@query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu;
static getStubConfig(
_,
stateObj?: HassEntity
): ClimateFanModesCardFeatureConfig {
static getStubConfig(): ClimateFanModesCardFeatureConfig {
return {
type: "climate-fan-modes",
style: "dropdown",
fan_modes: stateObj?.attributes.fan_modes || [],
};
}
@@ -122,25 +119,24 @@ class HuiClimateFanModesCardFeature
const stateObj = this.stateObj;
const modes = stateObj.attributes.fan_modes || [];
const options = modes
.filter((mode) => (this._config!.fan_modes || []).includes(mode))
.map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"fan_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="fan_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
const options = filterModes(
stateObj.attributes.fan_modes,
this._config!.fan_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"fan_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="fan_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
if (this._config.style === "icons") {
return html`
@@ -19,6 +19,7 @@ import {
import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { filterModes } from "./common/filter-modes";
import { ClimateHvacModesCardFeatureConfig } from "./types";
export const supportsClimateHvacModesCardFeature = (stateObj: HassEntity) => {
@@ -42,13 +43,9 @@ class HuiClimateHvacModesCardFeature
@query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu;
static getStubConfig(
_,
stateObj?: HassEntity
): ClimateHvacModesCardFeatureConfig {
static getStubConfig(): ClimateHvacModesCardFeatureConfig {
return {
type: "climate-hvac-modes",
hvac_modes: stateObj?.attributes.hvac_modes || [],
};
}
@@ -122,21 +119,23 @@ class HuiClimateHvacModesCardFeature
const color = stateColorCss(this.stateObj);
const modes = this._config.hvac_modes || [];
const ordererHvacModes = (this.stateObj.attributes.hvac_modes || [])
.concat()
.sort(compareClimateHvacModes);
const options = modes
.filter((mode) => this.stateObj?.attributes.hvac_modes.includes(mode))
.sort(compareClimateHvacModes)
.map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode),
icon: html`
<ha-svg-icon
slot="graphic"
.path=${climateHvacModeIcon(mode)}
></ha-svg-icon>
`,
}));
const options = filterModes(
ordererHvacModes,
this._config.hvac_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode),
icon: html`
<ha-svg-icon
slot="graphic"
.path=${climateHvacModeIcon(mode)}
></ha-svg-icon>
`,
}));
if (this._config.style === "dropdown") {
return html`
@@ -15,6 +15,7 @@ import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { ClimatePresetModesCardFeatureConfig } from "./types";
import { filterModes } from "./common/filter-modes";
export const supportsClimatePresetModesCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
@@ -40,14 +41,10 @@ class HuiClimatePresetModesCardFeature
@query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu;
static getStubConfig(
_,
stateObj?: HassEntity
): ClimatePresetModesCardFeatureConfig {
static getStubConfig(): ClimatePresetModesCardFeatureConfig {
return {
type: "climate-preset-modes",
style: "dropdown",
preset_modes: stateObj?.attributes.preset_modes || [],
};
}
@@ -124,25 +121,24 @@ class HuiClimatePresetModesCardFeature
const stateObj = this.stateObj;
const modes = stateObj.attributes.preset_modes || [];
const options = modes
.filter((mode) => (this._config!.preset_modes || []).includes(mode))
.map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
const options = filterModes(
stateObj.attributes.preset_modes,
this._config!.preset_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
if (this._config.style === "icons") {
return html`
@@ -15,6 +15,7 @@ import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { ClimateSwingModesCardFeatureConfig } from "./types";
import { filterModes } from "./common/filter-modes";
export const supportsClimateSwingModesCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
@@ -40,14 +41,10 @@ class HuiClimateSwingModesCardFeature
@query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu;
static getStubConfig(
_,
stateObj?: HassEntity
): ClimateSwingModesCardFeatureConfig {
static getStubConfig(): ClimateSwingModesCardFeatureConfig {
return {
type: "climate-swing-modes",
style: "dropdown",
swing_modes: stateObj?.attributes.swing_modes || [],
};
}
@@ -124,25 +121,24 @@ class HuiClimateSwingModesCardFeature
const stateObj = this.stateObj;
const modes = stateObj.attributes.swing_modes || [];
const options = modes
.filter((mode) => (this._config!.swing_modes || []).includes(mode))
.map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"swing_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
const options = filterModes(
stateObj.attributes.swing_modes,
this._config!.swing_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"swing_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
if (this._config.style === "icons") {
return html`
@@ -15,6 +15,7 @@ import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { FanPresetModesCardFeatureConfig } from "./types";
import { filterModes } from "./common/filter-modes";
export const supportsFanPresetModesCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
@@ -39,14 +40,10 @@ class HuiFanPresetModesCardFeature
@query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu;
static getStubConfig(
_,
stateObj?: HassEntity
): FanPresetModesCardFeatureConfig {
static getStubConfig(): FanPresetModesCardFeatureConfig {
return {
type: "fan-preset-modes",
style: "dropdown",
preset_modes: stateObj?.attributes.preset_modes || [],
};
}
@@ -121,25 +118,24 @@ class HuiFanPresetModesCardFeature
const stateObj = this.stateObj;
const modes = stateObj.attributes.preset_modes || [];
const options = modes
.filter((mode) => (this._config!.preset_modes || []).includes(mode))
.map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
const options = filterModes(
stateObj.attributes.preset_modes,
this._config!.preset_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
if (this._config.style === "icons") {
return html`
@@ -18,6 +18,7 @@ import {
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { HumidifierModesCardFeatureConfig } from "./types";
import { filterModes } from "./common/filter-modes";
export const supportsHumidifierModesCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
@@ -43,14 +44,10 @@ class HuiHumidifierModesCardFeature
@query("ha-control-select-menu", true)
private _haSelect?: HaControlSelectMenu;
static getStubConfig(
_,
stateObj?: HassEntity
): HumidifierModesCardFeatureConfig {
static getStubConfig(): HumidifierModesCardFeatureConfig {
return {
type: "humidifier-modes",
style: "dropdown",
modes: stateObj?.attributes.available_modes || [],
};
}
@@ -125,25 +122,24 @@ class HuiHumidifierModesCardFeature
const stateObj = this.stateObj;
const modes = stateObj.attributes.available_modes || [];
const options = modes
.filter((mode) => (this._config!.modes || []).includes(mode))
.map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
const options = filterModes(
stateObj.attributes.available_modes,
this._config!.modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
this.stateObj!,
"mode",
mode
),
icon: html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="mode"
.attributeValue=${mode}
></ha-attribute-icon>`,
}));
if (this._config.style === "icons") {
return html`
@@ -1,4 +1,4 @@
import { mdiLock, mdiLockOpen } from "@mdi/js";
import { mdiLock, mdiLockOpenVariant } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -90,7 +90,7 @@ class HuiLockCommandsCardFeature
pulse: isLocking(this.stateObj) || isUnlocking(this.stateObj),
})}
>
<ha-svg-icon .path=${mdiLockOpen}></ha-svg-icon>
<ha-svg-icon .path=${mdiLockOpenVariant}></ha-svg-icon>
</ha-control-button>
</ha-control-button-group>
`;
@@ -9,8 +9,9 @@ import { UNAVAILABLE } from "../../../data/entity";
import { InputSelectEntity } from "../../../data/input_select";
import { SelectEntity } from "../../../data/select";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature } from "../types";
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { SelectOptionsCardFeatureConfig } from "./types";
import { filterModes } from "./common/filter-modes";
export const supportsSelectOptionsCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
@@ -41,6 +42,13 @@ class HuiSelectOptionsCardFeature
};
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import(
"../editor/config-elements/hui-select-options-card-feature-editor"
);
return document.createElement("hui-select-options-card-feature-editor");
}
public setConfig(config: SelectOptionsCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
@@ -105,6 +113,11 @@ class HuiSelectOptionsCardFeature
const stateObj = this.stateObj;
const options = filterModes(
this.stateObj.attributes.options,
this._config.options
);
return html`
<div class="container">
<ha-control-select-menu
@@ -118,7 +131,7 @@ class HuiSelectOptionsCardFeature
@selected=${this._valueChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.options!.map(
${options.map(
(option) => html`
<ha-list-item .value=${option}>
${this.hass!.formatEntityState(stateObj, option)}
@@ -19,6 +19,7 @@ import {
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { WaterHeaterOperationModesCardFeatureConfig } from "./types";
import { filterModes } from "./common/filter-modes";
export const supportsWaterHeaterOperationModesCardFeature = (
stateObj: HassEntity
@@ -40,13 +41,9 @@ class HuiWaterHeaterOperationModeCardFeature
@state() _currentOperationMode?: OperationMode;
static getStubConfig(
_,
stateObj?: HassEntity
): WaterHeaterOperationModesCardFeatureConfig {
static getStubConfig(): WaterHeaterOperationModesCardFeatureConfig {
return {
type: "water-heater-operation-modes",
operation_modes: stateObj?.attributes.operation_list || [],
};
}
@@ -107,16 +104,18 @@ class HuiWaterHeaterOperationModeCardFeature
const color = stateColorCss(this.stateObj);
const modes = this._config.operation_modes || [];
const orderedModes = (this.stateObj.attributes.operation_list || [])
.concat()
.sort(compareWaterHeaterOperationMode);
const options = modes
.filter((mode) => this.stateObj?.attributes.operation_list.includes(mode))
.sort(compareWaterHeaterOperationMode)
.map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode),
path: computeOperationModeIcon(mode),
}));
const options = filterModes(
orderedModes,
this._config.operation_modes
).map<ControlSelectOption>((mode) => ({
value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode),
path: computeOperationModeIcon(mode as OperationMode),
}));
return html`
<div class="container">
@@ -75,6 +75,7 @@ export interface ClimatePresetModesCardFeatureConfig {
export interface SelectOptionsCardFeatureConfig {
type: "select-options";
options?: string[];
}
export interface NumericInputCardFeatureConfig {
@@ -1,4 +1,3 @@
import "@material/mwc-ripple";
import {
mdiFan,
mdiFanOff,
+11 -46
View File
@@ -1,21 +1,18 @@
import { consume } from "@lit-labs/context";
import "@material/mwc-ripple";
import type { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import {
HassConfig,
HassEntities,
HassEntity,
} from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, eventOptions, queryAsync, state } from "lit/decorators";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { DOMAINS_TOGGLE } from "../../../common/const";
@@ -27,13 +24,14 @@ import { computeStateDisplaySingleEntity } from "../../../common/entity/compute_
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import {
stateColorCss,
stateColorBrightness,
stateColorCss,
} from "../../../common/entity/state_color";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { iconColorCSS } from "../../../common/style/icon_color_css";
import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-card";
import "../../../components/ha-ripple";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate";
import {
configContext,
@@ -132,10 +130,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
})
_entity?: EntityRegistryDisplayEntry;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
private getStateColor(stateObj: HassEntity, config: ButtonCardConfig) {
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
return config && (config.state_color ?? domain === "light");
@@ -197,13 +191,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
return html`
<ha-card
@action=${this._handleAction}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
@@ -218,6 +205,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
"--state-color": colored ? this._computeColor(stateObj) : undefined,
})}
>
<ha-ripple></ha-ripple>
${this._config.show_icon
? html`
<ha-state-icon
@@ -252,7 +240,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
)}
</span>`
: ""}
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
</ha-card>
`;
}
@@ -282,31 +269,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
}
}
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
@eventOptions({ passive: true })
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
@eventOptions({ passive: true })
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
@eventOptions({ passive: true })
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
static get styles(): CSSResultGroup {
return [
iconColorCSS,
@@ -314,7 +276,9 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
ha-card {
--state-inactive-color: var(--paper-item-icon-color, #44739e);
--state-color: var(--paper-item-icon-color, #44739e);
--mdc-ripple-color: var(--state-color);
--ha-ripple-color: var(--state-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
cursor: pointer;
display: flex;
flex-direction: column;
@@ -340,6 +304,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
color: var(--state-color);
--mdc-icon-size: 100%;
transition: transform 180ms ease-in-out;
pointer-events: none;
}
ha-state-icon + span {
@@ -92,6 +92,7 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
return css`
.card-header {
color: var(--ha-card-header-color, var(--primary-text-color));
text-align: var(--ha-stack-title-text-align, start);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
font-weight: normal;
+10 -54
View File
@@ -1,5 +1,3 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { mdiExclamationThick, mdiHelp } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import {
@@ -10,13 +8,7 @@ import {
html,
nothing,
} from "lit";
import {
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
@@ -29,6 +21,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-ripple";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
import "../../../components/tile/ha-tile-badge";
@@ -313,36 +306,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
return this._renderStateContent(stateObj, "state");
}
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
if (!this.hasCardAction) return;
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
if (!this.hasCardAction) return;
this._rippleHandlers.endPress();
}
private handleRippleMouseEnter() {
if (!this.hasCardAction) return;
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
if (!this.hasCardAction) return;
this._rippleHandlers.endHover();
}
get hasCardAction() {
return (
!this._config?.tap_action ||
@@ -420,17 +383,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
role=${ifDefined(this.hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)}
aria-labelledby="info"
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
>
${this._shouldRenderRipple
? html`<mwc-ripple></mwc-ripple>`
: nothing}
<ha-ripple .disabled=${!this.hasCardAction}></ha-ripple>
</div>
<div class="content ${classMap(contentClasses)}">
<div
@@ -494,7 +448,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
box-shadow: var(--shadow-default), var(--shadow-focus);
}
ha-card {
--mdc-ripple-color: var(--tile-color);
--ha-ripple-color: var(--tile-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
height: 100%;
transition:
box-shadow 180ms ease-in-out,
@@ -526,13 +482,14 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
display: flex;
flex-direction: row;
align-items: center;
padding: 12px;
}
.vertical {
flex-direction: column;
text-align: center;
}
.vertical .icon-container {
margin-top: 12px;
margin-bottom: 12px;
margin-right: 0;
margin-inline-start: initial;
margin-inline-end: initial;
@@ -544,8 +501,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
position: relative;
flex: none;
margin-right: 12px;
margin-inline-start: 12px;
margin-inline-end: initial;
margin-inline-start: initial;
margin-inline-end: 12px;
direction: var(--direction);
transition: transform 180ms ease-in-out;
}
@@ -573,7 +530,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
ha-tile-info {
position: relative;
padding: 12px;
flex: 1;
min-width: 0;
transition: background-color 180ms ease-in-out;
@@ -1,6 +1,4 @@
/* eslint-disable max-classes-per-file */
import "@material/mwc-ripple";
import type { Ripple } from "@material/mwc-ripple";
import { noChange } from "lit";
import {
AttributePart,
@@ -41,8 +39,6 @@ declare global {
class ActionHandler extends HTMLElement implements ActionHandlerType {
public holdTime = 500;
public ripple: Ripple;
protected timer?: number;
protected held = false;
@@ -51,24 +47,21 @@ class ActionHandler extends HTMLElement implements ActionHandlerType {
private dblClickTimeout?: number;
constructor() {
super();
this.ripple = document.createElement("mwc-ripple");
}
public connectedCallback() {
Object.assign(this.style, {
position: "fixed",
width: isTouch ? "100px" : "50px",
height: isTouch ? "100px" : "50px",
transform: "translate(-50%, -50%)",
transform: "translate(-50%, -50%) scale(0)",
pointerEvents: "none",
zIndex: "999",
background: "var(--primary-color)",
display: null,
opacity: "0.2",
borderRadius: "50%",
transition: "transform 180ms ease-in-out",
});
this.appendChild(this.ripple);
this.ripple.primary = true;
[
"touchcancel",
"mouseout",
@@ -219,17 +212,16 @@ class ActionHandler extends HTMLElement implements ActionHandlerType {
Object.assign(this.style, {
left: `${x}px`,
top: `${y}px`,
display: null,
transform: "translate(-50%, -50%) scale(1)",
});
this.ripple.disabled = false;
this.ripple.startPress();
this.ripple.unbounded = true;
}
private stopAnimation() {
this.ripple.endPress();
this.ripple.disabled = true;
this.style.display = "none";
Object.assign(this.style, {
left: null,
top: null,
transform: "translate(-50%, -50%) scale(0)",
});
}
}
@@ -3,17 +3,23 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import { AlarmMode, ALARM_MODES } from "../../../../data/alarm_control_panel";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import { supportedAlarmModes } from "../../../../data/alarm_control_panel";
import type { HomeAssistant } from "../../../../types";
import {
LovelaceCardFeatureContext,
AlarmModesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
import "../../../../components/ha-form/ha-form";
type AlarmModesCardFeatureData = AlarmModesCardFeatureConfig & {
customize_modes: boolean;
};
@customElement("hui-alarm-modes-card-feature-editor")
export class HuiAlarmModesCardFeatureEditor
@@ -31,31 +37,40 @@ export class HuiAlarmModesCardFeatureEditor
}
private _schema = memoizeOne(
(localize: LocalizeFunc, stateObj?: HassEntity) =>
(
localize: LocalizeFunc,
stateObj: HassEntity | undefined,
customizeModes: boolean
) =>
[
{
name: "modes",
name: "customize_modes",
selector: {
select: {
multiple: true,
mode: "list",
options: Object.keys(ALARM_MODES)
.filter((mode) => {
const feature = ALARM_MODES[mode as AlarmMode].feature;
return (
stateObj && (!feature || supportsFeature(stateObj, feature))
);
})
.map((mode) => ({
value: mode,
label: `${localize(
`ui.panel.lovelace.editor.features.types.alarm-modes.modes_list.${mode}`
)}`,
})),
},
boolean: {},
},
},
] as const
...(customizeModes
? ([
{
name: "modes",
selector: {
select: {
multiple: true,
reorder: true,
options: stateObj
? supportedAlarmModes(stateObj).map((mode) => ({
value: mode,
label: `${localize(
`ui.panel.lovelace.editor.features.types.alarm-modes.modes_list.${mode}`
)}`,
}))
: [],
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[]
);
protected render() {
@@ -63,16 +78,25 @@ export class HuiAlarmModesCardFeatureEditor
return nothing;
}
const data: AlarmModesCardFeatureData = {
...this._config,
customize_modes: this._config.modes !== undefined,
};
const stateObj = this.context?.entity_id
? this.hass.states[this.context?.entity_id]
: undefined;
const schema = this._schema(this.hass.localize, stateObj);
const schema = this._schema(
this.hass.localize,
stateObj,
data.customize_modes
);
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
@@ -81,7 +105,21 @@ export class HuiAlarmModesCardFeatureEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const { customize_modes, ...config } = ev.detail
.value as AlarmModesCardFeatureData;
const stateObj = this.context?.entity_id
? this.hass!.states[this.context?.entity_id]
: undefined;
if (customize_modes && !config.modes) {
config.modes = stateObj ? supportedAlarmModes(stateObj) : [];
}
if (!customize_modes && config.modes) {
delete config.modes;
}
fireEvent(this, "config-changed", { config: config });
}
private _computeLabelCallback = (
@@ -89,13 +127,12 @@ export class HuiAlarmModesCardFeatureEditor
) => {
switch (schema.name) {
case "modes":
case "customize_modes":
return this.hass!.localize(
`ui.panel.lovelace.editor.features.types.alarm-modes.${schema.name}`
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
return "";
}
};
}
@@ -21,9 +21,9 @@ import {
import { HomeAssistant } from "../../../../types";
import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature";
import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature";
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-modes-card-feature";
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature";
import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature";
@@ -53,13 +53,13 @@ type SupportsFeature = (stateObj: HassEntity) => boolean;
const UI_FEATURE_TYPES = [
"alarm-modes",
"climate-fan-modes",
"climate-swing-modes",
"climate-hvac-modes",
"climate-preset-modes",
"climate-swing-modes",
"cover-open-close",
"cover-position",
"cover-tilt",
"cover-tilt-position",
"cover-tilt",
"fan-preset-modes",
"fan-speed",
"humidifier-modes",
@@ -82,14 +82,15 @@ type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number];
const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
"alarm-modes",
"climate-hvac-modes",
"climate-fan-modes",
"climate-swing-modes",
"climate-hvac-modes",
"climate-preset-modes",
"climate-swing-modes",
"fan-preset-modes",
"humidifier-modes",
"lawn-mower-commands",
"numeric-input",
"select-options",
"update-actions",
"vacuum-commands",
"water-heater-operation-modes",
@@ -17,6 +17,10 @@ import {
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
type ClimateFanModesCardFeatureData = ClimateFanModesCardFeatureConfig & {
customize_modes: boolean;
};
@customElement("hui-climate-fan-modes-card-feature-editor")
export class HuiClimateFanModesCardFeatureEditor
extends LitElement
@@ -36,7 +40,8 @@ export class HuiClimateFanModesCardFeatureEditor
(
localize: LocalizeFunc,
formatEntityAttributeValue: FormatEntityAttributeValueFunc,
stateObj?: HassEntity
stateObj: HassEntity | undefined,
customizeModes: boolean
) =>
[
{
@@ -55,19 +60,33 @@ export class HuiClimateFanModesCardFeatureEditor
},
},
{
name: "fan_modes",
name: "customize_modes",
selector: {
select: {
multiple: true,
mode: "list",
options:
stateObj?.attributes.fan_modes?.map((mode) => ({
value: mode,
label: formatEntityAttributeValue(stateObj, "fan_mode", mode),
})) || [],
},
boolean: {},
},
},
...(customizeModes
? ([
{
name: "fan_modes",
selector: {
select: {
multiple: true,
reorder: true,
options:
stateObj?.attributes.fan_modes?.map((mode) => ({
value: mode,
label: formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
),
})) || [],
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[]
);
@@ -80,16 +99,17 @@ export class HuiClimateFanModesCardFeatureEditor
? this.hass.states[this.context?.entity_id]
: undefined;
const data: ClimateFanModesCardFeatureConfig = {
const data: ClimateFanModesCardFeatureData = {
style: "dropdown",
fan_modes: [],
...this._config,
customize_modes: this._config.fan_modes !== undefined,
};
const schema = this._schema(
this.hass.localize,
this.hass.formatEntityAttributeValue,
stateObj
stateObj,
data.customize_modes
);
return html`
@@ -104,7 +124,21 @@ export class HuiClimateFanModesCardFeatureEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const { customize_modes, ...config } = ev.detail
.value as ClimateFanModesCardFeatureData;
const stateObj = this.context?.entity_id
? this.hass!.states[this.context?.entity_id]
: undefined;
if (customize_modes && !config.fan_modes) {
config.fan_modes = stateObj?.attributes.fan_modes || [];
}
if (!customize_modes && config.fan_modes) {
delete config.fan_modes;
}
fireEvent(this, "config-changed", { config: config });
}
private _computeLabelCallback = (
@@ -113,6 +147,7 @@ export class HuiClimateFanModesCardFeatureEditor
switch (schema.name) {
case "style":
case "fan_modes":
case "customize_modes":
return this.hass!.localize(
`ui.panel.lovelace.editor.features.types.climate-fan-modes.${schema.name}`
);
@@ -6,8 +6,11 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import type { FormatEntityStateFunc } from "../../../../common/translations/entity-state";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import { HVAC_MODES } from "../../../../data/climate";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import { compareClimateHvacModes } from "../../../../data/climate";
import type { HomeAssistant } from "../../../../types";
import {
ClimateHvacModesCardFeatureConfig,
@@ -15,6 +18,10 @@ import {
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
type ClimateHvacModesCardFeatureData = ClimateHvacModesCardFeatureConfig & {
customize_modes: boolean;
};
@customElement("hui-climate-hvac-modes-card-feature-editor")
export class HuiClimateHvacModesCardFeatureEditor
extends LitElement
@@ -34,7 +41,8 @@ export class HuiClimateHvacModesCardFeatureEditor
(
localize: LocalizeFunc,
formatEntityState: FormatEntityStateFunc,
stateObj?: HassEntity
stateObj: HassEntity | undefined,
customizeModes: boolean
) =>
[
{
@@ -53,21 +61,34 @@ export class HuiClimateHvacModesCardFeatureEditor
},
},
{
name: "hvac_modes",
name: "customize_modes",
selector: {
select: {
multiple: true,
mode: "list",
options: HVAC_MODES.filter((mode) =>
stateObj?.attributes.hvac_modes?.includes(mode)
).map((mode) => ({
value: mode,
label: stateObj ? formatEntityState(stateObj, mode) : mode,
})),
},
boolean: {},
},
},
] as const
...(customizeModes
? ([
{
name: "hvac_modes",
selector: {
select: {
reorder: true,
multiple: true,
options: (stateObj?.attributes.hvac_modes || [])
.concat()
.sort(compareClimateHvacModes)
.map((mode) => ({
value: mode,
label: stateObj
? formatEntityState(stateObj, mode)
: mode,
})),
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[]
);
protected render() {
@@ -79,16 +100,17 @@ export class HuiClimateHvacModesCardFeatureEditor
? this.hass.states[this.context?.entity_id]
: undefined;
const data: ClimateHvacModesCardFeatureConfig = {
const data: ClimateHvacModesCardFeatureData = {
style: "icons",
hvac_modes: [],
...this._config,
customize_modes: this._config.hvac_modes !== undefined,
};
const schema = this._schema(
this.hass.localize,
this.hass.formatEntityState,
stateObj
stateObj,
data.customize_modes
);
return html`
@@ -103,7 +125,24 @@ export class HuiClimateHvacModesCardFeatureEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const { customize_modes, ...config } = ev.detail
.value as ClimateHvacModesCardFeatureData;
const stateObj = this.context?.entity_id
? this.hass!.states[this.context?.entity_id]
: undefined;
if (customize_modes && !config.hvac_modes) {
const ordererHvacModes = (stateObj?.attributes.hvac_modes || [])
.concat()
.sort(compareClimateHvacModes);
config.hvac_modes = ordererHvacModes;
}
if (!customize_modes && config.hvac_modes) {
delete config.hvac_modes;
}
fireEvent(this, "config-changed", { config: config });
}
private _computeLabelCallback = (
@@ -112,6 +151,7 @@ export class HuiClimateHvacModesCardFeatureEditor
switch (schema.name) {
case "hvac_modes":
case "style":
case "customize_modes":
return this.hass!.localize(
`ui.panel.lovelace.editor.features.types.climate-hvac-modes.${schema.name}`
);
@@ -17,6 +17,10 @@ import {
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
type ClimatePresetModesCardFeatureData = ClimatePresetModesCardFeatureConfig & {
customize_modes: boolean;
};
@customElement("hui-climate-preset-modes-card-feature-editor")
export class HuiClimatePresetModesCardFeatureEditor
extends LitElement
@@ -36,7 +40,8 @@ export class HuiClimatePresetModesCardFeatureEditor
(
localize: LocalizeFunc,
formatEntityAttributeValue: FormatEntityAttributeValueFunc,
stateObj?: HassEntity
stateObj: HassEntity | undefined,
customizeModes: boolean
) =>
[
{
@@ -55,23 +60,33 @@ export class HuiClimatePresetModesCardFeatureEditor
},
},
{
name: "preset_modes",
name: "customize_modes",
selector: {
select: {
multiple: true,
mode: "list",
options:
stateObj?.attributes.preset_modes?.map((mode) => ({
value: mode,
label: formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
),
})) || [],
},
boolean: {},
},
},
...(customizeModes
? ([
{
name: "preset_modes",
selector: {
select: {
reorder: true,
multiple: true,
options:
stateObj?.attributes.preset_modes?.map((mode) => ({
value: mode,
label: formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
),
})) || [],
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[]
);
@@ -84,16 +99,17 @@ export class HuiClimatePresetModesCardFeatureEditor
? this.hass.states[this.context?.entity_id]
: undefined;
const data: ClimatePresetModesCardFeatureConfig = {
const data: ClimatePresetModesCardFeatureData = {
style: "dropdown",
preset_modes: [],
...this._config,
customize_modes: this._config.preset_modes !== undefined,
};
const schema = this._schema(
this.hass.localize,
this.hass.formatEntityAttributeValue,
stateObj
stateObj,
data.customize_modes
);
return html`
@@ -108,7 +124,21 @@ export class HuiClimatePresetModesCardFeatureEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const { customize_modes, ...config } = ev.detail
.value as ClimatePresetModesCardFeatureData;
const stateObj = this.context?.entity_id
? this.hass!.states[this.context?.entity_id]
: undefined;
if (customize_modes && !config.preset_modes) {
config.preset_modes = stateObj?.attributes.preset_modes || [];
}
if (!customize_modes && config.preset_modes) {
delete config.preset_modes;
}
fireEvent(this, "config-changed", { config: config });
}
private _computeLabelCallback = (
@@ -117,6 +147,7 @@ export class HuiClimatePresetModesCardFeatureEditor
switch (schema.name) {
case "style":
case "preset_modes":
case "customize_modes":
return this.hass!.localize(
`ui.panel.lovelace.editor.features.types.climate-preset-modes.${schema.name}`
);
@@ -17,6 +17,10 @@ import {
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
type ClimateSwingModesCardFeatureData = ClimateSwingModesCardFeatureConfig & {
customize_modes: boolean;
};
@customElement("hui-climate-swing-modes-card-feature-editor")
export class HuiClimateSwingModesCardFeatureEditor
extends LitElement
@@ -36,7 +40,8 @@ export class HuiClimateSwingModesCardFeatureEditor
(
localize: LocalizeFunc,
formatEntityAttributeValue: FormatEntityAttributeValueFunc,
stateObj?: HassEntity
stateObj: HassEntity | undefined,
customizeModes: boolean
) =>
[
{
@@ -55,23 +60,33 @@ export class HuiClimateSwingModesCardFeatureEditor
},
},
{
name: "swing_modes",
name: "customize_modes",
selector: {
select: {
multiple: true,
mode: "list",
options:
stateObj?.attributes.swing_modes?.map((mode) => ({
value: mode,
label: formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
),
})) || [],
},
boolean: {},
},
},
...(customizeModes
? ([
{
name: "swing_modes",
selector: {
select: {
reorder: true,
multiple: true,
options:
stateObj?.attributes.swing_modes?.map((mode) => ({
value: mode,
label: formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
),
})) || [],
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[]
);
@@ -84,16 +99,17 @@ export class HuiClimateSwingModesCardFeatureEditor
? this.hass.states[this.context?.entity_id]
: undefined;
const data: ClimateSwingModesCardFeatureConfig = {
const data: ClimateSwingModesCardFeatureData = {
style: "dropdown",
swing_modes: [],
...this._config,
customize_modes: this._config.swing_modes !== undefined,
};
const schema = this._schema(
this.hass.localize,
this.hass.formatEntityAttributeValue,
stateObj
stateObj,
data.customize_modes
);
return html`
@@ -108,7 +124,21 @@ export class HuiClimateSwingModesCardFeatureEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const { customize_modes, ...config } = ev.detail
.value as ClimateSwingModesCardFeatureData;
const stateObj = this.context?.entity_id
? this.hass!.states[this.context?.entity_id]
: undefined;
if (customize_modes && !config.swing_modes) {
config.swing_modes = stateObj?.attributes.swing_modes || [];
}
if (!customize_modes && config.swing_modes) {
delete config.swing_modes;
}
fireEvent(this, "config-changed", { config: config });
}
private _computeLabelCallback = (
@@ -117,6 +147,7 @@ export class HuiClimateSwingModesCardFeatureEditor
switch (schema.name) {
case "style":
case "swing_modes":
case "customize_modes":
return this.hass!.localize(
`ui.panel.lovelace.editor.features.types.climate-swing-modes.${schema.name}`
);
@@ -17,6 +17,10 @@ import {
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
type FanPresetModesCardFeatureData = FanPresetModesCardFeatureConfig & {
customize_modes: boolean;
};
@customElement("hui-fan-preset-modes-card-feature-editor")
export class HuiFanPresetModesCardFeatureEditor
extends LitElement
@@ -36,7 +40,8 @@ export class HuiFanPresetModesCardFeatureEditor
(
localize: LocalizeFunc,
formatEntityAttributeValue: FormatEntityAttributeValueFunc,
stateObj?: HassEntity
stateObj: HassEntity | undefined,
customizeModes: boolean
) =>
[
{
@@ -55,23 +60,33 @@ export class HuiFanPresetModesCardFeatureEditor
},
},
{
name: "preset_modes",
name: "customize_modes",
selector: {
select: {
multiple: true,
mode: "list",
options:
stateObj?.attributes.preset_modes?.map((mode) => ({
value: mode,
label: formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
),
})) || [],
},
boolean: {},
},
},
...(customizeModes
? ([
{
name: "preset_modes",
selector: {
select: {
reorder: true,
multiple: true,
options:
stateObj?.attributes.preset_modes?.map((mode) => ({
value: mode,
label: formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
),
})) || [],
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[]
);
@@ -84,16 +99,17 @@ export class HuiFanPresetModesCardFeatureEditor
? this.hass.states[this.context?.entity_id]
: undefined;
const data: FanPresetModesCardFeatureConfig = {
const data: FanPresetModesCardFeatureData = {
style: "dropdown",
preset_modes: [],
...this._config,
customize_modes: this._config.preset_modes !== undefined,
};
const schema = this._schema(
this.hass.localize,
this.hass.formatEntityAttributeValue,
stateObj
stateObj,
data.customize_modes
);
return html`
@@ -108,7 +124,21 @@ export class HuiFanPresetModesCardFeatureEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const { customize_modes, ...config } = ev.detail
.value as FanPresetModesCardFeatureData;
const stateObj = this.context?.entity_id
? this.hass!.states[this.context?.entity_id]
: undefined;
if (customize_modes && !config.preset_modes) {
config.preset_modes = stateObj?.attributes.preset_modes || [];
}
if (!customize_modes && config.preset_modes) {
delete config.preset_modes;
}
fireEvent(this, "config-changed", { config: config });
}
private _computeLabelCallback = (
@@ -117,6 +147,7 @@ export class HuiFanPresetModesCardFeatureEditor
switch (schema.name) {
case "style":
case "preset_modes":
case "customize_modes":
return this.hass!.localize(
`ui.panel.lovelace.editor.features.types.fan-preset-modes.${schema.name}`
);
@@ -17,6 +17,10 @@ import {
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
type HumidifierModesCardFeatureData = HumidifierModesCardFeatureConfig & {
customize_modes: boolean;
};
@customElement("hui-humidifier-modes-card-feature-editor")
export class HuiHumidifierModesCardFeatureEditor
extends LitElement
@@ -36,7 +40,8 @@ export class HuiHumidifierModesCardFeatureEditor
(
localize: LocalizeFunc,
formatEntityAttributeValue: FormatEntityAttributeValueFunc,
stateObj?: HassEntity
stateObj: HassEntity | undefined,
customizeModes: boolean
) =>
[
{
@@ -55,19 +60,33 @@ export class HuiHumidifierModesCardFeatureEditor
},
},
{
name: "modes",
name: "customize_modes",
selector: {
select: {
multiple: true,
mode: "list",
options:
stateObj?.attributes.available_modes?.map((mode) => ({
value: mode,
label: formatEntityAttributeValue(stateObj, "mode", mode),
})) || [],
},
boolean: {},
},
},
...(customizeModes
? ([
{
name: "modes",
selector: {
select: {
reorder: true,
multiple: true,
options:
stateObj?.attributes.available_modes?.map((mode) => ({
value: mode,
label: formatEntityAttributeValue(
stateObj,
"mode",
mode
),
})) || [],
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[]
);
@@ -80,16 +99,17 @@ export class HuiHumidifierModesCardFeatureEditor
? this.hass.states[this.context?.entity_id]
: undefined;
const data: HumidifierModesCardFeatureConfig = {
const data: HumidifierModesCardFeatureData = {
style: "dropdown",
modes: [],
...this._config,
customize_modes: this._config.modes !== undefined,
};
const schema = this._schema(
this.hass.localize,
this.hass.formatEntityAttributeValue,
stateObj
stateObj,
data.customize_modes
);
return html`
@@ -104,7 +124,21 @@ export class HuiHumidifierModesCardFeatureEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const { customize_modes, ...config } = ev.detail
.value as HumidifierModesCardFeatureData;
const stateObj = this.context?.entity_id
? this.hass!.states[this.context?.entity_id]
: undefined;
if (customize_modes && !config.modes) {
config.modes = stateObj?.attributes.available_modes || [];
}
if (!customize_modes && config.modes) {
delete config.modes;
}
fireEvent(this, "config-changed", { config: config });
}
private _computeLabelCallback = (
@@ -113,6 +147,7 @@ export class HuiHumidifierModesCardFeatureEditor
switch (schema.name) {
case "style":
case "modes":
case "customize_modes":
return this.hass!.localize(
`ui.panel.lovelace.editor.features.types.humidifier-modes.${schema.name}`
);
@@ -0,0 +1,140 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { FormatEntityStateFunc } from "../../../../common/translations/entity-state";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import {
LovelaceCardFeatureContext,
SelectOptionsCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
type SelectOptionsCardFeatureData = SelectOptionsCardFeatureConfig & {
customize_options: boolean;
};
@customElement("hui-select-options-card-feature-editor")
export class HuiSelectOptionsCardFeatureEditor
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: SelectOptionsCardFeatureConfig;
public setConfig(config: SelectOptionsCardFeatureConfig): void {
this._config = config;
}
private _schema = memoizeOne(
(
formatEntityState: FormatEntityStateFunc,
stateObj: HassEntity | undefined,
customizeOptions: boolean
) =>
[
{
name: "customize_options",
selector: {
boolean: {},
},
},
...(customizeOptions
? ([
{
name: "options",
selector: {
select: {
multiple: true,
reorder: true,
options:
stateObj?.attributes.options?.map((option) => ({
value: option,
label: formatEntityState(stateObj, option),
})) || [],
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[]
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const stateObj = this.context?.entity_id
? this.hass.states[this.context?.entity_id]
: undefined;
const data: SelectOptionsCardFeatureData = {
...this._config,
customize_options: this._config.options !== undefined,
};
const schema = this._schema(
this.hass.formatEntityState,
stateObj,
data.customize_options
);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
const { customize_options, ...config } = ev.detail
.value as SelectOptionsCardFeatureData;
const stateObj = this.context?.entity_id
? this.hass!.states[this.context?.entity_id]
: undefined;
if (customize_options && !config.options) {
config.options = stateObj?.attributes.options || [];
}
if (!customize_options && config.options) {
delete config.options;
}
fireEvent(this, "config-changed", { config: config });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "options":
case "customize_options":
return this.hass!.localize(
`ui.panel.lovelace.editor.features.types.select-options.${schema.name}`
);
default:
return "";
}
};
}
declare global {
interface HTMLElementTagNameMap {
"hui-select-options-card-feature-editor": HuiSelectOptionsCardFeatureEditor;
}
}
@@ -5,14 +5,22 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { FormatEntityStateFunc } from "../../../../common/translations/entity-state";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import {
WaterHeaterOperationModesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
import { OPERATION_MODES } from "../../../../data/water_heater";
import { compareWaterHeaterOperationMode } from "../../../../data/water_heater";
type WaterHeaterOperationModesCardFeatureData =
WaterHeaterOperationModesCardFeatureConfig & {
customize_modes: boolean;
};
@customElement("hui-water-heater-operation-modes-card-feature-editor")
export class HuiWaterHeaterOperationModesCardFeatureEditor
@@ -30,24 +38,41 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor
}
private _schema = memoizeOne(
(formatEntityState: FormatEntityStateFunc, stateObj?: HassEntity) =>
(
formatEntityState: FormatEntityStateFunc,
stateObj: HassEntity | undefined,
customizeModes: boolean
) =>
[
{
name: "operation_modes",
name: "customize_modes",
selector: {
select: {
multiple: true,
mode: "list",
options: OPERATION_MODES.filter((mode) =>
stateObj?.attributes.operation_list?.includes(mode)
).map((mode) => ({
value: mode,
label: stateObj ? formatEntityState(stateObj, mode) : mode,
})),
},
boolean: {},
},
},
] as const
...(customizeModes
? ([
{
name: "operation_modes",
selector: {
select: {
reorder: true,
multiple: true,
options: (stateObj?.attributes.operation_list || [])
.concat()
.sort(compareWaterHeaterOperationMode)
.map((mode) => ({
value: mode,
label: stateObj
? formatEntityState(stateObj, mode)
: mode,
})),
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[]
);
protected render() {
@@ -59,12 +84,21 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor
? this.hass.states[this.context?.entity_id]
: undefined;
const schema = this._schema(this.hass.formatEntityState, stateObj);
const data: WaterHeaterOperationModesCardFeatureData = {
...this._config,
customize_modes: this._config.operation_modes !== undefined,
};
const schema = this._schema(
this.hass.formatEntityState,
stateObj,
data.customize_modes
);
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
@@ -73,7 +107,23 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const { customize_modes, ...config } = ev.detail
.value as WaterHeaterOperationModesCardFeatureData;
const stateObj = this.context?.entity_id
? this.hass!.states[this.context?.entity_id]
: undefined;
if (customize_modes && !config.operation_modes) {
config.operation_modes = (stateObj?.attributes.operation_list || [])
.concat()
.sort(compareWaterHeaterOperationMode);
}
if (!customize_modes && config.operation_modes) {
delete config.operation_modes;
}
fireEvent(this, "config-changed", { config: config });
}
private _computeLabelCallback = (
@@ -81,13 +131,12 @@ export class HuiWaterHeaterOperationModesCardFeatureEditor
) => {
switch (schema.name) {
case "operation_modes":
case "customize_modes":
return this.hass!.localize(
`ui.panel.lovelace.editor.features.types.water-heater-modes.${schema.name}`
`ui.panel.lovelace.editor.features.types.water-heater-operation-modes.${schema.name}`
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
return "";
}
};
}
+2 -4
View File
@@ -886,9 +886,9 @@ class HUIRoot extends LitElement {
const configBackground = viewConfig.background || this.config.background;
if (configBackground) {
this.style.setProperty("--lovelace-background", configBackground);
root.style.setProperty("--lovelace-background", configBackground);
} else {
this.style.removeProperty("--lovelace-background");
root.style.removeProperty("--lovelace-background");
}
root.appendChild(view);
@@ -1013,8 +1013,6 @@ class HUIRoot extends LitElement {
padding-inline-start: env(safe-area-inset-left);
padding-inline-end: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
hui-view {
background: var(
--lovelace-background,
var(--primary-background-color)
+28 -1
View File
@@ -143,6 +143,11 @@ export class HUIView extends ReactiveElement {
return this;
}
public connectedCallback(): void {
super.connectedCallback();
this._applyTheme();
}
public willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
@@ -212,7 +217,7 @@ export class HUIView extends ReactiveElement {
this.hass.themes !== oldHass.themes ||
this.hass.selectedTheme !== oldHass.selectedTheme
) {
applyThemesOnElement(this, this.hass.themes, this._viewConfigTheme);
this._applyTheme();
}
}
if (changedProperties.has("narrow")) {
@@ -238,6 +243,28 @@ export class HUIView extends ReactiveElement {
}
}
private _applyTheme() {
applyThemesOnElement(this, this.hass.themes, this._viewConfigTheme);
if (this._viewConfigTheme) {
// Set lovelace background color to root element, so it will be placed under the header too
const computedStyles = getComputedStyle(this);
let lovelaceBackground = computedStyles.getPropertyValue(
"--lovelace-background"
);
if (!lovelaceBackground) {
lovelaceBackground = computedStyles.getPropertyValue(
"--primary-background-color"
);
}
if (lovelaceBackground) {
this.parentElement?.style.setProperty(
"--lovelace-background",
lovelaceBackground
);
}
}
}
private async _initializeConfig() {
let viewConfig = this.lovelace.config.views[this.index];
let isStrategy = false;
+32 -10
View File
@@ -445,6 +445,7 @@
"was_opened": "was opened",
"was_closed": "was closed",
"is_opening": "is opening",
"is_opened": "is opened",
"is_closing": "is closing",
"was_unlocked": "was unlocked",
"was_locked": "was locked",
@@ -1889,6 +1890,7 @@
"check_updates": "Check for updates",
"no_new_updates": "No new updates found",
"updates_refreshed": "{count} {count, plural,\n one {update}\n other {updates}\n} refreshed",
"checking_updates": "Checking for updates...",
"title": "{count} {count, plural,\n one {update}\n other {updates}\n}",
"unable_to_fetch": "Unable to load updates",
"more_updates": "Show all updates",
@@ -2941,7 +2943,7 @@
"event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]",
"sunrise": "Sunrise",
"sunset": "Sunset",
"offset": "Offset (optional)",
"offset": "Offset in seconds or HH:MM:SS (optional)",
"description": {
"picker": "When the sun sets or rises.",
"sets": "When the sun sets{hasDuration, select, \n true { offset by {duration}} \n other {}\n }",
@@ -3250,6 +3252,9 @@
"service_based_on_template": "Call a service based on a template on {targets}",
"service_based_on_name": "Call a service ''{name}'' on {targets}",
"service_name": "{domain} ''{name}'' on {targets}",
"service_based_on_template_no_targets": "Call a service based on a template",
"service_based_on_name_no_targets": "Call a service ''{name}''",
"service_name_no_targets": "{domain} ''{name}''",
"service": "Call a service",
"target_template": "templated {name}",
"target_unknown_entity": "unknown entity",
@@ -4061,12 +4066,14 @@
"search": "Search {number} entities",
"unnamed_entity": "Unnamed entity",
"status": {
"restored": "Restored",
"available": "Available",
"unavailable": "Unavailable",
"enabled": "Enabled",
"disabled": "Disabled",
"readonly": "Read-only",
"hidden": "Hidden"
"visible": "Visible",
"hidden": "Hidden",
"not_provided": "Not provided",
"unmanageable": "Unmanageable"
},
"headers": {
"state_icon": "State icon",
@@ -4076,7 +4083,10 @@
"area": "Area",
"disabled_by": "Disabled by",
"status": "Status",
"domain": "Domain"
"domain": "Domain",
"availability": "Availability",
"visibility": "Visibility",
"enabled": "Enabled"
},
"selected": "{number} selected",
"enable_selected": {
@@ -5964,7 +5974,8 @@
"armed_vacation": "[%key:ui::card::alarm_control_panel::modes::armed_vacation%]",
"armed_custom_bypass": "[%key:ui::card::alarm_control_panel::modes::armed_custom_bypass%]",
"disarmed": "[%key:ui::card::alarm_control_panel::modes::disarmed%]"
}
},
"customize_modes": "Customize alarm modes"
},
"light-brightness": {
"label": "Light brightness"
@@ -5996,16 +6007,18 @@
"dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]",
"icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]"
},
"customize_modes": "Customize fan modes",
"fan_modes": "Fan modes"
},
"climate-swing-modes": {
"label": "Climate swing modes",
"swing_modes": "Swing modes",
"style": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style%]",
"style_list": {
"dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]",
"icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]"
},
"swing_modes": "Swing modes"
"customize_modes": "Customize swing modes"
},
"climate-hvac-modes": {
"label": "Climate HVAC modes",
@@ -6014,7 +6027,8 @@
"style_list": {
"dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]",
"icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]"
}
},
"customize_modes": "Customize HVAC modes"
},
"climate-preset-modes": {
"label": "Climate preset modes",
@@ -6023,6 +6037,7 @@
"dropdown": "Dropdown",
"icons": "Icons"
},
"customize_modes": "Customize preset modes",
"preset_modes": "Preset modes"
},
"fan-preset-modes": {
@@ -6032,6 +6047,7 @@
"dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]",
"icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]"
},
"customize_modes": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::customize_modes%]",
"preset_modes": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::preset_modes%]"
},
"humidifier-toggle": {
@@ -6044,10 +6060,13 @@
"dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]",
"icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]"
},
"customize_modes": "Customize modes",
"modes": "Modes"
},
"select-options": {
"label": "Select options"
"label": "Select options",
"options": "Options",
"customize_options": "Customize options"
},
"numeric-input": {
"label": "Numeric input",
@@ -6065,7 +6084,8 @@
},
"water-heater-operation-modes": {
"label": "Water heater operation modes",
"operation_modes": "Operation modes"
"operation_modes": "Operation modes",
"customize_modes": "Customize operation modes"
},
"lawn-mower-commands": {
"label": "Lawn mower commands",
@@ -6933,6 +6953,8 @@
"remove_all": "Remove all selections",
"download_data": "Download data",
"download_data_error": "Unable to download data",
"add_card": "Add current view as card",
"add_card_error": "Unable to add card",
"error_no_data": "You need to select data first."
}
},
+861 -2454
View File
File diff suppressed because it is too large Load Diff