Compare commits

..

69 Commits

Author SHA1 Message Date
Aidan Timson
bc52ab410c Split repeat building blocks in picker (#26385)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-08-11 11:07:41 +02:00
karwosts
3b0220fa92 Support multiple for StateSelector (#25716)
* Support `multiple` for StateSelector

* lint

* Fixup after merge
2025-08-11 11:24:37 +03:00
Wendelin
a60c9f788d Fix first letter uppercase in some buttons (#26485) 2025-08-11 10:47:06 +03:00
karwosts
d9c297c06a Improve robustness of UI for if/then action (#26477) 2025-08-11 08:40:42 +03:00
renovate[bot]
3789bebb2b Update dependency @bundle-stats/plugin-webpack-filter to v4.21.2 (#26480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 08:36:51 +03:00
renovate[bot]
bbecf5f368 Update dependency hls.js to v1.6.9 (#26474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-10 14:30:38 +02:00
renovate[bot]
e580b30219 Update dependency @rsdoctor/rspack-plugin to v1.2.0 (#26471)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-10 14:30:12 +02:00
renovate[bot]
ed8c8ad3e3 Update dependency @rsdoctor/rspack-plugin to v1.1.11 (#26470)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-09 15:19:04 +02:00
Simon Lamon
4f61d5689b Fix diagnostic download (#26466)
Diagnostic fix 2
2025-08-09 15:17:59 +02:00
renovate[bot]
60a18185d7 Update dependency @tsparticles/engine to v3.9.1 (#26420)
* Update dependency @tsparticles/engine to v3.9.1
2025-08-09 06:19:38 +00:00
karwosts
e0246b8488 Support button feature for input_button (#26444) 2025-08-09 07:52:14 +02:00
renovate[bot]
1cd0fae84a Update dependency @awesome.me/webawesome to v3.0.0-beta.4 (#26465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 21:42:37 +02:00
renovate[bot]
e8a1ebbff4 Update dependency fs-extra to v11.3.1 (#26464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 19:38:28 +02:00
karwosts
c5010b8502 Fix some date-range bugs (#26441) 2025-08-08 16:57:54 +03:00
Petar Petrov
a7db401b62 Show sankey chart in vertical layout on mobile (#26439)
* Show sankey chart in vertical layout on mobile

* ts fix
2025-08-08 16:37:44 +03:00
Petar Petrov
49c7dad6eb Font improvements for Sankey chart (#26438)
* Use theme vars for sankey chart font

* improve font size calculation
2025-08-08 08:43:59 +02:00
Aidan Timson
521c3d40b7 Add missing button feature translation (#26437) 2025-08-08 08:42:45 +02:00
renovate[bot]
709a1d2ef0 Update dependency typescript-eslint to v8.39.0 (#26446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 08:42:06 +02:00
renovate[bot]
3c5d7b97d1 Update dependency core-js to v3.45.0 (#26449)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 08:41:49 +02:00
renovate[bot]
9165c8bc57 Update dependency hls.js to v1.6.8 (#26451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 08:40:58 +02:00
Luke Mondy
0b3e4eab23 Add 'Not' to lovelace visibility conditions (#26408)
:Add 'Not' to lovelace visibility conditions
2025-08-07 16:40:27 +03:00
Wendelin
39d14c943c Revert "Add Mobile team and design has codeowner of the theme colors" (#26432)
Revert "Add Mobile team and design has codeowner of the theme colors (#26428)"

This reverts commit 9588987e30.
2025-08-07 15:06:41 +02:00
Wendelin
09469be93f Fix css var naming --ha-color-border-primary (#26433) 2025-08-07 15:06:13 +02:00
karwosts
6e215870ef Fix mqtt config panel (#26422) 2025-08-07 16:06:07 +03:00
Wendelin
d5985dcaaf Fix button start/end slot margins, add reduce-left-padding flag (#26431)
* Fix button start/end slot margins, add reduce-left-padding flag

* Always reduce padding when icons are there

* Revert icon padding changes
2025-08-07 15:05:56 +02:00
Timothy
bbd9d8887d Fix typo in Neutral80 color (#26430) 2025-08-07 10:07:12 +00:00
Timothy
9588987e30 Add Mobile team and design has codeowner of the theme colors (#26428) 2025-08-07 09:56:47 +00:00
Wendelin
52c05a4426 Fix plain button in legacy browsers (#26426) 2025-08-07 09:50:05 +02:00
renovate[bot]
e8224df4e5 Update dependency echarts to v6 (#26356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-07 09:41:59 +03:00
karwosts
83a6df1621 Fix a dangerous button color (#26418) 2025-08-07 07:40:31 +02:00
renovate[bot]
c46ebc8d3e Update dependency marked to v16.1.2 (#26423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-07 07:38:25 +02:00
Aidan Timson
fca530411f Fix Mod-S (Ctrl-S/Cmd-S) support for automation/scene/script YAML editors (#26412)
* Fix automation and script yaml mode Mod-S (Ctrl/Cmd-S) support

* Fix manual script editor

* Fix manual automation editor save

* Fix scene yaml mode
2025-08-06 18:52:41 +02:00
Norbert Rittel
c2c64b9923 Fix summary for Choose building block by counting options (#26409)
* Fix summary for Choose building block

* Sentence-case "If-then" to match label of that building block
2025-08-06 18:52:03 +02:00
renovate[bot]
9968c27a8e Update dependency lint-staged to v16.1.4 (#26414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 18:45:36 +02:00
Wendelin
96796ac5da Add border radius css var ha prefix (#26411) 2025-08-06 12:59:34 +00:00
Bram Kragten
37def6d3e4 add save button to AI suggestions settings (#26407) 2025-08-06 14:27:58 +02:00
Bram Kragten
013d603ba0 Fix buttons in button row (#26405) 2025-08-06 11:15:09 +00:00
Bram Kragten
b76407d28d Update zwave js buttons (#26404) 2025-08-06 13:13:22 +02:00
Bram Kragten
4e969ccf09 Update color variables (#26403) 2025-08-06 10:42:21 +00:00
Bram Kragten
cdfd6431c3 Fix colors in network graphs (#26397) 2025-08-05 15:14:21 +02:00
Bram Kragten
c363995718 Fix network graph not rendering (#26396) 2025-08-05 15:05:23 +02:00
Jan-Philipp Benecke
53497aa632 Add localization for third-party data reporting in the Z-Wave JS dashboard (#26395)
* Add localization for third-party data reporting in the Z-Wave JS dashboard

* Run prettier
2025-08-05 13:00:14 +02:00
Stefan Agner
8d89b0e57f Fix System information dialog unhealthy/unsupported list (#26393)
The System Information dialog was not displaying translated list of
unhealthy and unsupported reasons because the wrong translation keys
were used. This commit updates the translation keys to the correct
ones.
2025-08-05 12:10:51 +02:00
Stefan Agner
92cf8b5579 Remove eMMC specific references in disk life time handling (#26379)
* Remove eMMC specific references in disk life time handling

Remove eMMC specific calculations and references in the disk life
time handling to generalize the code for all disk types. This includes
updating translations and UI components to reflect a more generic
approach to disk life time metrics.

* Assume 30 MB/s as the speed for disk operations

The previous code tried to estimate based on disk type, 30 MB/s for
eMMC devices and 10 MB/s for others. However, this did not work
correctly since the disk_life_time returns null for non-eMMC devices,
leading to 30 MB/s being used for all devices.

Now disk_life_time is not a eMMC indicator anymore. Simply assume a
constant speed of 30 MB/s for all disk operations explicitly.
2025-08-05 10:42:42 +02:00
karwosts
6068c32176 Pass narrow through parallel/sequence automation actions (#26386) 2025-08-04 17:51:17 +02:00
karwosts
38893324af Fix energy now button (#26384)
Update hui-energy-period-selector.ts
2025-08-04 16:19:26 +02:00
Wendelin
a39ab3c174 Improve ha button radius variables (#26382)
* Improve ha button radius variables

* fixes

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-08-04 12:58:12 +00:00
Christoph
797d2be5bf show spinner on update button (during update installation) (#26110)
* scroll to top when installing an update

* Revert "scroll to top when installing an update"

This reverts commit d0051b0c4c.

* add progress spinner to update button

* refactor disabled logic for update/skip button

* do not run update when disabled button is clicked

* refactor: use new ha-button to show progress

* refactor: move functions to update.ts
2025-08-04 14:51:38 +02:00
Wendelin
99a91e1019 Improve neutral color palette (#26381)
Improve neutral, add docs, removed unused var.
2025-08-04 12:24:42 +00:00
Wendelin
5de8d07ce0 Fix ha-buttons (#26373)
* Fix ha-button supervisor network

* Fix button appearance for entity row

* Fix logs button menu mobile width

* Fix new logs indicator
2025-08-04 14:18:31 +02:00
dependabot[bot]
3a31a4a721 Bump home-assistant/wheels from 2025.03.0 to 2025.07.0 (#26375)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-04 11:01:02 +02:00
Squazel
05f4419a92 Fix picture-glance card icon styling for unavailable/unknown entities (#26352) 2025-08-04 08:16:11 +02:00
renovate[bot]
5ea8feb86b Update rspack monorepo to v1.4.11 (#26365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 06:52:21 +02:00
Jan-Philipp Benecke
8fd70b3ae6 Improve Z-Wave JS config dashboard styling (#26368) 2025-08-04 06:50:35 +02:00
renovate[bot]
343aa40bc8 Update dependency @types/luxon to v3.7.1 (#26351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-03 08:10:32 +02:00
Jan-Philipp Benecke
6022f9a77e Do not show AI suggestion button when no inputs in save dialog (#26357) 2025-08-03 08:10:06 +02:00
renovate[bot]
bd9de0680e Update dependency @types/luxon to v3.7.0 (#26342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-01 17:09:44 +02:00
Simon Lamon
b8000d5bc1 Fix diagnostic download on integration level (#26341) 2025-08-01 13:16:09 +02:00
Wendelin
c6efa1127f Fix dialog secondary button design (#26344) 2025-08-01 13:13:42 +02:00
Bram Kragten
688a3d91d3 Add support for sub config flows in conversation agent picker (#26336) 2025-08-01 13:13:28 +02:00
Timothy
68151a2a70 Add Bruno and Timo as codeowners of the external_app folders (#26345) 2025-08-01 11:49:47 +02:00
Wendelin
c2ca556151 Fix line-height, fix script editor buttons (#26337)
* Fix line-height

* Fix script root buttons
2025-07-31 16:52:36 +02:00
Wendelin
df86b27af4 Use tilecard button feature editor (#26335)
Use button feature editor
2025-07-31 13:27:40 +02:00
Wendelin
eba1f401cc Fix ha-button with missing label and links (#26332) 2025-07-31 12:40:17 +02:00
Wendelin
19c2f9c9e8 Revert "Use query params instead of path for media browser navigate ids" (#26333) 2025-07-31 12:38:52 +02:00
Bram Kragten
4250447d14 Fix area picker text alignment in voice wizard (#26330) 2025-07-31 11:52:33 +02:00
Joost Lekkerkerker
4666197f28 Use underscores in AI task name (#26327) 2025-07-30 21:52:09 +02:00
Franck Nijhof
a5ca36c93f Add weekdays to time trigger (#25908)
* Add weekdays to time trigger

* Update src/translations/en.json

Co-authored-by: Norbert Rittel <norbert@rittel.de>

* Localization changes

---------

Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-07-30 19:45:10 +02:00
Norbert Rittel
a88950e16c Correct the setup steps for Matter sharing in Google Home app (#26322)
Correct the setup steps in the Google Home app
2025-07-30 19:40:41 +02:00
58 changed files with 956 additions and 586 deletions

View File

@@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2025.03.0
uses: home-assistant/wheels@2025.07.0
with:
abi: cp313
tag: musllinux_1_2

8
CODEOWNERS Normal file
View File

@@ -0,0 +1,8 @@
# People marked here will be automatically requested for a review
# when the code that they own is touched.
# https://github.com/blog/2392-introducing-code-owners
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Part of the frontend that mobile developper should review
src/external_app/ @bgoncal @TimoPtr
test/external_app/ @bgoncal @TimoPtr

View File

@@ -64,4 +64,4 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/butt
**CSS Custom Properties**
- `--ha-button-height` - Height of the button.
- `--ha-button-border-radius` - Border radius of the button. Defaults to `var(--border-radius-pill)`.
- `--ha-button-border-radius` - Border radius of the button. Defaults to `var(--ha-border-radius-pill)`.

View File

@@ -2,13 +2,13 @@ import { mdiDelete, mdiPlus } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-button";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
addHassioDockerRegistry,

View File

@@ -7,10 +7,14 @@ import { fireEvent } from "../../../../src/common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-tooltip";
import "../../../../src/components/ha-svg-icon";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-textfield";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-tooltip";
import type {
HassioAddonInfo,
HassioAddonRepository,
@@ -24,10 +28,6 @@ import {
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {

View File

@@ -26,7 +26,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@awesome.me/webawesome": "3.0.0-beta.3",
"@awesome.me/webawesome": "3.0.0-beta.4",
"@babel/runtime": "7.28.2",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6",
@@ -88,7 +88,7 @@
"@shoelace-style/shoelace": "2.20.1",
"@swc/helpers": "0.5.17",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.8.1",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.7.9",
"@vaadin/vaadin-themable-mixin": "24.7.9",
@@ -100,7 +100,7 @@
"barcode-detector": "3.0.5",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.44.0",
"core-js": "3.45.0",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.1.0",
@@ -108,12 +108,12 @@
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"echarts": "5.6.0",
"echarts": "6.0.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.7",
"hls.js": "1.6.9",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.16",
@@ -124,7 +124,7 @@
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.1",
"marked": "16.1.1",
"marked": "16.1.2",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@@ -154,14 +154,14 @@
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.0",
"@babel/preset-env": "7.28.0",
"@bundle-stats/plugin-webpack-filter": "4.21.1",
"@bundle-stats/plugin-webpack-filter": "4.21.2",
"@lokalise/node-api": "15.0.0",
"@octokit/auth-oauth-device": "8.0.1",
"@octokit/plugin-retry": "8.0.1",
"@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.1.10",
"@rspack/cli": "1.4.10",
"@rspack/core": "1.4.10",
"@rsdoctor/rspack-plugin": "1.2.0",
"@rspack/cli": "1.4.11",
"@rspack/core": "1.4.11",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22",
"@types/chromecast-caf-sender": "1.0.11",
@@ -173,7 +173,7 @@
"@types/leaflet-draw": "1.0.12",
"@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.6.2",
"@types/luxon": "3.7.1",
"@types/mocha": "10.0.10",
"@types/qrcode": "1.5.5",
"@types/sortablejs": "1.15.8",
@@ -195,7 +195,7 @@
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "3.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.3.0",
"fs-extra": "11.3.1",
"glob": "11.0.3",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
@@ -205,7 +205,7 @@
"husky": "9.1.7",
"jsdom": "26.1.0",
"jszip": "3.10.1",
"lint-staged": "16.1.2",
"lint-staged": "16.1.4",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -219,7 +219,7 @@
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.8.3",
"typescript-eslint": "8.38.0",
"typescript-eslint": "8.39.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4",
"webpack-stats-plugin": "1.1.3",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20250806.0"
version = "20250730.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@@ -132,15 +132,13 @@ export const shiftDateRange = (
end = calcDate(endDate, addDays, locale, config, difference);
} else {
const difference =
((calcDateDifferenceProperty(
(calcDateDifferenceProperty(
endDate,
startDate,
differenceInMilliseconds,
locale,
config
) as number) +
1) *
(forward ? 1 : -1);
) as number) * (forward ? 1 : -1);
start = calcDate(startDate, addMilliseconds, locale, config, difference);
end = calcDate(endDate, addMilliseconds, locale, config, difference);
}

View File

@@ -387,24 +387,25 @@ export class HaChartBase extends LitElement {
lastTipX = e.x;
lastTipY = e.y;
this.chart?.setOption({
xAxis: ensureArray(this.chart?.getOption().xAxis as any).map(
(axis: XAXisOption) =>
axis.show
? {
...axis,
axisPointer: {
...axis.axisPointer,
status: "show",
handle: {
color: style.getPropertyValue("primary-color"),
margin: 0,
size: 20,
...axis.axisPointer?.handle,
show: true,
},
xAxis: ensureArray(
(this.chart?.getOption().xAxis as any) ?? []
).map((axis: XAXisOption) =>
axis.show
? {
...axis,
axisPointer: {
...axis.axisPointer,
status: "show",
handle: {
color: style.getPropertyValue("primary-color"),
margin: 0,
size: 20,
...axis.axisPointer?.handle,
show: true,
},
}
: axis
},
}
: axis
),
});
});
@@ -417,21 +418,22 @@ export class HaChartBase extends LitElement {
return;
}
this.chart?.setOption({
xAxis: ensureArray(this.chart?.getOption().xAxis as any).map(
(axis: XAXisOption) =>
axis.show
? {
...axis,
axisPointer: {
...axis.axisPointer,
handle: {
...axis.axisPointer?.handle,
show: false,
},
status: "hide",
xAxis: ensureArray(
(this.chart?.getOption().xAxis as any) ?? []
).map((axis: XAXisOption) =>
axis.show
? {
...axis,
axisPointer: {
...axis.axisPointer,
handle: {
...axis.axisPointer?.handle,
show: false,
},
}
: axis
status: "hide",
},
}
: axis
),
});
this.chart?.dispatchAction({
@@ -598,6 +600,13 @@ export class HaChartBase extends LitElement {
textBorderWidth: 2,
},
},
sankey: {
label: {
color: style.getPropertyValue("--primary-text-color"),
textBorderColor: style.getPropertyValue("--primary-background-color"),
textBorderWidth: 2,
},
},
categoryAxis: {
axisLine: { show: false },
axisTick: { show: false },

View File

@@ -186,23 +186,22 @@ export class HaSankeyChart extends LitElement {
""
);
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
const availableWidth = params.rect.width + 6;
const fontSize = Math.min(
FONT_SIZE,
(params.rect.width / wordWidth) * FONT_SIZE
(availableWidth / wordWidth) * FONT_SIZE
);
return {
fontSize: fontSize > 1 ? fontSize : 0,
width: params.rect.width,
width: availableWidth,
align: "center",
dy: -2, // shift up or the lowest row labels may be cut off
};
}
// estimate the number of lines after the label is wrapped
// this is a very rough estimate, but it works for now
const lineCount = Math.ceil(params.labelRect.width / labelSpace);
// `overflow: "break"` allows the label to overflow outside its height, so we need to account for that
const availableHeight = params.rect.height + 8; // account for the margin
const fontSize = Math.min(
(params.rect.height / lineCount) * FONT_SIZE,
(availableHeight / params.labelRect.height) * FONT_SIZE,
FONT_SIZE
);
return {

View File

@@ -0,0 +1,149 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { ensureArray } from "../../common/array/ensure-array";
import type { HomeAssistant } from "../../types";
import "./ha-entity-state-picker";
@customElement("ha-entity-states-picker")
export class HaEntityStatesPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId?: string;
@property() public attribute?: string;
@property({ attribute: false }) public extraOptions?: any[];
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@property() public label?: string;
@property({ type: Array }) public value?: string[];
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ attribute: false })
public hideStates?: string[];
private _keys: string[] = [];
private _getKey(index: number) {
if (!this._keys[index]) {
this._keys[index] = Math.random().toString();
}
return this._keys[index];
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("value")) {
this.value = ensureArray(this.value);
}
}
protected render() {
if (!this.hass) {
return nothing;
}
const value = this.value || [];
const hide = [...(this.hideStates || []), ...value];
return html`
${repeat(
value,
(_state, index) => this._getKey(index),
(state, index) => html`
<div>
<ha-entity-state-picker
.index=${index}
.hass=${this.hass}
.entityId=${this.entityId}
.attribute=${this.attribute}
.extraOptions=${this.extraOptions}
.hideStates=${hide.filter((v) => v !== state)}
.allowCustomValue=${this.allowCustomValue}
.label=${this.label}
.value=${state}
.disabled=${this.disabled}
.helper=${this.disabled && index === value.length - 1
? this.helper
: undefined}
@value-changed=${this._valueChanged}
></ha-entity-state-picker>
</div>
`
)}
<div>
${this.disabled && value.length
? nothing
: keyed(
value.length,
html`<ha-entity-state-picker
.hass=${this.hass}
.entityId=${this.entityId}
.attribute=${this.attribute}
.extraOptions=${this.extraOptions}
.hideStates=${hide}
.allowCustomValue=${this.allowCustomValue}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !value.length}
@value-changed=${this._addValue}
></ha-entity-state-picker>`
)}
</div>
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const newState = ev.detail.value;
const newValue = [...this.value!];
const index = (ev.currentTarget as any)?.index;
if (index == null) {
return;
}
if (newState === undefined) {
newValue.splice(index, 1);
this._keys.splice(index, 1);
fireEvent(this, "value-changed", {
value: newValue,
});
return;
}
newValue[index] = newState;
fireEvent(this, "value-changed", {
value: newValue,
});
}
private _addValue(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: [...(this.value || []), ev.detail.value],
});
}
static override styles = css`
div {
margin-top: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-states-picker": HaEntityStatesPicker;
}
}

View File

@@ -27,7 +27,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
* @csspart spinner - The spinner that shows when the button is in the loading state.
*
* @cssprop --ha-button-height - The height of the button.
* @cssprop --ha-button-border-radius - The border radius of the button. defaults to `var(--border-radius-pill)`.
* @cssprop --ha-button-border-radius - The border radius of the button. defaults to `var(--ha-border-radius-pill)`.
*
* @attr {("small"|"medium")} size - Sets the button size.
* @attr {("brand"|"neutral"|"danger"|"warning"|"success")} variant - Sets the button color variant. "primary" is default.
@@ -57,7 +57,7 @@ export class HaButton extends Button {
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-form-control-border-radius: var(
--ha-button-border-radius,
var(--border-radius-pill)
var(--ha-border-radius-pill)
);
--wa-form-control-height: var(
@@ -154,6 +154,7 @@ export class HaButton extends Button {
:host([appearance~="plain"]) .button {
color: var(--wa-color-on-normal);
background-color: transparent;
}
:host([appearance~="plain"]) .button.disabled {
background-color: transparent;
@@ -210,6 +211,13 @@ export class HaButton extends Button {
.button.disabled {
opacity: 1;
}
slot[name="start"]::slotted(*) {
margin-inline-end: 4px;
}
slot[name="end"]::slotted(*) {
margin-inline-start: 4px;
}
`,
];
}

View File

@@ -207,6 +207,7 @@ export class HaComboBox extends LitElement {
aria-label=${ifDefined(this.hass?.localize("ui.common.clear"))}
class=${`clear-button ${this.label ? "" : "no-label"}`}
.path=${mdiClose}
?disabled=${this.disabled}
@click=${this._clearValue}
></ha-svg-icon>`
: ""}
@@ -393,7 +394,8 @@ export class HaComboBox extends LitElement {
:host([opened]) .toggle-button {
color: var(--primary-color);
}
.toggle-button[disabled] {
.toggle-button[disabled],
.clear-button[disabled] {
color: var(--disabled-text-color);
pointer-events: none;
}

View File

@@ -506,7 +506,7 @@ export class HaControlSlider extends LitElement {
width: 100%;
}
.slider .slider-track-bar {
--border-radius: var(--control-slider-border-radius);
--ha-border-radius: var(--control-slider-border-radius);
--slider-size: 100%;
position: absolute;
height: 100%;

View File

@@ -254,21 +254,37 @@ export class HaDateRangePicker extends LitElement {
}
private _applyDateRange() {
if (this.hass.locale.time_zone === TimeZone.server) {
const dateRangePicker = this._dateRangePicker;
let start = new Date(this._dateRangePicker.start);
let end = new Date(this._dateRangePicker.end);
const startDate = fromZonedTime(
dateRangePicker.start,
this.hass.config.time_zone
);
const endDate = fromZonedTime(
dateRangePicker.end,
this.hass.config.time_zone
);
if (this.timePicker) {
start.setSeconds(0);
start.setMilliseconds(0);
end.setSeconds(0);
end.setMilliseconds(0);
dateRangePicker.clickRange([startDate, endDate]);
if (
end.getHours() === 0 &&
end.getMinutes() === 0 &&
start.getFullYear() === end.getFullYear() &&
start.getMonth() === end.getMonth() &&
start.getDate() === end.getDate()
) {
end.setDate(end.getDate() + 1);
}
}
if (this.hass.locale.time_zone === TimeZone.server) {
start = fromZonedTime(start, this.hass.config.time_zone);
end = fromZonedTime(end, this.hass.config.time_zone);
}
if (
start.getTime() !== this._dateRangePicker.start.getTime() ||
end.getTime() !== this._dateRangePicker.end.getTime()
) {
this._dateRangePicker.clickRange([start, end]);
}
this._dateRangePicker.clickedApply();
}

View File

@@ -23,13 +23,13 @@ export class HaFab extends FabBase {
:host .mdc-fab--extended {
border-radius: var(
--ha-button-border-radius,
var(--border-radius-pill)
var(--ha-border-radius-pill)
);
}
:host .mdc-fab.mdc-fab--extended .ripple {
border-radius: var(
--ha-button-border-radius,
var(--border-radius-pill)
var(--ha-border-radius-pill)
);
}
:host .mdc-fab--extended .mdc-fab__icon {

View File

@@ -106,6 +106,8 @@ export const computeInitialHaFormData = (
data[field.name] = [];
} else if ("media" in selector || "target" in selector) {
data[field.name] = {};
} else if ("state" in selector) {
data[field.name] = selector.state?.multiple ? [] : "";
} else {
throw new Error(
`Selector ${Object.keys(selector)[0]} not supported in initial form data`

View File

@@ -7,8 +7,8 @@ import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-button";
import "./ha-icon-button";
import "./ha-textfield";
import "./ha-input-helper-text";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-multi-textfield")
@@ -79,6 +79,7 @@ class HaMultiTextField extends LitElement {
@click=${this._addItem}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.addLabel ??
(this.label
? this.hass?.localize("ui.components.multi-textfield.add_item", {
@@ -86,7 +87,6 @@ class HaMultiTextField extends LitElement {
})
: this.hass?.localize("ui.common.add")) ??
"Add"}
<ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon>
</ha-button>
</div>
${this.helper

View File

@@ -121,6 +121,10 @@ const SELECTOR_SCHEMAS = {
name: "entity_id",
selector: { entity: {} },
},
{
name: "multiple",
selector: { boolean: {} },
},
] as const,
target: [] as const,
template: [] as const,

View File

@@ -4,6 +4,7 @@ import type { StateSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import "../entity/ha-entity-state-picker";
import "../entity/ha-entity-states-picker";
@customElement("ha-selector-state")
export class HaSelectorState extends SubscribeMixin(LitElement) {
@@ -27,6 +28,25 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
};
protected render() {
if (this.selector.state?.multiple) {
return html`
<ha-entity-states-picker
.hass=${this.hass}
.entityId=${this.selector.state?.entity_id ||
this.context?.filter_entity}
.attribute=${this.selector.state?.attribute ||
this.context?.filter_attribute}
.extraOptions=${this.selector.state?.extra_options}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-value
.hideStates=${this.selector.state?.hide_states}
></ha-entity-states-picker>
`;
}
return html`
<ha-entity-state-picker
.hass=${this.hass}

View File

@@ -116,7 +116,7 @@ class DialogMediaManage extends LitElement {
`
: html`
<ha-button
class="danger"
variant="danger"
slot="navigationIcon"
.disabled=${this._deleting}
@click=${this._handleDelete}
@@ -327,10 +327,6 @@ class DialogMediaManage extends LitElement {
display: block;
}
.danger {
--mdc-theme-primary: var(--error-color);
}
ha-tip {
margin: 16px;
}

View File

@@ -18,9 +18,9 @@ import { fireEvent } from "../../common/dom/fire_event";
import { debounce } from "../../common/util/debounce";
import { isUnavailableState } from "../../data/entity";
import type {
MediaPlayerItem,
MediaPickedEvent,
MediaPlayerBrowseAction,
MediaPlayerItem,
MediaPlayerLayoutType,
} from "../../data/media-player";
import {
@@ -32,6 +32,7 @@ import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import {
brandsUrl,
@@ -44,16 +45,15 @@ import "../ha-alert";
import "../ha-button";
import "../ha-button-menu";
import "../ha-card";
import "../ha-spinner";
import "../ha-fab";
import "../ha-icon-button";
import "../ha-svg-icon";
import "../ha-tooltip";
import "../ha-list";
import "../ha-list-item";
import "../ha-spinner";
import "../ha-svg-icon";
import "../ha-tooltip";
import "./ha-browse-media-tts";
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
import { loadVirtualizer } from "../../resources/virtualizer";
declare global {
interface HASSDomEvents {

View File

@@ -30,6 +30,10 @@ export const ACTION_ICONS = {
wait_template: mdiCodeBraces,
wait_for_trigger: mdiTrafficLight,
repeat: mdiRefresh,
repeat_count: mdiRefresh,
repeat_while: mdiRefresh,
repeat_until: mdiRefresh,
repeat_for_each: mdiRefresh,
choose: mdiArrowDecision,
if: mdiCallSplit,
device_id: mdiDevices,
@@ -57,7 +61,10 @@ export const ACTION_GROUPS: AutomationElementGroup = {
delay: {},
wait_template: {},
wait_for_trigger: {},
repeat: {},
repeat_count: {},
repeat_while: {},
repeat_until: {},
repeat_for_each: {},
choose: {},
if: {},
stop: {},

View File

@@ -397,6 +397,7 @@ export interface StateSelector {
entity_id?: string | string[];
attribute?: string;
hide_states?: string[];
multiple?: boolean;
} | null;
}

View File

@@ -4,7 +4,7 @@ import type {
HassEntityBase,
HassEvent,
} from "home-assistant-js-websocket";
import { BINARY_STATE_ON } from "../common/const";
import { BINARY_STATE_ON, BINARY_STATE_OFF } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature";
@@ -52,6 +52,15 @@ export const updateCanInstall = (
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const latestVersionIsSkipped = (entity: UpdateEntity): boolean =>
!!(
entity.attributes.latest_version &&
entity.attributes.skipped_version === entity.attributes.latest_version
);
export const updateButtonIsDisabled = (entity: UpdateEntity): boolean =>
entity.state === BINARY_STATE_OFF && !latestVersionIsSkipped(entity);
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
!!entity.attributes.in_progress;

View File

@@ -7,6 +7,7 @@ import { relativeTime } from "../../../common/datetime/relative_time";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-checkbox";
import "../../../components/ha-faded";
import "../../../components/ha-markdown";
@@ -26,6 +27,8 @@ import {
UpdateEntityFeature,
updateIsInstalling,
updateReleaseNotes,
latestVersionIsSkipped,
updateButtonIsDisabled,
} from "../../../data/update";
import type { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../../generic/show-dialog-box";
@@ -180,11 +183,6 @@ class MoreInfoUpdate extends LitElement {
return nothing;
}
const skippedVersion =
this.stateObj.attributes.latest_version &&
this.stateObj.attributes.skipped_version ===
this.stateObj.attributes.latest_version;
const createBackupTexts = this._computeCreateBackupTexts();
return html`
@@ -312,7 +310,7 @@ class MoreInfoUpdate extends LitElement {
<ha-button
appearance="plain"
@click=${this._handleSkip}
.disabled=${skippedVersion ||
.disabled=${latestVersionIsSkipped(this.stateObj) ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
@@ -325,9 +323,8 @@ class MoreInfoUpdate extends LitElement {
? html`
<ha-button
@click=${this._handleInstall}
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
!skippedVersion) ||
updateIsInstalling(this.stateObj)}
.loading=${updateIsInstalling(this.stateObj)}
.disabled=${updateButtonIsDisabled(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.update"

View File

@@ -0,0 +1,34 @@
import type { LitElement } from "lit";
import { state } from "lit/decorators";
import type { Constructor } from "../types";
import { isMobileClient } from "../util/is_mobile";
import { listenMediaQuery } from "../common/dom/media_query";
export const MobileAwareMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
class MobileAwareClass extends superClass {
@state() protected _isMobileSize = false;
protected _isMobileClient = isMobileClient;
private _unsubMql?: () => void;
public connectedCallback() {
super.connectedCallback();
this._unsubMql = listenMediaQuery(
"all and (max-width: 450px), all and (max-height: 500px)",
(matches) => {
this._isMobileSize = matches;
}
);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubMql?.();
this._unsubMql = undefined;
}
}
return MobileAwareClass;
};

View File

@@ -17,6 +17,7 @@ import type { Action } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
VIRTUAL_ACTIONS,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
import type HaAutomationActionRow from "./ha-automation-action-row";
@@ -184,6 +185,8 @@ export default class HaAutomationAction extends LitElement {
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action));
} else if (action in VIRTUAL_ACTIONS) {
actions = this.actions.concat(VIRTUAL_ACTIONS[action]);
} else if (isService(action)) {
actions = this.actions.concat({
action: getService(action),
@@ -317,9 +320,6 @@ export default class HaAutomationAction extends LitElement {
display: block;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */

View File

@@ -39,7 +39,7 @@ export class HaIfAction extends LitElement implements ActionElement {
)}*:
</h3>
<ha-automation-condition
.conditions=${action.if}
.conditions=${action.if ?? []}
.disabled=${this.disabled}
@value-changed=${this._ifChanged}
.hass=${this.hass}
@@ -52,7 +52,7 @@ export class HaIfAction extends LitElement implements ActionElement {
)}*:
</h3>
<ha-automation-action
.actions=${action.then}
.actions=${action.then ?? []}
.disabled=${this.disabled}
@value-changed=${this._thenChanged}
.hass=${this.hass}

View File

@@ -15,6 +15,8 @@ export class HaParallelAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public action!: ParallelAction;
public static get defaultConfig(): ParallelAction {
@@ -29,6 +31,7 @@ export class HaParallelAction extends LitElement implements ActionElement {
return html`
<ha-automation-action
.actions=${action.parallel}
.narrow=${this.narrow}
.disabled=${this.disabled}
@value-changed=${this._actionsChanged}
.hass=${this.hass}

View File

@@ -11,7 +11,6 @@ import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
import { isTemplate } from "../../../../../common/string/has-template";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -35,22 +34,8 @@ export class HaRepeatAction extends LitElement implements ActionElement {
}
private _schema = memoizeOne(
(localize: LocalizeFunc, type: string, template: boolean) =>
(type: string, template: boolean) =>
[
{
name: "type",
selector: {
select: {
mode: "dropdown",
options: OPTIONS.map((opt) => ({
value: opt,
label: localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label`
),
})),
},
},
},
...(type === "count"
? ([
{
@@ -94,7 +79,6 @@ export class HaRepeatAction extends LitElement implements ActionElement {
const action = this.action.repeat;
const type = getType(action);
const schema = this._schema(
this.hass.localize,
type ?? "count",
"count" in action && typeof action.count === "string"
? isTemplate(action.count)
@@ -170,10 +154,6 @@ export class HaRepeatAction extends LitElement implements ActionElement {
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "type":
return this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type_select"
);
case "count":
return this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label"

View File

@@ -15,6 +15,8 @@ export class HaSequenceAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public action!: SequenceAction;
public static get defaultConfig(): SequenceAction {
@@ -29,6 +31,7 @@ export class HaSequenceAction extends LitElement implements ActionElement {
return html`
<ha-automation-action
.actions=${action.sequence}
.narrow=${this.narrow}
.disabled=${this.disabled}
@value-changed=${this._actionsChanged}
.hass=${this.hass}

View File

@@ -345,9 +345,6 @@ export default class HaAutomationCondition extends LitElement {
.buttons {
order: 1;
}
ha-svg-icon {
height: 20px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */

View File

@@ -487,6 +487,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.disabled=${Boolean(this._readOnly)}
.dirty=${this._dirty}
@value-changed=${this._valueChanged}
@editor-save=${this._handleSaveAutomation}
></manual-automation-editor>
`}
</div>
@@ -517,6 +518,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveAutomation}
.showErrors=${false}
disable-fullscreen
></ha-yaml-editor>`

View File

@@ -255,9 +255,6 @@ export default class HaAutomationOption extends LitElement {
display: block;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */

View File

@@ -1,7 +1,40 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { ACTION_GROUPS } from "../../../data/action";
import type { ActionType } from "../../../data/script";
export const PASTE_VALUE = "__paste__";
// These will be replaced with the correct action
export const VIRTUAL_ACTIONS: Record<
keyof (typeof ACTION_GROUPS)["building_blocks"]["members"],
ActionType
> = {
repeat_count: {
repeat: {
count: 2,
sequence: [],
},
},
repeat_while: {
repeat: {
while: [],
sequence: [],
},
},
repeat_until: {
repeat: {
until: [],
sequence: [],
},
},
repeat_for_each: {
repeat: {
for_each: {},
sequence: [],
},
},
} as const;
export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action";
add: (key: string) => void;

View File

@@ -296,9 +296,6 @@ export default class HaAutomationTrigger extends LitElement {
display: block;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */

View File

@@ -1442,10 +1442,11 @@ export class HaConfigDevicePage extends LitElement {
}
private async _signUrl(ev) {
const signedUrl = await getSignedPath(
this.hass,
ev.currentTarget.getAttribute("href")
);
const a = ev.currentTarget.getAttribute("href")
? ev.currentTarget
: ev.currentTarget.closest("a");
const signedUrl = await getSignedPath(this.hass, a.getAttribute("href"));
fileDownload(signedUrl.path);
}

View File

@@ -17,6 +17,10 @@ import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
@@ -64,10 +68,6 @@ import "./ha-config-entry-row";
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
import { showPickConfigEntryDialog } from "./show-pick-config-entry-dialog";
import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
export const renderConfigEntryError = (
hass: HomeAssistant,

View File

@@ -14,6 +14,9 @@ import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import "./mqtt-subscribe-card";
import type { Action } from "../../../../../data/script";
import { callExecuteScript } from "../../../../../data/service";
import { showToast } from "../../../../../util/toast";
const qosLevel = ["0", "1", "2"];
@@ -55,14 +58,6 @@ export class MQTTConfigPanel extends LitElement {
})
private _retain = false;
@state()
@storage({
key: "panel-dev-mqtt-allow-template-ls",
state: true,
subscribe: false,
})
private _allowTemplate = false;
protected render(): TemplateResult {
return html`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
@@ -108,25 +103,7 @@ export class MQTTConfigPanel extends LitElement {
></ha-switch>
</ha-formfield>
</div>
<p>
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.mqtt.allow_template"
)}
>
<ha-switch
@change=${this._handleAllowTemplate}
.checked=${this._allowTemplate}
></ha-switch>
</ha-formfield>
</p>
<p>
${this._allowTemplate
? this.hass.localize("ui.panel.config.mqtt.payload")
: this.hass.localize(
"ui.panel.config.mqtt.payload_no_template"
)}
</p>
<p>${this.hass.localize("ui.panel.config.mqtt.payload")}</p>
<ha-code-editor
mode="jinja2"
autocomplete-entities
@@ -171,21 +148,28 @@ export class MQTTConfigPanel extends LitElement {
this._retain = (ev.target! as any).checked;
}
private _handleAllowTemplate(ev: CustomEvent) {
this._allowTemplate = (ev.target! as any).checked;
}
private _publish(): void {
if (!this.hass) {
return;
}
this.hass.callService("mqtt", "publish", {
topic: this._topic,
payload: !this._allowTemplate ? this._payload : undefined,
payload_template: this._allowTemplate ? this._payload : undefined,
qos: parseInt(this._qos),
retain: this._retain,
});
const script: Action[] = [
{
action: "mqtt.publish",
data: {
topic: this._topic,
payload: this._payload,
qos: parseInt(this._qos),
retain: this._retain,
},
},
];
callExecuteScript(this.hass, script).catch((err) =>
showToast(this, {
message: err.message,
})
);
}
private async _openOptionFlow() {

View File

@@ -105,7 +105,6 @@ class MqttSubscribeCard extends LitElement {
size="small"
.disabled=${this._topic === ""}
@click=${this._handleSubmit}
type="submit"
>
${this._subscribed
? this.hass.localize("ui.panel.config.mqtt.stop_listening")

View File

@@ -320,6 +320,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
.hass=${this.hass}
.defaultValue=${this._config}
@value-changed=${this._yamlChanged}
@editor-save=${this._saveScene}
.showErrors=${false}
disable-fullscreen
></ha-yaml-editor>`;

View File

@@ -438,6 +438,7 @@ export class HaScriptEditor extends SubscribeMixin(
.disabled=${this._readOnly}
.dirty=${this._dirty}
@value-changed=${this._valueChanged}
@editor-save=${this._handleSave}
></manual-script-editor>
`}
</div>
@@ -450,6 +451,7 @@ export class HaScriptEditor extends SubscribeMixin(
.readOnly=${this._readOnly}
disable-fullscreen
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSave}
.showErrors=${false}
></ha-yaml-editor>`
: nothing}

View File

@@ -426,13 +426,7 @@ class HaPanelHistory extends LitElement {
private _dateRangeChanged(ev) {
this._startDate = ev.detail.value.startDate;
const endDate = ev.detail.value.endDate;
if (endDate.getHours() === 0 && endDate.getMinutes() === 0) {
endDate.setDate(endDate.getDate() + 1);
endDate.setMilliseconds(endDate.getMilliseconds() - 1);
}
this._endDate = endDate;
this._endDate = ev.detail.value.endDate;
this._updatePath();
}

View File

@@ -236,10 +236,6 @@ export class HaPanelLogbook extends LitElement {
private _dateRangeChanged(ev) {
const startDate = ev.detail.value.startDate;
const endDate = ev.detail.value.endDate;
if (endDate.getHours() === 0 && endDate.getMinutes() === 0) {
endDate.setDate(endDate.getDate() + 1);
endDate.setMilliseconds(endDate.getMilliseconds() - 1);
}
this._time = {
range: [startDate, endDate],
};

View File

@@ -21,7 +21,7 @@ export const supportsButtonCardFeature = (
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return ["button", "script"].includes(domain);
return ["button", "input_button", "script"].includes(domain);
};
@customElement("hui-button-card-feature")
@@ -43,7 +43,8 @@ class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
if (!this.hass || !this._stateObj) return;
const domain = computeDomain(this._stateObj.entity_id);
const service = domain === "button" ? "press" : "turn_on";
const service =
domain === "button" || domain === "input_button" ? "press" : "turn_on";
this.hass.callService(domain, service, {
entity_id: this._stateObj.entity_id,

View File

@@ -23,6 +23,7 @@ import type { Link, Node } from "../../../../components/chart/ha-sankey-chart";
import { getGraphColorByIndex } from "../../../../common/color/colors";
import { formatNumber } from "../../../../common/number/format_number";
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
import { MobileAwareMixin } from "../../../../mixins/mobile-aware-mixin";
const DEFAULT_CONFIG: Partial<EnergySankeyCardConfig> = {
group_by_floor: true,
@@ -31,7 +32,7 @@ const DEFAULT_CONFIG: Partial<EnergySankeyCardConfig> = {
@customElement("hui-energy-sankey-card")
class HuiEnergySankeyCard
extends SubscribeMixin(LitElement)
extends SubscribeMixin(MobileAwareMixin(LitElement))
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -70,7 +71,11 @@ class HuiEnergySankeyCard
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return changedProps.has("_config") || changedProps.has("_data");
return (
changedProps.has("_config") ||
changedProps.has("_data") ||
changedProps.has("_isMobileSize")
);
}
protected render() {
@@ -373,13 +378,17 @@ class HuiEnergySankeyCard
const hasData = nodes.some((node) => node.value > 0);
const vertical =
this._config.layout === "vertical" ||
(this._config.layout !== "horizontal" && this._isMobileSize);
return html`
<ha-card .header=${this._config.title}>
<div class="card-content">
${hasData
? html`<ha-sankey-chart
.data=${{ nodes, links }}
.vertical=${this._config.layout === "vertical"}
.vertical=${vertical}
.valueFormatter=${this._valueFormatter}
></ha-sankey-chart>`
: html`${this.hass.localize(

View File

@@ -213,7 +213,7 @@ export interface EnergyCarbonGaugeCardConfig extends EnergyCardBaseConfig {
export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
type: "energy-sankey";
title?: string;
layout?: "vertical" | "horizontal";
layout?: "vertical" | "horizontal" | "auto";
group_by_floor?: boolean;
group_by_area?: boolean;
}

View File

@@ -2,6 +2,7 @@ import {
mdiAccount,
mdiAmpersand,
mdiGateOr,
mdiNotEqualVariant,
mdiNumeric,
mdiResponsive,
mdiStateMachine,
@@ -14,5 +15,6 @@ export const ICON_CONDITION: Record<Condition["condition"], string> = {
screen: mdiResponsive,
user: mdiAccount,
and: mdiAmpersand,
not: mdiNotEqualVariant,
or: mdiGateOr,
};

View File

@@ -11,7 +11,8 @@ export type Condition =
| ScreenCondition
| UserCondition
| OrCondition
| AndCondition;
| AndCondition
| NotCondition;
// Legacy conditional card condition
export interface LegacyCondition {
@@ -58,6 +59,11 @@ export interface AndCondition extends BaseCondition {
conditions?: Condition[];
}
export interface NotCondition extends BaseCondition {
condition: "not";
conditions?: Condition[];
}
function getValueFromEntityId(
hass: HomeAssistant,
value: string
@@ -149,6 +155,11 @@ function checkAndCondition(condition: AndCondition, hass: HomeAssistant) {
return checkConditionsMet(condition.conditions, hass);
}
function checkNotCondition(condition: NotCondition, hass: HomeAssistant) {
if (!condition.conditions) return true;
return !checkConditionsMet(condition.conditions, hass);
}
function checkOrCondition(condition: OrCondition, hass: HomeAssistant) {
if (!condition.conditions) return true;
return condition.conditions.some((c) => checkConditionsMet([c], hass));
@@ -175,6 +186,8 @@ export function checkConditionsMet(
return checkStateNumericCondition(c, hass);
case "and":
return checkAndCondition(c, hass);
case "not":
return checkNotCondition(c, hass);
case "or":
return checkOrCondition(c, hass);
default:
@@ -247,6 +260,10 @@ function validateAndCondition(condition: AndCondition) {
return condition.conditions != null;
}
function validateNotCondition(condition: NotCondition) {
return condition.conditions != null;
}
function validateOrCondition(condition: OrCondition) {
return condition.conditions != null;
}
@@ -276,6 +293,8 @@ export function validateConditionalConfig(
return validateNumericStateCondition(c);
case "and":
return validateAndCondition(c);
case "not":
return validateNotCondition(c);
case "or":
return validateOrCondition(c);
default:

View File

@@ -18,6 +18,7 @@ import "./ha-card-condition-editor";
import type { HaCardConditionEditor } from "./ha-card-condition-editor";
import type { LovelaceConditionEditorConstructor } from "./types";
import "./types/ha-card-condition-and";
import "./types/ha-card-condition-not";
import "./types/ha-card-condition-numeric_state";
import "./types/ha-card-condition-or";
import "./types/ha-card-condition-screen";
@@ -30,6 +31,7 @@ const UI_CONDITION = [
"screen",
"user",
"and",
"not",
"or",
] as const satisfies readonly Condition["condition"][];

View File

@@ -0,0 +1,62 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { any, array, assert, literal, object, optional } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { HomeAssistant } from "../../../../../types";
import type {
NotCondition,
Condition,
StateCondition,
} from "../../../common/validate-condition";
import "../ha-card-conditions-editor";
const notConditionStruct = object({
condition: literal("not"),
conditions: optional(array(any())),
});
@customElement("ha-card-condition-not")
export class HaCardConditionNot extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: NotCondition;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): NotCondition {
return { condition: "not", conditions: [] };
}
protected static validateUIConfig(condition: StateCondition) {
return assert(condition, notConditionStruct);
}
protected render() {
return html`
<ha-card-conditions-editor
nested
.hass=${this.hass}
.conditions=${this.condition.conditions}
@value-changed=${this._valueChanged}
>
</ha-card-conditions-editor>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const conditions = ev.detail.value as Condition[];
const condition = {
...this.condition,
conditions,
};
fireEvent(this, "value-changed", { value: condition });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-not": HaCardConditionNot;
}
}

View File

@@ -32,7 +32,7 @@ export const coreColorStyles = css`
--ha-color-neutral-50: #7a7a7a;
--ha-color-neutral-60: #989898;
--ha-color-neutral-70: #b1b1b1;
--ha-color-neutral-80: #b1b1b1;
--ha-color-neutral-80: #cccccc;
--ha-color-neutral-90: #e6e6e6;
--ha-color-neutral-95: #f3f3f3;

View File

@@ -15,9 +15,9 @@ export const semanticColorStyles = css`
--ha-color-text-disabled: var(--ha-color-neutral-60);
--ha-color-text-link: var(--ha-color-primary-40);
/* border primary */
--ha-color-border-quiet: var(--ha-color-primary-80);
--ha-color-border-normal: var(--ha-color-primary-70);
--ha-color-border-loud: var(--ha-color-primary-40);
--ha-color-border-primary-quiet: var(--ha-color-primary-80);
--ha-color-border-primary-normal: var(--ha-color-primary-70);
--ha-color-border-primary-loud: var(--ha-color-primary-40);
/* border neutral */
--ha-color-border-neutral-quiet: var(--ha-color-neutral-80);

View File

@@ -3,21 +3,21 @@ import { extractDerivedVars } from "../../common/style/derived-css-vars";
export const coreStyles = css`
html {
--border-width-sm: 1px;
--border-width-md: 2px;
--border-width-lg: 3px;
--ha-border-width-sm: 1px;
--ha-border-width-md: 2px;
--ha-border-width-lg: 3px;
--border-radius-sm: 4px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
--border-radius-xl: 16px;
--border-radius-2xl: 24px;
--border-radius-3xl: 28px;
--border-radius-4xl: 32px;
--border-radius-5xl: 36px;
--border-radius-pill: 9999px;
--border-radius-circle: 50%;
--border-radius-square: 0;
--ha-border-radius-sm: 4px;
--ha-border-radius-md: 8px;
--ha-border-radius-lg: 12px;
--ha-border-radius-xl: 16px;
--ha-border-radius-2xl: 24px;
--ha-border-radius-3xl: 28px;
--ha-border-radius-4xl: 32px;
--ha-border-radius-5xl: 36px;
--ha-border-radius-pill: 9999px;
--ha-border-radius-circle: 50%;
--ha-border-radius-square: 0;
}
`;

View File

@@ -256,10 +256,10 @@
},
"timer": {
"actions": {
"start": "start",
"pause": "pause",
"cancel": "cancel",
"finish": "finish"
"start": "Start",
"pause": "Pause",
"cancel": "Cancel",
"finish": "Finish"
}
},
"vacuum": {
@@ -438,7 +438,6 @@
"supports_markdown": "Supports {markdown_help_link}",
"markdown": "Markdown"
},
"components": {
"selectors": {
"media": {
@@ -1972,11 +1971,11 @@
},
"google_home": {
"header": "Share from Google Home",
"step_1": "Find your device in the Google Home app. Tap the gear icon to open the device settings.",
"step_1": "Find your device in the Google Home app. Tap the gear icon to open the device settings, then tap on Device information.",
"step_2": "Tap {linked_matter_apps_services}.",
"step_3": "Tap {link_apps_services} and choose {home_assistant} from the list.",
"linked_matter_apps_services": "Linked Matter apps and services",
"link_apps_services": "Link apps & services",
"link_apps_services": "Link apps and services",
"no_home_assistant": "I can't find Home Assistant on the list",
"redirect": "You are redirected to the Home Assistant app. Please follow the instructions."
},
@@ -4375,7 +4374,6 @@
},
"repeat": {
"label": "Repeat",
"type_select": "Repeat type",
"type": {
"count": {
"label": "Count"
@@ -4403,6 +4401,30 @@
"for_each": "for every item: {items}"
}
},
"repeat_count": {
"label": "Repeat multiple times",
"description": {
"picker": "Repeat a sequence of actions a fixed number of times."
}
},
"repeat_while": {
"label": "Repeat while",
"description": {
"picker": "Repeat a sequence of actions as long as a condition is satisfied. The condition is checked before each run of the sequence."
}
},
"repeat_until": {
"label": "Repeat until",
"description": {
"picker": "Repeat a sequence of actions until a condition is satisfied. The condition is checked after each run of the sequence."
}
},
"repeat_for_each": {
"label": "Repeat for each",
"description": {
"picker": "Repeat a sequence for each element of a list."
}
},
"choose": {
"label": "Choose",
"default": "Default actions",
@@ -4418,9 +4440,9 @@
"no_conditions": "[%key:ui::panel::config::devices::automation::conditions::no_conditions%]",
"sequence": "Actions",
"description": {
"picker": "Choose what to do based on conditions (Similar to If-Then, but more powerful).",
"full": "Choose {number, plural,\n one {an action}\n other{between {number} actions}\n}",
"no_action": "Choose an action"
"picker": "Choose what to do based on conditions (Similar to If-then, but more powerful).",
"full": "Choose {number, plural,\n one {an option}\n other{between {number} options}\n}",
"no_action": "Choose an option"
}
},
"if": {
@@ -5633,8 +5655,6 @@
"description_publish": "Publish a packet",
"topic": "Topic",
"payload": "Payload (template allowed)",
"payload_no_template": "Payload",
"allow_template": "Allow template",
"publish": "Publish",
"description_listen": "Listen to a topic",
"json_formatting": "Format JSON content",
@@ -7285,6 +7305,9 @@
"or": {
"label": "Or"
},
"not": {
"label": "Not"
},
"and": {
"label": "And"
}
@@ -7795,6 +7818,9 @@
"edit": "Edit feature",
"remove": "Remove feature",
"types": {
"button": {
"label": "Button"
},
"cover-open-close": {
"label": "Cover open/close"
},

View File

@@ -46,7 +46,7 @@ export const registerServiceWorker = async (
// the new service worker. Above we listen for `controllerchange`
// so we reload the page once a new service worker activates.
action: () => installingWorker.postMessage({ type: "skipWaiting" }),
text: "reload",
text: "Reload",
},
duration: -1,
dismissable: false,

695
yarn.lock

File diff suppressed because it is too large Load Diff