mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-12 02:39:26 +00:00
Compare commits
80 Commits
add-no-dev
...
add-zwave-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3b69f9cc8d | ||
![]() |
6d3940db1e | ||
![]() |
20d174431d | ||
![]() |
1900710e06 | ||
![]() |
ed86a48e1c | ||
![]() |
d2bdb52926 | ||
![]() |
9c57c9f151 | ||
![]() |
9e9cb15a42 | ||
![]() |
6421a9443d | ||
![]() |
f2b43ddad8 | ||
![]() |
e55b59d9b7 | ||
![]() |
4a77359a06 | ||
![]() |
505d7b6ddb | ||
![]() |
79cdc43699 | ||
![]() |
8ff9823cd7 | ||
![]() |
3488c60818 | ||
![]() |
43a422cdca | ||
![]() |
11f2bef05c | ||
![]() |
ff9f331287 | ||
![]() |
cdf64ccdaa | ||
![]() |
8b220acca2 | ||
![]() |
8fdb7fa1d5 | ||
![]() |
008c842431 | ||
![]() |
bc41de0d9c | ||
![]() |
7310c9cf6d | ||
![]() |
84b436c08e | ||
![]() |
1925a47bdc | ||
![]() |
438a426458 | ||
![]() |
f923deb71d | ||
![]() |
e79bc71ab7 | ||
![]() |
11b0990d2b | ||
![]() |
870cb0c65f | ||
![]() |
deda2009f8 | ||
![]() |
b2797ab8da | ||
![]() |
644dcb0381 | ||
![]() |
c65f4f7a6e | ||
![]() |
68a79490dc | ||
![]() |
6febe8552e | ||
![]() |
f611f23f6f | ||
![]() |
627e06663b | ||
![]() |
ab01633069 | ||
![]() |
17dcc90638 | ||
![]() |
d0df029ff1 | ||
![]() |
86a7e69812 | ||
![]() |
af9417f2a6 | ||
![]() |
7120ad99b9 | ||
![]() |
334c245b65 | ||
![]() |
bcb72d83b8 | ||
![]() |
c99e0e846b | ||
![]() |
ec3f63e8a3 | ||
![]() |
1bc33a30ec | ||
![]() |
8cca233b7c | ||
![]() |
a78608bfb4 | ||
![]() |
1a797b3415 | ||
![]() |
2b27a4da2b | ||
![]() |
1df92fa863 | ||
![]() |
cdde85315a | ||
![]() |
dc67f9faf4 | ||
![]() |
3ad1be50a2 | ||
![]() |
8aadfe7d28 | ||
![]() |
cff54b73a4 | ||
![]() |
b54cfeb0c0 | ||
![]() |
cefe612b11 | ||
![]() |
4bc874b497 | ||
![]() |
f3abaa8e02 | ||
![]() |
21a563fe98 | ||
![]() |
35d6c638ab | ||
![]() |
68f8239708 | ||
![]() |
0db64cca0b | ||
![]() |
accfda5f4b | ||
![]() |
c97c20f57d | ||
![]() |
2725d0191d | ||
![]() |
852cc62398 | ||
![]() |
654e3ce437 | ||
![]() |
20a3a00aec | ||
![]() |
22b927d666 | ||
![]() |
709d6be2e3 | ||
![]() |
fbda9ca418 | ||
![]() |
4e97e3763e | ||
![]() |
4c9c52d27d |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
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
|
||||
|
||||
|
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -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.
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
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
|
||||
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
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
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
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
|
||||
|
6
.github/workflows/nightly.yaml
vendored
6
.github/workflows/nightly.yaml
vendored
@@ -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
|
||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -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
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -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
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 });
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import { selectedDemoConfig } from "./configs/demo-configs";
|
||||
import { mockAreaRegistry } from "./stubs/area_registry";
|
||||
import { mockAuth } from "./stubs/auth";
|
||||
import { mockConfigEntries } from "./stubs/config_entries";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
@@ -23,10 +24,10 @@ import { mockLovelace } from "./stubs/lovelace";
|
||||
import { mockMediaPlayer } from "./stubs/media_player";
|
||||
import { mockPersistentNotification } from "./stubs/persistent_notification";
|
||||
import { mockRecorder } from "./stubs/recorder";
|
||||
import { mockTodo } from "./stubs/todo";
|
||||
import { mockSensor } from "./stubs/sensor";
|
||||
import { mockSystemLog } from "./stubs/system_log";
|
||||
import { mockTemplate } from "./stubs/template";
|
||||
import { mockTodo } from "./stubs/todo";
|
||||
import { mockTranslations } from "./stubs/translations";
|
||||
|
||||
@customElement("ha-demo")
|
||||
@@ -62,6 +63,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockEnergy(hass);
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
|
@@ -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
|
||||
|
62
package.json
62
package.json
@@ -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/helper-define-polyfill-provider": "0.6.1",
|
||||
"@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",
|
||||
@@ -175,8 +174,9 @@
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.11",
|
||||
"@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.0",
|
||||
"@typescript-eslint/parser": "7.7.0",
|
||||
"@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"
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240424.0"
|
||||
version = "20240501.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -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) {
|
||||
|
@@ -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",
|
||||
|
@@ -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"]) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { mdiArrowDown, mdiArrowUp, mdiChevronDown } from "@mdi/js";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@@ -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=${mdiChevronDown}
|
||||
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>
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -20,8 +20,6 @@ import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./search-input-outlined";
|
||||
|
||||
export const FILTER_NO_DEVICE = "__NO_DEVICE__";
|
||||
|
||||
@customElement("ha-filter-devices")
|
||||
export class HaFilterDevices extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -34,9 +32,6 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-device-option" })
|
||||
public noDeviceOption = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
@@ -74,12 +69,11 @@ export class HaFilterDevices extends LitElement {
|
||||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list class="ha-scrollbar">
|
||||
<mwc-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._devices(
|
||||
this.hass.devices,
|
||||
this._filter || "",
|
||||
this.noDeviceOption,
|
||||
this.value
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
@@ -143,13 +137,9 @@ export class HaFilterDevices extends LitElement {
|
||||
}
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(
|
||||
devices: HomeAssistant["devices"],
|
||||
filter: string,
|
||||
noDeviceOption: boolean,
|
||||
_value
|
||||
) => {
|
||||
const values = Object.values(devices)
|
||||
(devices: HomeAssistant["devices"], filter: string, _value) => {
|
||||
const values = Object.values(devices);
|
||||
return values
|
||||
.filter(
|
||||
(device) =>
|
||||
!filter ||
|
||||
@@ -162,28 +152,6 @@ export class HaFilterDevices extends LitElement {
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
if (noDeviceOption) {
|
||||
values.unshift({
|
||||
id: FILTER_NO_DEVICE,
|
||||
name: this.hass.localize("ui.panel.config.devices.no_device"),
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: [],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
identifiers: [],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: null,
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
});
|
||||
}
|
||||
return values;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -203,7 +171,7 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
for (const deviceId of this.value) {
|
||||
value.push(deviceId);
|
||||
if (this.type && deviceId !== FILTER_NO_DEVICE) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
||||
}
|
||||
}
|
||||
|
198
src/components/ha-filter-domains.ts
Normal file
198
src/components/ha-filter-domains.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-domain-icon";
|
||||
import "./search-input-outlined";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
@customElement("ha-filter-domains")
|
||||
export class HaFilterDomains extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.headers.domain"
|
||||
)}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list
|
||||
class="ha-scrollbar"
|
||||
@click=${this._handleItemClick}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._domains(this.hass.states, this._filter),
|
||||
(i) => i,
|
||||
(domain) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${domain}
|
||||
.selected=${(this.value || []).includes(domain)}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
brandFallback
|
||||
></ha-domain-icon>
|
||||
${domainToName(this.hass.localize, domain)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</mwc-list> `
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _domains = memoizeOne((states, filter) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
domains.add(computeDomain(entityId));
|
||||
});
|
||||
return Array.from(domains)
|
||||
.filter((domain) => !filter || domain.toLowerCase().includes(filter))
|
||||
.sort((a, b) => stringCompare(a, b, this.hass.locale.language));
|
||||
});
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
const listItem = ev.target.closest("ha-check-list-item");
|
||||
const value = listItem?.value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (this.value?.includes(value)) {
|
||||
this.value = this.value?.filter((val) => val !== value);
|
||||
} else {
|
||||
this.value = [...(this.value || []), value];
|
||||
}
|
||||
|
||||
listItem.selected = this.value.includes(value);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: 0;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
display: block;
|
||||
padding: 0 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-domains": HaFilterDomains;
|
||||
}
|
||||
}
|
@@ -71,7 +71,7 @@ export class HaFilterEntities extends LitElement {
|
||||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list class="ha-scrollbar">
|
||||
<mwc-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
this.hass.states,
|
||||
|
@@ -55,7 +55,11 @@ export class HaFilterIntegrations extends LitElement {
|
||||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list class="ha-scrollbar" @click=${this._handleItemClick}>
|
||||
<mwc-list
|
||||
class="ha-scrollbar"
|
||||
@click=${this._handleItemClick}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._integrations(this._manifests, this._filter, this.value),
|
||||
(i) => i.domain,
|
||||
|
@@ -71,6 +71,10 @@ export const computeInitialHaFormData = (
|
||||
if (selector.country?.countries?.length) {
|
||||
data[field.name] = selector.country.countries[0];
|
||||
}
|
||||
} else if ("language" in selector) {
|
||||
if (selector.language?.languages?.length) {
|
||||
data[field.name] = selector.language.languages[0];
|
||||
}
|
||||
} else if ("duration" in selector) {
|
||||
data[field.name] = {
|
||||
hours: 0,
|
||||
@@ -93,7 +97,9 @@ export const computeInitialHaFormData = (
|
||||
) {
|
||||
data[field.name] = {};
|
||||
} else {
|
||||
throw new Error("Selector not supported in initial form data");
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -1,13 +1,29 @@
|
||||
import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base";
|
||||
import { styles } from "@material/mwc-formfield/mwc-formfield.css";
|
||||
import { css } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-formfield")
|
||||
export class HaFormfield extends FormfieldBase {
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected override render() {
|
||||
const classes = {
|
||||
"mdc-form-field--align-end": this.alignEnd,
|
||||
"mdc-form-field--space-between": this.spaceBetween,
|
||||
"mdc-form-field--nowrap": this.nowrap,
|
||||
};
|
||||
|
||||
return html` <div class="mdc-form-field ${classMap(classes)}">
|
||||
<slot></slot>
|
||||
<label class="mdc-label" @click=${this._labelClick}
|
||||
><slot name="label">${this.label}</slot></label
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected _labelClick() {
|
||||
const input = this.input as HTMLInputElement | undefined;
|
||||
if (!input) return;
|
||||
@@ -39,6 +55,9 @@ export class HaFormfield extends FormfieldBase {
|
||||
margin-inline-end: 10px;
|
||||
margin-inline-start: inline;
|
||||
}
|
||||
.mdc-form-field {
|
||||
align-items: var(--ha-formfield-align-items, center);
|
||||
}
|
||||
.mdc-form-field > label {
|
||||
direction: var(--direction);
|
||||
margin-inline-start: 0;
|
||||
|
@@ -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;
|
||||
|
@@ -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
src/components/ha-ripple.ts
Normal file
63
src/components/ha-ripple.ts
Normal 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>`;
|
||||
}
|
||||
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "./ha-map";
|
||||
import type { HaMap } from "./ha-map";
|
||||
@@ -61,7 +61,8 @@ export class HaLocationsEditor extends LitElement {
|
||||
|
||||
@property({ type: Number }) public zoom = 16;
|
||||
|
||||
@property({ type: Boolean }) public darkMode = false;
|
||||
@property({ attribute: "theme-mode", type: String })
|
||||
public themeMode: ThemeMode = "auto";
|
||||
|
||||
@state() private _locationMarkers?: Record<string, Marker | Circle>;
|
||||
|
||||
@@ -133,7 +134,7 @@ export class HaLocationsEditor extends LitElement {
|
||||
.layers=${this._getLayers(this._circles, this._locationMarkers)}
|
||||
.zoom=${this.zoom}
|
||||
.autoFit=${this.autoFit}
|
||||
?forceDarkMode=${this.darkMode}
|
||||
.themeMode=${this.themeMode}
|
||||
></ha-map>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
|
@@ -1,32 +1,32 @@
|
||||
import { isToday } from "date-fns";
|
||||
import type {
|
||||
Circle,
|
||||
CircleMarker,
|
||||
LatLngTuple,
|
||||
LatLngExpression,
|
||||
LatLngTuple,
|
||||
Layer,
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
} from "leaflet";
|
||||
import { isToday } from "date-fns";
|
||||
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
||||
import { CSSResultGroup, PropertyValues, ReactiveElement, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
formatTimeWeekday,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
setupLeafletMap,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import {
|
||||
formatTimeWithSeconds,
|
||||
formatTimeWeekday,
|
||||
} from "../../common/datetime/format_time";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { loadPolyfillIfNeeded } from "../../resources/resize-observer.polyfill";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { HomeAssistant, ThemeMode } from "../../types";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
import "../ha-icon-button";
|
||||
import "./ha-entity-marker";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
|
||||
const getEntityId = (entity: string | HaMapEntity): string =>
|
||||
typeof entity === "string" ? entity : entity.entity_id;
|
||||
@@ -69,9 +69,8 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
@property({ type: Boolean }) public fitZones = false;
|
||||
|
||||
@property({ type: Boolean }) public forceDarkMode = false;
|
||||
|
||||
@property({ type: Boolean }) public forceLightMode = false;
|
||||
@property({ attribute: "theme-mode", type: String })
|
||||
public themeMode: ThemeMode = "auto";
|
||||
|
||||
@property({ type: Number }) public zoom = 14;
|
||||
|
||||
@@ -156,8 +155,7 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
if (
|
||||
!changedProps.has("forceDarkMode") &&
|
||||
!changedProps.has("forceLightMode") &&
|
||||
!changedProps.has("themeMode") &&
|
||||
(!changedProps.has("hass") ||
|
||||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
|
||||
) {
|
||||
@@ -166,14 +164,18 @@ export class HaMap extends ReactiveElement {
|
||||
this._updateMapStyle();
|
||||
}
|
||||
|
||||
private get _darkMode() {
|
||||
return (
|
||||
this.themeMode === "dark" ||
|
||||
(this.themeMode === "auto" && Boolean(this.hass.themes.darkMode))
|
||||
);
|
||||
}
|
||||
|
||||
private _updateMapStyle(): void {
|
||||
const darkMode =
|
||||
!this.forceLightMode &&
|
||||
(this.forceDarkMode || (this.hass.themes.darkMode ?? false));
|
||||
const map = this.renderRoot.querySelector("#map");
|
||||
map!.classList.toggle("dark", darkMode);
|
||||
map!.classList.toggle("forced-dark", this.forceDarkMode);
|
||||
map!.classList.toggle("forced-light", this.forceLightMode);
|
||||
map!.classList.toggle("dark", this._darkMode);
|
||||
map!.classList.toggle("forced-dark", this.themeMode === "dark");
|
||||
map!.classList.toggle("forced-light", this.themeMode === "light");
|
||||
}
|
||||
|
||||
private async _loadMap(): Promise<void> {
|
||||
@@ -403,13 +405,7 @@ export class HaMap extends ReactiveElement {
|
||||
"--dark-primary-color"
|
||||
);
|
||||
|
||||
const className = this.forceLightMode
|
||||
? "light"
|
||||
: this.forceDarkMode
|
||||
? "dark"
|
||||
: this.hass.themes.darkMode
|
||||
? "dark"
|
||||
: "light";
|
||||
const className = this._darkMode ? "dark" : "light";
|
||||
|
||||
for (const entity of this.entities) {
|
||||
const stateObj = hass.states[getEntityId(entity)];
|
||||
|
@@ -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);
|
||||
});
|
||||
|
@@ -44,6 +44,7 @@ export interface IntegrationManifest {
|
||||
| "local_polling"
|
||||
| "local_push";
|
||||
single_config_entry?: boolean;
|
||||
version?: string;
|
||||
}
|
||||
export interface IntegrationSetup {
|
||||
domain: string;
|
||||
|
@@ -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":
|
||||
|
@@ -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}`
|
||||
|
@@ -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)];
|
||||
};
|
||||
|
@@ -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>(
|
||||
|
@@ -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) {
|
||||
|
@@ -16,21 +16,23 @@ class MoreInfoCounter extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const disabled = isUnavailableState(this.stateObj!.state);
|
||||
const disabled = isUnavailableState(this.stateObj.state);
|
||||
|
||||
return html`
|
||||
<div class="actions">
|
||||
<mwc-button
|
||||
.action=${"increment"}
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
.disabled=${disabled ||
|
||||
Number(this.stateObj.state) === this.stateObj.attributes.maximum}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.increment")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
.action=${"decrement"}
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
.disabled=${disabled ||
|
||||
Number(this.stateObj.state) === this.stateObj.attributes.minimum}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.decrement")}
|
||||
</mwc-button>
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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,4 +1,3 @@
|
||||
import "@material/mwc-ripple";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
|
@@ -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" />
|
||||
|
@@ -41,7 +41,7 @@ import type { HomeAssistant } from "../types";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
|
||||
const AMSTERDAM: [number, number] = [52.3731339, 4.8903147];
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
const darkMql = matchMedia("(prefers-color-scheme: dark)");
|
||||
const LOCATION_MARKER_ID = "location";
|
||||
|
||||
@customElement("onboarding-location")
|
||||
@@ -199,7 +199,7 @@ class OnboardingLocation extends LitElement {
|
||||
this._highlightedMarker
|
||||
)}
|
||||
zoom="14"
|
||||
.darkMode=${mql.matches}
|
||||
.themeMode=${darkMql.matches ? "dark" : "light"}
|
||||
.disabled=${this._working}
|
||||
@location-updated=${this._locationChanged}
|
||||
@marker-clicked=${this._markerClicked}
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -97,7 +97,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
<ha-automation-trigger
|
||||
role="region"
|
||||
aria-labelledby="triggers-heading"
|
||||
.triggers=${this.config.trigger}
|
||||
.triggers=${this.config.trigger || []}
|
||||
.path=${["trigger"]}
|
||||
@value-changed=${this._triggerChanged}
|
||||
@item-moved=${this._itemMoved}
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -1,16 +1,19 @@
|
||||
import { mdiContentCopy, mdiHelpCircle } from "@mdi/js";
|
||||
import { mdiContentCopy, mdiEye, mdiEyeOff, mdiHelpCircle } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-settings-row";
|
||||
import "../../../../components/ha-switch";
|
||||
// eslint-disable-next-line
|
||||
import { formatDate } from "../../../../common/datetime/format_date";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import {
|
||||
CloudStatusLoggedIn,
|
||||
@@ -20,8 +23,8 @@ import {
|
||||
} from "../../../../data/cloud";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
|
||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
|
||||
|
||||
@customElement("cloud-remote-pref")
|
||||
export class CloudRemotePref extends LitElement {
|
||||
@@ -29,6 +32,10 @@ export class CloudRemotePref extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _unmaskedUrl = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.cloudStatus) {
|
||||
return nothing;
|
||||
@@ -109,36 +116,189 @@ export class CloudRemotePref extends LitElement {
|
||||
)}
|
||||
></ha-alert>
|
||||
`
|
||||
: ""}
|
||||
${this.hass.localize("ui.panel.config.cloud.account.remote.info")}
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.cloud.account.remote.${
|
||||
remote_connected
|
||||
? "instance_is_available"
|
||||
: "instance_will_be_available"
|
||||
}`
|
||||
)}
|
||||
<a
|
||||
href="https://${remote_domain}"
|
||||
target="_blank"
|
||||
class="break-word"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.nabu_casa_url"
|
||||
)}</a
|
||||
>.
|
||||
<ha-svg-icon
|
||||
.url=${`https://${remote_domain}`}
|
||||
@click=${this._copyURL}
|
||||
.path=${mdiContentCopy}
|
||||
></ha-svg-icon>
|
||||
: strict_connection === "drop_connection"
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
`ui.panel.config.cloud.account.remote.drop_connection_warning_title`
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.cloud.account.remote.drop_connection_warning`
|
||||
)}</ha-alert
|
||||
>`
|
||||
: nothing}
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.cloud.account.remote.info")}
|
||||
</p>
|
||||
${remote_connected
|
||||
? nothing
|
||||
: html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.info_instance_will_be_available"
|
||||
)}
|
||||
</p>
|
||||
`}
|
||||
<div class="url-container">
|
||||
<div class="textfield-container">
|
||||
<ha-textfield
|
||||
.value=${this._unmaskedUrl
|
||||
? `https://${remote_domain}`
|
||||
: "https://•••••••••••••••••.ui.nabu.casa"}
|
||||
readonly
|
||||
.suffix=${
|
||||
// reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.cloud.account.remote.${this._unmaskedUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
@click=${this._toggleUnmaskedUrl}
|
||||
.path=${this._unmaskedUrl ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-button
|
||||
.url=${`https://${remote_domain}`}
|
||||
@click=${this._copyURL}
|
||||
unelevated
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.copy_link"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.advanced_options"
|
||||
"ui.panel.config.cloud.account.remote.security_options"
|
||||
)}
|
||||
>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_secondary"
|
||||
)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
|
||||
<div class="strict-connection-container">
|
||||
<ha-formfield>
|
||||
<ha-radio
|
||||
name="strict-connection-mode"
|
||||
value="disabled"
|
||||
.checked=${strict_connection === "disabled"}
|
||||
@change=${this._strictConnectionModeChanged}
|
||||
></ha-radio>
|
||||
<div slot="label">
|
||||
<div class="primary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_disabled"
|
||||
)}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_disabled_secondary"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-formfield>
|
||||
|
||||
<ha-formfield>
|
||||
<ha-radio
|
||||
name="strict-connection-mode"
|
||||
value="guard_page"
|
||||
.checked=${strict_connection === "guard_page"}
|
||||
@change=${this._strictConnectionModeChanged}
|
||||
></ha-radio>
|
||||
<div slot="label">
|
||||
<div class="primary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page"
|
||||
)}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page_secondary"
|
||||
)}
|
||||
${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>
|
||||
|
||||
<ha-formfield>
|
||||
<ha-radio
|
||||
name="strict-connection-mode"
|
||||
value="drop_connection"
|
||||
.checked=${strict_connection === "drop_connection"}
|
||||
@change=${this._strictConnectionModeChanged}
|
||||
></ha-radio>
|
||||
<div slot="label">
|
||||
<div class="primary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection"
|
||||
)}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection_secondary"
|
||||
)}
|
||||
${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>
|
||||
</div>
|
||||
|
||||
${strict_connection !== "disabled"
|
||||
? html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link_secondary"
|
||||
)}</span
|
||||
>
|
||||
<ha-button @click=${this._createLoginUrl}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_create_link"
|
||||
)}</ha-button
|
||||
>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<hr />
|
||||
<ha-settings-row wrap-heading>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.external_activation"
|
||||
@@ -154,62 +314,8 @@ export class CloudRemotePref extends LitElement {
|
||||
@change=${this._toggleAllowRemoteEnabledChanged}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_secondary"
|
||||
)}</span
|
||||
>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_mode"
|
||||
)}
|
||||
@selected=${this._setStrictConnectionMode}
|
||||
naturalMenuWidth
|
||||
.value=${strict_connection}
|
||||
>
|
||||
<ha-list-item value="disabled">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_modes.disabled"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="guard_page">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_modes.guard_page"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="drop_connection">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_modes.drop_connection"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-select>
|
||||
</ha-settings-row>
|
||||
${strict_connection !== "disabled"
|
||||
? html` <ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link_secondary"
|
||||
)}</span
|
||||
>
|
||||
<ha-button @click=${this._createLoginUrl}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_create_link"
|
||||
)}</ha-button
|
||||
>
|
||||
</ha-settings-row>`
|
||||
: nothing}
|
||||
<ha-settings-row>
|
||||
<hr />
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.certificate_info"
|
||||
@@ -249,6 +355,10 @@ export class CloudRemotePref extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleUnmaskedUrl(): void {
|
||||
this._unmaskedUrl = !this._unmaskedUrl;
|
||||
}
|
||||
|
||||
private async _toggleChanged(ev) {
|
||||
const toggle = ev.target as HaSwitch;
|
||||
|
||||
@@ -279,15 +389,21 @@ export class CloudRemotePref extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _setStrictConnectionMode(ev) {
|
||||
const mode = ev.target.value;
|
||||
private async _strictConnectionModeChanged(ev) {
|
||||
const toggle = ev.target as HaRadio;
|
||||
|
||||
if (ev.target.value === this.cloudStatus?.prefs.strict_connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateCloudPref(this.hass, {
|
||||
strict_connection: mode,
|
||||
strict_connection: ev.target.value,
|
||||
});
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
} catch (err: any) {
|
||||
alert(err.message);
|
||||
toggle.checked = !toggle.checked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,17 +432,22 @@ export class CloudRemotePref extends LitElement {
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link_created_message"
|
||||
)}
|
||||
<pre>${result.response.url}</pre>
|
||||
<ha-button
|
||||
.url=${result.response.url}
|
||||
@click=${this._copyURL}
|
||||
unelevated
|
||||
<div
|
||||
style="display: flex; align-items: center; gap: 8px; margin-top: 8px;"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_copy_link"
|
||||
)}
|
||||
</ha-button>`,
|
||||
<ha-textfield .value=${result.response.url} readonly></ha-textfield>
|
||||
<ha-button
|
||||
style="flex-basis: 180px;"
|
||||
.url=${result.response.url}
|
||||
@click=${this._copyURL}
|
||||
unelevated
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.copy_link"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>`,
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, { text: err.message });
|
||||
@@ -343,8 +464,8 @@ export class CloudRemotePref extends LitElement {
|
||||
}
|
||||
.header-actions {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
inset-inline-end: 24px;
|
||||
right: 16px;
|
||||
inset-inline-end: 16px;
|
||||
inset-inline-start: initial;
|
||||
top: 24px;
|
||||
display: flex;
|
||||
@@ -378,17 +499,74 @@ export class CloudRemotePref extends LitElement {
|
||||
.card-actions a {
|
||||
text-decoration: none;
|
||||
}
|
||||
ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
ha-expansion-panel {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-formfield {
|
||||
margin-top: 8px;
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
border-top: none !important;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-content-padding: 0 16px;
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.url-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.textfield-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.textfield-container ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
.toggle-unmasked-url {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-formfield {
|
||||
margin-left: -12px;
|
||||
margin-inline-start: -12px;
|
||||
margin-inline-end: initial;
|
||||
--ha-formfield-align-items: start;
|
||||
}
|
||||
.strict-connection-container {
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.strict-connection-container ha-formfield {
|
||||
--ha-formfield-align-items: start;
|
||||
}
|
||||
.strict-connection-container .primary {
|
||||
font-size: 14px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.strict-connection-container .secondary {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
margin: 8px 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -52,6 +52,8 @@ import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-domains";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-integrations";
|
||||
import "../../../components/ha-filter-labels";
|
||||
@@ -96,7 +98,6 @@ import { configSections } from "../ha-panel-config";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { FILTER_NO_DEVICE } from "../../../components/ha-filter-devices";
|
||||
|
||||
export interface StateEntity
|
||||
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
|
||||
@@ -115,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")
|
||||
@@ -197,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"),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -309,7 +329,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
type: "icon",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
width: "68px",
|
||||
template: (entry) =>
|
||||
entry.unavailable ||
|
||||
@@ -338,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(
|
||||
@@ -353,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,
|
||||
@@ -388,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>();
|
||||
@@ -443,41 +486,44 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
entryIds.includes(entity.config_entry_id))
|
||||
);
|
||||
filter.value!.forEach((domain) => filteredDomains.add(domain));
|
||||
} else if (key === "ha-filter-domains" && filter.value?.length) {
|
||||
filteredEntities = filteredEntities.filter((entity) =>
|
||||
filter.value?.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
} else if (key === "ha-filter-labels" && filter.value?.length) {
|
||||
filteredEntities = filteredEntities.filter((entity) =>
|
||||
entity.labels.some((lbl) => filter.value!.includes(lbl))
|
||||
);
|
||||
} else if (filter.items) {
|
||||
filteredEntities = filteredEntities.filter(
|
||||
(entity) =>
|
||||
filter.items!.has(entity.entity_id) ||
|
||||
(key === "ha-filter-devices" &&
|
||||
filter.value?.includes(FILTER_NO_DEVICE) &&
|
||||
!entity.device_id)
|
||||
filteredEntities = filteredEntities.filter((entity) =>
|
||||
filter.items!.has(entity.entity_id)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -499,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"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -785,8 +840,16 @@ ${
|
||||
.expanded=${this._expandedFilter === "ha-filter-devices"}
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
no-device-option
|
||||
></ha-filter-devices>
|
||||
<ha-filter-domains
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-domains"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
.expanded=${this._expandedFilter === "ha-filter-domains"}
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-domains>
|
||||
<ha-filter-integrations
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-integrations"]?.value}
|
||||
@@ -852,7 +915,7 @@ ${
|
||||
protected firstUpdated() {
|
||||
this._filters = {
|
||||
"ha-filter-states": {
|
||||
value: ["unavailable", "readonly"],
|
||||
value: ["enabled"],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
@@ -867,10 +930,7 @@ ${
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-states": {
|
||||
value: [
|
||||
...(this._filters["ha-filter-states"]?.value || []),
|
||||
"disabled",
|
||||
],
|
||||
value: [],
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-integrations": {
|
||||
@@ -883,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%;
|
||||
|
@@ -47,6 +47,7 @@ class DialogZHAReconfigureDevice extends LitElement {
|
||||
|
||||
public showDialog(params: ZHAReconfigureDeviceDialogParams): void {
|
||||
this._params = params;
|
||||
this._clusterConfigurationStatuses = new Map();
|
||||
this._stages = undefined;
|
||||
}
|
||||
|
||||
|
@@ -424,7 +424,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
? {
|
||||
physics: {
|
||||
barnesHut: {
|
||||
springConstant: 0.05,
|
||||
springConstant: 0,
|
||||
avoidOverlap: 10,
|
||||
damping: 0.09,
|
||||
},
|
||||
|
@@ -64,7 +64,7 @@ export const showRepairsFlowDialog = (
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
: step.reason;
|
||||
},
|
||||
|
||||
renderShowFormStepHeader(hass, step) {
|
||||
|
@@ -122,7 +122,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
<ha-automation-action
|
||||
role="region"
|
||||
aria-labelledby="sequence-heading"
|
||||
.actions=${this.config.sequence}
|
||||
.actions=${this.config.sequence || []}
|
||||
.path=${["sequence"]}
|
||||
@value-changed=${this._sequenceChanged}
|
||||
@item-moved=${this._itemMoved}
|
||||
|
@@ -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);
|
||||
|
@@ -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)
|
||||
|
@@ -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,
|
||||
|
7
src/panels/lovelace/card-features/common/filter-modes.ts
Normal file
7
src/panels/lovelace/card-features/common/filter-modes.ts
Normal file
@@ -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,
|
||||
|
@@ -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 {
|
||||
|
@@ -159,6 +159,9 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
? false
|
||||
: this.hass.themes.darkMode;
|
||||
|
||||
const themeMode =
|
||||
this._config.theme_mode || (this._config.dark_mode ? "dark" : "auto");
|
||||
|
||||
return html`
|
||||
<ha-card id="card" .header=${this._config.title}>
|
||||
<div id="root">
|
||||
@@ -169,9 +172,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
.paths=${this._getHistoryPaths(this._config, this._stateHistory)}
|
||||
.autoFit=${this._config.auto_fit || false}
|
||||
.fitZones=${this._config.fit_zones}
|
||||
?forceDarkMode=${this._config.theme_mode === "dark" ||
|
||||
this._config.dark_mode}
|
||||
?forceLightMode=${this._config.theme_mode === "light"}
|
||||
.themeMode=${themeMode}
|
||||
interactiveZones
|
||||
renderPassive
|
||||
></ha-map>
|
||||
|
@@ -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;
|
||||
|
@@ -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}`
|
||||
);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user