Compare commits

..
Author SHA1 Message Date
Maarten Lakerveld bcf493223a Share one base class across the home summary panels
The maintenance, climate and security panels carried the same ~300 line shell: config loading with revision guards, queued saves with failure recovery, the edit visibility toggle and view generation. Move it into HaHomeSummaryPanelBase, keyed on the system data key, which doubles as the strategy type, URL path and translation namespace. Security only adds its strategy options and the favorite and alert handlers, which the base binds as optional hooks.

The view container now takes min-height: 100% of the top app bar's scroll area. Climate still had min-height: 100vh from before its top bar migration, which overflowed the scroll area by the header height, while security and maintenance had dropped it and cut themed backgrounds off below short content.

The home panel subscribed to and loaded the three configs in three copies of the same block. Keep them in one record keyed the same way and loop over the keys.
2026-11-09 15:47:23 +01:00
Maarten Lakerveld 8731a5682b Edit security favorites, alerts and visibility inline on the page
Give the security page a hidden_entities list like the other pages and replace its edit dialog with the inline edit mode. In edit mode every tile, favorites included, carries an eye, a star and, for alert-capable domains, a bell that cycles through none, warning and alert. Each tap saves at once. The Home dashboard summary badge, overview check and area views take the same hidden list.

Hidden, favorite and alert flags stay independent, so a hidden entity can still be a favorite or raise alerts, and the Home security summary stays while alerts are configured.

In edit mode the sidebar lists every configured alert, active or not, in its configured color, so a saved alert can always be changed or cleared there, including one whose entity no longer matches the page or was deleted. These entries pick the level with a three-way control instead of the cycling bell, so clearing is an explicit choice and an entry does not vanish on the way from alert back to warning. Changing a level keeps the alert in its place in the list. Favorites and alert entries show regardless of the hidden list, so they carry the star and the alert control but no eye. A deleted entity shows its ID in place of its card, so two stale entries stay distinguishable.

Toggling the eye only flips the wrapper's own flag and saves, like on the other pages. A star or bell tap moves the entity between the grid and the sidebar lists, so those regenerate the view, and the wrapper card's focus restoration covers the star and the alert controls and tells the grid tile and the sidebar entry of one entity apart, so the recreated wrapper focuses the control that fired. When a tap removes the entry itself, focus moves to the same control on the next entry in that list. When the list runs out, the last favorite hands focus to the first tile of the section that takes its place at the top of the page, and the last alert entry to the entity's own page tile if it has one; the strategy puts that fallback on the wrapper. When there is no fallback, or the recreated tile cannot take focus because the narrow layout shows the Activity tab, the card tells the panel and the panel focuses its exit-edit-mode button instead, so keyboard focus never drops to the page. Entering edit mode drops any request left over from an entry without either. ha-control-select gains a focus() that lands on its selected option for this. A failed save reloads the configuration and forces a rebuild so the wrappers show the stored state again.

The dialog, its alerts editor, the entity filter helper and the dialog test go away. Strategy tests pin down that a hidden entity keeps its favorite and alert entries, that stale favorites and alerts only show in edit mode, and the shape of the edit-mode wrappers.
2026-11-09 15:47:22 +01:00
Maarten Lakerveld a7a09af3be Resolve climate area sensors once and label them by slot
Build the visible area sensor map a single time with the hidden list applied, so the page and the Home badge share one rule and the separate showAreaSensor check goes away. Label the temperature and humidity tiles from the area slot they fill instead of reading device_class back off the state, which produced an empty name for sensors set through the API without a device class. Drop the trend-graph branch for non-area entities, which the climate filters never admit.
2026-11-09 15:47:22 +01:00
Maarten Lakerveld 50678a9039 Let users hide entities from the climate page
Give the climate page the same hidden_entities system data, config
loading and inline visibility edit mode the maintenance page has. The
climate resolver also covers the area temperature and humidity
sensors, which the page adds outside the entity filters, so they can
be hidden too and the Home climate badge skips them.
2026-11-09 15:47:22 +01:00
183 changed files with 2258 additions and 5173 deletions

No files matched your search

-8
View File
@@ -28,18 +28,10 @@ AI can be a helpful tool for writing code and documentation. However, due to the
All contributions must be reviewed and understood by the contributor before submission. You should be able to explain every change in a pull request you submit. Pull requests that appear to be unreviewed AI output will be closed without review.
## Design contributions
AI can be a helpful tool to explore ideas and build mockups and prototypes. But AI tends to give the most common answer, not the one that fits Home Assistant and its users. So we require a human who brings the point of view and owns the design decisions.
You should be able to explain the problem your design solves and why you made your choices. If a mockup or prototype was mostly generated with AI, say so when you share it. Check your work against the [design guidelines](https://contributors.home-assistant.io/design/), as AI tools often make up components, colors, or spacing that don't exist in Home Assistant. Design proposals that look like unreviewed AI output may be closed or hidden.
## Our use of AI
Some of our projects use AI tools to assist with code reviews, issue triaging, reporting, and other project management tasks. These tools may leave comments on pull requests or issues. As with any automated tooling, these comments are not always correct.
We also use AI to help keep the design system, the Figma kit, and the code in sync. The design decisions behind them are made by people.
If an AI tool leaves a comment on your contribution, treat it as you would any other review comment. If you believe it is incorrect, say so; a brief explanation is sufficient. Maintainers always have the final say. If in doubt, ask a maintainer.
## Enforcement
@@ -44,21 +44,14 @@ export const lorawanFixtures: ConnectivityFixtures = {
],
commands: ["lorawan/"],
manifests: [
manifest("lorawan", "LoRaWAN", {
config_flow: false,
integration_type: "service",
}),
manifest("lorawan", "LoRaWAN", { integration_type: "helper" }),
manifest("sensecap_lorawan", "SenseCAP", {
integration_type: "device",
dependencies: ["lorawan"],
}),
manifest("chirpstack", "ChirpStack", {
integration_type: "hub",
dependencies: ["lorawan"],
}),
manifest("chirpstack", "ChirpStack", { integration_type: "hub" }),
manifest("thethingsnetwork_lorawan", "The Things Network", {
integration_type: "hub",
dependencies: ["lorawan"],
}),
],
configEntries: [
@@ -10,26 +10,19 @@ export const FRONIUS_ENTRY_ID = "mock-fronius";
export const RS485_PORT = "/dev/ttyUSB1";
export const modbusFixtures: ConnectivityFixtures = {
components: ["modbus", "flexit", "solaredge_modbus", "fronius"],
components: ["modbus"],
commands: ["modbus/"],
manifests: [
manifest("modbus", "Modbus", {
config_flow: false,
iot_class: "local_polling",
}),
manifest("flexit", "Flexit", {
integration_type: "device",
dependencies: ["modbus"],
iot_class: "local_polling",
}),
manifest("solaredge_modbus", "SolarEdge Modbus", {
integration_type: "device",
dependencies: ["modbus"],
iot_class: "local_polling",
}),
manifest("fronius", "Fronius", {
integration_type: "device",
dependencies: ["modbus"],
iot_class: "local_polling",
}),
],
+8 -8
View File
@@ -77,11 +77,11 @@
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.24",
"@tsparticles/engine": "4.5.3",
"@tsparticles/preset-links": "4.5.3",
"@versatiles/style": "9.0.0",
"@tsparticles/engine": "4.5.2",
"@tsparticles/preset-links": "4.5.2",
"@versatiles/style": "8.1.1",
"@vibrant/color": "4.0.4",
"@vvo/tzdb": "6.204.0",
"@vvo/tzdb": "6.202.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"barcode-detector": "3.2.3",
"cally": "0.9.2",
@@ -108,7 +108,7 @@
"leaflet.markercluster": "1.5.3",
"lit": "3.3.3",
"lit-html": "3.3.3",
"maplibre-gl": "6.17.0",
"maplibre-gl": "6.16.0",
"marked": "18.1.3",
"memoize-one": "6.0.0",
"nanoid": "6.0.2",
@@ -147,7 +147,7 @@
"@eslint/js": "10.0.1",
"@gfx/zopfli": "1.0.15",
"@home-assistant/components": "workspace:*",
"@html-eslint/eslint-plugin": "0.68.0",
"@html-eslint/eslint-plugin": "0.67.0",
"@lokalise/node-api": "17.0.1",
"@octokit/auth-oauth-device": "8.0.5",
"@octokit/rest": "22.0.1",
@@ -180,7 +180,7 @@
"eslint": "10.14.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-plugin-import-x": "4.18.0",
"eslint-plugin-import-x": "4.17.1",
"eslint-plugin-lit": "2.3.1",
"eslint-plugin-lit-a11y": "5.1.3",
"eslint-plugin-unused-imports": "4.4.1",
@@ -208,7 +208,7 @@
"pinst": "3.0.0",
"prettier": "3.9.10",
"rspack-manifest-plugin": "5.2.2",
"serve": "14.2.7",
"serve": "14.2.6",
"sinon": "22.1.0",
"tar": "7.5.22",
"terser-webpack-plugin": "5.6.1",
+236 -182
View File
@@ -277,7 +277,7 @@ importers:
version: 1.0.3
'@maplibre/maplibre-gl-leaflet':
specifier: 0.1.4
version: 0.1.4(@types/[email protected])([email protected])([email protected]7.0)
version: 0.1.4(@types/[email protected])([email protected])([email protected]6.0)
'@mdi/js':
specifier: 7.4.47
version: 7.4.47
@@ -291,20 +291,20 @@ importers:
specifier: 0.5.24
version: 0.5.24
'@tsparticles/engine':
specifier: 4.5.3
version: 4.5.3
specifier: 4.5.2
version: 4.5.2
'@tsparticles/preset-links':
specifier: 4.5.3
version: 4.5.3
specifier: 4.5.2
version: 4.5.2
'@versatiles/style':
specifier: 9.0.0
version: 9.0.0([email protected]7.0)
specifier: 8.1.1
version: 8.1.1([email protected]6.0)
'@vibrant/color':
specifier: 4.0.4
version: 4.0.4
'@vvo/tzdb':
specifier: 6.204.0
version: 6.204.0
specifier: 6.202.0
version: 6.202.0
'@webcomponents/scoped-custom-element-registry':
specifier: 0.0.10
version: 0.0.10
@@ -384,8 +384,8 @@ importers:
specifier: 3.3.3
version: 3.3.3
maplibre-gl:
specifier: 6.17.0
version: 6.17.0
specifier: 6.16.0
version: 6.16.0
marked:
specifier: 18.1.3
version: 18.1.3
@@ -496,8 +496,8 @@ importers:
specifier: workspace:*
version: link:packages/components
'@html-eslint/eslint-plugin':
specifier: 0.68.0
version: 0.68.0([email protected]([email protected]))
specifier: 0.67.0
version: 0.67.0([email protected]([email protected]))
'@lokalise/node-api':
specifier: 17.0.1
version: 17.0.1
@@ -595,8 +595,8 @@ importers:
specifier: 0.13.11
version: 0.13.11([email protected])
eslint-plugin-import-x:
specifier: 4.18.0
version: 4.18.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])
specifier: 4.17.1
version: 4.17.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])
eslint-plugin-lit:
specifier: 2.3.1
version: 2.3.1([email protected]([email protected]))
@@ -679,8 +679,8 @@ importers:
specifier: 5.2.2
version: 5.2.2(@rspack/[email protected](@swc/[email protected]))
serve:
specifier: 14.2.7
version: 14.2.7([email protected])
specifier: 14.2.6
version: 14.2.6([email protected])
sinon:
specifier: 22.1.0
version: 22.1.0
@@ -2075,27 +2075,27 @@ packages:
resolution: {integrity: sha512-EX1qiX5h3LAoTjuE3rgfuRlNq3Rds5tGrgDGlHoFbxhXaCSTTtuYU6tdR8vgOXKGUDi5gBDrcoNdLYKFXSrknA==}
engines: {node: '>=14.17.0'}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-u3HioqcKIH6dVQp2uulmpcuUqgKLqiuOWyHAl7hV4hIUkuJ3ge7vEUwPoWvNRbfy3aWOMygttokdGxL5q9YFIg==}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-MPoKw4woCkuFYs2xdt0vEjzGbUzU8lxeTZkxyrtHM+zZlaK/u/lqcIapZmyCTQBqNE0H9J8zc9TmXjygfHcuXg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-fiR8UTYBr4+fGftEG1u+tT3AiuOSowvSZbsY4tLcMl9hOuFzquHw4BIaFFlV3xTeqF5Y0M69aPJoEhFJUkQyog==}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-gr2wKoS1OOZa6uL8DDNCjvPyTu6mWsyQJ/WXmvP/DeFRe6pdumsb8ePYfP7qZW5741MtVd0OpLbxa82XivxxWQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: '>=8.0.0 || ^10.0.0-0'
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-X22bU2qzEpzPI0bHtnfXzoUS8q/It/DNycJnIdsPzBDXcly+PItV5AJU4nm/+9FZCXSE3IZJvORRCTuvvpV8Bg==}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-ejdtqLqclQPDfivgsD2yWHXa+FyyEytMlGNNfiHw/UbUeV9tn1hBjIZLJ5VvzjO0IweaAIWIZKlEPgKT6tYP8A==}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-zLgikGAF7izvrvUefy5EBEl6WpM0sTN9xzldwp7UgkkjR+zaFvqi9YsOWj+Y7Tu3XMXI0d7Gxv7PPPumVmJTuw==}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-fvFAuAaKzkg9deV69uT1NoVJSwXTu3mvGDWzlZKe+xwm5JnkbWC626eyDRlAQDzff2/zX7tWFq4EMgOoZmgtRA==}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-qbIF3MqmRckg6pXhwY/Ytxnb0eOoq5LPVt9wq8N4UBQHxL2z4cF407FHAiLNhpy8DO4AJUQbI6IZzhN8fHkvPA==}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-leHUXnpFgRSR3o7rpw5OtOVGfzcChIgrhXDgPlBwdoL7V4+5KZjxGLJAPVwbj7sVVNrSWynlIQ1Wn4FuNKxZhw==}
'@html-eslint/[email protected]8.0':
resolution: {integrity: sha512-FW+FjFHp/w1V4uZ0us7keR6mAA+WGh6OsHvJ+Kzy8aBeogyOapDPEMT+zkXSsiJ+V9maJ6umqG20BXcs2y6lsw==}
'@html-eslint/[email protected]7.0':
resolution: {integrity: sha512-8w9V0sTm2DGLh2uslL7+Un9g/FHNcevgouiTLhlAJoBhhGOK5+fX76QCKdWbgmpudNGo8Nh1kHKsJP3a6twzXA==}
'@humanfs/[email protected]':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
@@ -3270,85 +3270,85 @@ packages:
resolution: {integrity: sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==}
engines: {node: '>=12'}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-Fz2bTDc6Folav8isOk2scXM+LCSPhQ9hD8xCSmsEGuGH7JkE+XTsS4j6E9H6b+RFlZ1yb2TxdVBJnfXy8XM9Bw==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-bwvu84lMX5Sp79pyKu5yW2SFAqY52B7OlaxDjeeP3TiO7hJLFWxd8VPmSfOarcYK/z/uxp/dZfbcvp/5ylXyiQ==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-f0gHWnqChK4ksE9Bq3YN0iuJVMi2B81YSkCL5BrohuykoaKCLSVVSqjhefmELTMrdiFVsdt6WPJ+HjVQgjt7CA==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-lA+GP2EN5uN3TnOMEFqYBS0yLuQyZ2UlHy72TedgREGeEsrUoXB/FtZKseVt0g9FId4ZI6jXizxEavs65PbUsA==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-kbZoUA2uqR2043FHfH2+KFz7JhYTZjyTeDqVL/Z3z38mfdHEVrOJ4q+mwYjr+hX4tq2YoLf1D4DA6w/er1AlKA==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-NaxB1X/90lSu+uI0ayaaNC4okLIwJ93nBWvrdYHQ5/xsyan01qU+rnDjdg8Y9fPewncXlHkOaQezHeG4RjDubg==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-ak8AAPQKinJlwztRAo+OA5ItwjLtymmjayefQCF6K1FIWs5SaWiHWbzMB5vdOvuGUE6OOchqj8NLcu3HfHmdjw==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-JmaqBMZW/OzXGeQBz1g67EwTKeOYccIFrQMnBmiZgavO7LLhglABpyTWJAT9Cb7M/kgx3GIecZGkPxFzQ44pxQ==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-NAm9yL8o3oaeYgB7WLzuoD0yfLw9kvEwu0N+UHTwoe+kYRLJvmoUyMlED7hPQHW74VTlSmuZ6gqWTnaOv1mFLA==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-kNwh59Oyi8lvFQYK9z4YgAhS0GcOfFLkIhafTo0qlklqSUl3CvZq0aS14t368oz4Qk/l29pvnLJQg8pmRxIMaw==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/plugin-interactivity': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/plugin-interactivity': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-fJJp0xVEeGQDpkjGzvftMbCrLWtwqo+ees49gCnr9SZYVMRbxi3vQYukvi4ULOzYgEW5bTseVprDfiqOwfQJcw==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-7mO9tpJvXkbRIn4OjjUNoiPKAEJkEeazLl8eQB9W9lYpxuNCyZZ+sXJpS2LazyBNKTvGy1P0ekoX/rHqc84vKA==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-k+AkTG8r2UTE3n2u4kmb9dODRVtBc3Yttk7/UQ5HPlcBMr8AnKbx9gyxzzCx2MaEFZG2My0cghJPvQvibrAW7Q==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-gzcPgOzb3vP3p1AF/op5045buNYjsUOHoeTx9WHABrbDDVrQYUo1KsThg6Uo6Uahd8yMIuWUDlzjgF722KBn0w==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-jfyHjPvrc8ORwaiIJ3xgzIveRhlImn21TiblpakeF2Ft5HgxJ+Lb8eIN/2VUJFjdqLpQ3H1nGSJspHYzeU5RtQ==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-/gIqWofLBYLcYcEGt1jxZPD+kgCJOoajS22NxxbVi80MrUxvT586qqbhYiiAesBxRUgCZOJnK39BF2S4A0Ik7Q==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-4w1m8WFL5UJElwTO978P9fOub2vtLCYRLD0YpmXY+AumZw02wLRH6Al7xY2Sgchbgm/uLHkCDzeUTYUi9GzURg==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-uzZGjqxFhz/LUAKnbBYApgelXBrXSJt25fad8TxL5RoDVGgd22pxPErwEtCTAbhJyL6Z5Clj9p5vk9Vve+sodg==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-kyriGuLkOu5uVv3cHRacJRk8XMm9gga7OzCuf3oxbgfqqIaazva4Qw8N19RCpVCW1EfwyWFGq3h5VD/QplkA0w==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-Oh2FPN26M+JlSFvLr7rOJEyTO7n0AUu5x3gHCx4mX9Dqm0JQsL0T17nUMc8pra/nHOemiK3W+dCIm0Y7/8GQiQ==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-lKlI9BaHXy+L30d2CKWWn3B9Ye7LOqRHav8rzXcxAQMfZxj4iK1UuGRT+27/KzQMZBYMPd9G4R7x7lV8RngFdQ==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-y6s2yant8vBdvIw6chTlPVxf9T5lzdmpSyEsI6gX3obTSaJyhw6nbfl6iWcQNx1jjgp4dYVhEf8PneNMqC8VQw==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-U21Aj8pPjxW7wEH1Itm7Ik1rBO5rTXyxKWLjZXEDlT1agXxcwCVbL/5rffUQzbeRFAk/t4KiUGi4rTvAS62wTQ==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-WH8dfjSMWa5uCzheemqy+UzQXIpIymWVrnBOK3L3eBusYFhAg3L0ChBhjKBDjPDlKOquoeRrdt5D8ebgBjlY8g==}
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-jtJAqqnltG12YCy7VpTtIIEOlRNmVDz11SQrZlxFUHWgIYa0UcTEUDeLpP4h9IW7KtnxJKBo/xu1JLRq4tY6fw==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-D6sKXKLPZZ45csOfj3mUQ1bzBgO5mJw7qfpAIYLlk7H+slrHlIoOwxwWgwcuBTY7JJ0jLgiq2062yLHxjEfdeA==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-VaWHjSacuvcDpUPKriQMeG4xzNTKRuK7tZJL0toDQdB4uei2LX5qUBUqTrI0oBWW5Rk0QdUZR83LkhCLVJWtew==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-/mJkwVoFZZl4TtkPmAhdZYuiSjaegJ4zs5l7YJdwKPnN7/6xmvJwrQhj9UGQHwusyUBZ5/V+6cXQFEG7Em154Q==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-gx9cV9LutgY0f25AQtFqW285jGCsHpAxYKbmoN4MOUEYOVX9WiDfrf59Ta9c/SelLvOanVsu5V1hVzBsJyX37w==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-vpSXG73F0lFMsb1uwgut7SiuFNF0Hk5QRD7i8DJO5oyyTrSkrHUjQHFFL7ONb+zmPzLC1J8SlfHXkldJZlpKww==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-I+tPJgq7M5Qcu4pX97J1Hfo0s+bnCmj32IiZ3AJTFFA/+xScaL0SF7T7tW5YtXR45ZwYjaSdnmQ80CwrLxJjdQ==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-6EXelSVOv0LQrAretlDMIxMV/e1+GfJftBfPsHuCTPrTbmcoEIHQRC9VsGQDS/k4ija6oA2GTqTvoni8Ztbk8A==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
resolution: {integrity: sha512-hh62wuc4HZ+BydUC62bA74hgr+qV888iWm23V222d6rqIkOpYzi5tcoPAh48Mgjl2gMSigTE6sySJez7reffNQ==}
'@tsparticles/[email protected].2':
resolution: {integrity: sha512-XwcyTnRssZ+ssuL500ojFaMdR5w+eb9XVs7MHd2G8q3A5N5a2UAKIhExiTEbWyAGXSgfHo/LQxUTX2Lq1cxSRg==}
peerDependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tufjs/[email protected]':
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
@@ -3785,8 +3785,8 @@ packages:
cpu: [x64]
os: [win32]
'@versatiles/style@9.0.0':
resolution: {integrity: sha512-hKtVfdQvxLYX7BaGKQhgtcnOHtj+dc1qU7hXL3V4KzMWU+RZtmGI83m5JPxGIB5M/UoDKc3cyXZaf2OeiL5jCA==}
'@versatiles/style@8.1.1':
resolution: {integrity: sha512-wotbl1VYI0JK94oq7Ji/QTdUi4jHoXtWpBnia/tcFBvAM8Ho4URnRjkNdJ6VbMM/3PqoFEYhUispAhsdCvv5hA==}
engines: {node: '>=20'}
peerDependencies:
maplibre-gl: '>=5.5.0'
@@ -3864,8 +3864,8 @@ packages:
'@vscode/[email protected]':
resolution: {integrity: sha512-2ZUIRfhofZ/npLlf872EBnPmn27Kt4M2UssmQIfnJvgGgMYZJ5fvtHEDnttBBf2hnVtBgNCqZMVHJA+wsFVqTA==}
'@vvo/[email protected]4.0':
resolution: {integrity: sha512-GC7i3D4rCs6l5jnf6NofoRRumZwVHS1cBNhgbenDLLyJNqJOrTo/8zrWovIrJl5BujiFurqn0Ayj10wysmOKrQ==}
'@vvo/[email protected]2.0':
resolution: {integrity: sha512-MGsedU65ypk/Y7yrp+oOLBzhQj/124xvyoUzx7csPiBSf1b3lgTp5imatuWU4SF6s1Ue3I6GxC+GxE8R5O1l0w==}
'@web/[email protected]':
resolution: {integrity: sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==}
@@ -4050,6 +4050,9 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
[email protected]:
resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
[email protected]:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@@ -4397,9 +4400,6 @@ packages:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==}
[email protected]:
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
engines: {node: '>= 10.0'}
@@ -4420,6 +4420,10 @@ packages:
resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==}
engines: {node: '>=18.20'}
[email protected]:
resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
[email protected]:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
@@ -4905,9 +4909,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
[email protected]:
resolution: {integrity: sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==}
[email protected]:
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
engines: {node: '>= 0.4'}
@@ -4961,8 +4962,8 @@ packages:
eslint-plugin-import: '>=1.4.0'
webpack: '>=1.11.0'
[email protected]8.0:
resolution: {integrity: sha512-UfxHFmV+f9vwybrNn6LQ1rPzfeDMPGi/dRbqQnCFO/jBzYrujSOyX7jxqBSYr/HwXs/V4XJdEGPvC3Re46nzKg==}
[email protected]7.1:
resolution: {integrity: sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/utils': ^8.56.0
@@ -5066,6 +5067,10 @@ packages:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
[email protected]:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
[email protected]:
resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==}
@@ -5283,6 +5288,10 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
[email protected]:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
@@ -5489,6 +5498,10 @@ packages:
resolution: {integrity: sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==}
engines: {node: '>= 20'}
[email protected]:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
[email protected]:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
@@ -5625,6 +5638,11 @@ packages:
[email protected]:
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
[email protected]:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==}
engines: {node: '>= 0.4'}
@@ -5774,6 +5792,10 @@ packages:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
@@ -6116,8 +6138,8 @@ packages:
[email protected]:
resolution: {integrity: sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==}
[email protected]7.0:
resolution: {integrity: sha512-2chIZwpLGenoUQOcnm9Pk4SLbA8ppNFq8jlpXiq9vcYm3Y1wJ3fc82FL0GLwBaPYJCLzPv/mC46KEupVov75rQ==}
[email protected]6.0:
resolution: {integrity: sha512-gApO66EMtoDZuio680RUBvskR7ahFd/NNhLN4tBAs+Y7FeT4gQV8zVFgYVMRmFxQznOEzecW19w6xOd9XjZjiA==}
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
[email protected]:
@@ -6413,6 +6435,10 @@ packages:
resolution: {integrity: sha512-vzc1svxw/kw1IRjFsLi6gaxe1Olqm88V0tIfu2u5raL0b1gChe6ZEXNkyUlKxUC7s/egt5NxZHkbY18tMKKLfQ==}
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
[email protected]:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
@@ -7014,8 +7040,8 @@ packages:
[email protected]:
resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==}
[email protected].7:
resolution: {integrity: sha512-mTJ9LAyBwHhO2T/PNaI0oWxmKl7FFuJ+ZEHKL1rRO69fWV1Y4qcsFB3gpuD2HfHvJCwvJq5ChibjjAGe0wc0vw==}
[email protected].6:
resolution: {integrity: sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==}
engines: {node: '>= 14'}
hasBin: true
@@ -7269,6 +7295,10 @@ packages:
resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==}
engines: {node: '>=10'}
[email protected]:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
@@ -7409,10 +7439,6 @@ packages:
resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==}
engines: {node: '>=20.0.0'}
[email protected]:
resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==}
engines: {node: ^16.14.0 || >= 17.3.0}
[email protected]:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
@@ -9797,40 +9823,40 @@ snapshots:
- '@types/node'
- '@types/react'
'@html-eslint/[email protected]8.0':
'@html-eslint/[email protected]7.0':
dependencies:
'@html-eslint/types': 0.68.0
'@html-eslint/types': 0.67.0
html-standard: 0.0.13
'@html-eslint/[email protected]8.0([email protected]([email protected]))':
'@html-eslint/[email protected]7.0([email protected]([email protected]))':
dependencies:
'@eslint/plugin-kit': 0.4.1
'@html-eslint/core': 0.68.0
'@html-eslint/parser': 0.68.0
'@html-eslint/template-parser': 0.68.0
'@html-eslint/template-syntax-parser': 0.68.0
'@html-eslint/types': 0.68.0
'@html-eslint/core': 0.67.0
'@html-eslint/parser': 0.67.0
'@html-eslint/template-parser': 0.67.0
'@html-eslint/template-syntax-parser': 0.67.0
'@html-eslint/types': 0.67.0
'@rviscomi/capo.js': 2.2.1
eslint: 10.14.0([email protected])
html-standard: 0.0.13
'@html-eslint/[email protected]8.0':
'@html-eslint/[email protected]7.0':
dependencies:
'@html-eslint/template-syntax-parser': 0.68.0
'@html-eslint/types': 0.68.0
'@html-eslint/template-syntax-parser': 0.67.0
'@html-eslint/types': 0.67.0
css-tree: 3.2.1
es-html-parser: 0.3.2
'@html-eslint/[email protected]8.0':
'@html-eslint/[email protected]7.0':
dependencies:
'@html-eslint/types': 0.68.0
'@html-eslint/types': 0.67.0
es-html-parser: 0.3.2
'@html-eslint/[email protected]8.0':
'@html-eslint/[email protected]7.0':
dependencies:
'@html-eslint/types': 0.68.0
'@html-eslint/types': 0.67.0
'@html-eslint/[email protected]8.0':
'@html-eslint/[email protected]7.0':
dependencies:
'@types/css-tree': 2.3.11
'@types/estree': 1.0.9
@@ -10080,11 +10106,11 @@ snapshots:
dependencies:
kdbush: 4.1.0
'@maplibre/[email protected](@types/[email protected])([email protected])([email protected]7.0)':
'@maplibre/[email protected](@types/[email protected])([email protected])([email protected]6.0)':
dependencies:
'@types/leaflet': 1.9.22
leaflet: 1.9.4
maplibre-gl: 6.17.0
maplibre-gl: 6.16.0
'@maplibre/[email protected]':
dependencies:
@@ -11092,88 +11118,88 @@ snapshots:
magic-string: 0.30.21
string.prototype.matchall: 4.0.12
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
'@tsparticles/[email protected].2':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/plugin-blend': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/plugin-hex-color': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/plugin-hsl-color': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/plugin-move': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/plugin-rgb-color': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/shape-circle': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/updater-opacity': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/updater-out-modes': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/updater-paint': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/updater-size': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/engine': 4.5.2
'@tsparticles/plugin-blend': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/plugin-hex-color': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/plugin-hsl-color': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/plugin-move': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/plugin-rgb-color': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/shape-circle': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/updater-opacity': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/updater-out-modes': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/updater-paint': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/updater-size': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3': {}
'@tsparticles/[email protected].2': {}
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)(@tsparticles/[email protected].3(@tsparticles/[email protected].3))':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)(@tsparticles/[email protected].2(@tsparticles/[email protected].2))':
dependencies:
'@tsparticles/canvas-utils': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/engine': 4.5.3
'@tsparticles/plugin-interactivity': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/canvas-utils': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/engine': 4.5.2
'@tsparticles/plugin-interactivity': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3':
'@tsparticles/[email protected].2':
dependencies:
'@tsparticles/basic': 4.5.3
'@tsparticles/engine': 4.5.3
'@tsparticles/interaction-particles-links': 4.5.3(@tsparticles/[email protected].3)(@tsparticles/[email protected].3(@tsparticles/[email protected].3))
'@tsparticles/plugin-interactivity': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/basic': 4.5.2
'@tsparticles/engine': 4.5.2
'@tsparticles/interaction-particles-links': 4.5.2(@tsparticles/[email protected].2)(@tsparticles/[email protected].2(@tsparticles/[email protected].2))
'@tsparticles/plugin-interactivity': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/animation-utils': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/engine': 4.5.3
'@tsparticles/animation-utils': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/engine': 4.5.3
'@tsparticles/engine': 4.5.2
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
dependencies:
'@tsparticles/animation-utils': 4.5.3(@tsparticles/[email protected].3)
'@tsparticles/engine': 4.5.3
'@tsparticles/animation-utils': 4.5.2(@tsparticles/[email protected].2)
'@tsparticles/engine': 4.5.2
'@tufjs/[email protected]': {}
@@ -11527,11 +11553,11 @@ snapshots:
'@unrs/[email protected]':
optional: true
'@versatiles/style@9.0.0([email protected]7.0)':
'@versatiles/style@8.1.1([email protected]6.0)':
dependencies:
'@maplibre/maplibre-gl-style-spec': 26.5.1
optionalDependencies:
maplibre-gl: 6.17.0
maplibre-gl: 6.16.0
'@vibrant/[email protected]': {}
@@ -11621,7 +11647,7 @@ snapshots:
'@vscode/[email protected]': {}
'@vvo/[email protected]4.0': {}
'@vvo/[email protected]2.0': {}
'@web/[email protected]':
dependencies:
@@ -11760,6 +11786,8 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.2
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -12137,8 +12165,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
source-map: 0.6.1
@@ -12153,6 +12179,12 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
arch: 2.2.0
execa: 5.1.1
is-wsl: 2.2.0
[email protected]:
dependencies:
string-width: 4.2.3
@@ -12661,8 +12693,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
es-errors: 1.3.0
@@ -12721,17 +12751,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected]8.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected]):
[email protected]7.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected]):
dependencies:
'@typescript-eslint/types': 8.72.1
cjs-module-lexer: 2.3.0
comment-parser: 1.4.8
debug: 4.4.3([email protected])
es-module-lexer: 3.0.4
eslint: 10.14.0([email protected])
eslint-import-context: 0.1.9([email protected])
is-glob: 4.0.3
minimatch: 11.0.5
minimatch: 10.2.6
semver: 7.8.5
stable-hash-x: 0.2.0
unrs-resolver: 1.12.2
@@ -12856,6 +12884,18 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
cross-spawn: 7.0.6
get-stream: 6.0.1
human-signals: 2.1.0
is-stream: 2.0.1
merge-stream: 2.0.0
npm-run-path: 4.0.1
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
[email protected]: {}
[email protected]:
@@ -13105,6 +13145,8 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.2
[email protected]: {}
[email protected]:
dependencies:
call-bound: 1.0.4
@@ -13377,6 +13419,8 @@ snapshots:
- kerberos
- supports-color
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -13506,6 +13550,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
call-bound: 1.0.4
@@ -13628,6 +13674,10 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
is-docker: 2.2.1
[email protected]: {}
[email protected]: {}
@@ -13994,7 +14044,7 @@ snapshots:
[email protected]: {}
[email protected]7.0:
[email protected]6.0:
dependencies:
'@mapbox/point-geometry': 1.1.0
'@mapbox/tiny-sdf': 2.3.0
@@ -14318,6 +14368,10 @@ snapshots:
- kerberos
- supports-color
[email protected]:
dependencies:
path-key: 3.1.1
[email protected]:
dependencies:
boolbase: 1.0.0
@@ -14988,7 +15042,7 @@ snapshots:
path-to-regexp: 3.3.0
range-parser: 1.2.0
[email protected].7([email protected]):
[email protected].6([email protected]):
dependencies:
'@zeit/schemas': 2.36.0
ajv: 8.18.0
@@ -14996,10 +15050,10 @@ snapshots:
boxen: 7.0.0
chalk: 5.0.1
chalk-template: 0.4.0
clipboardy: 3.0.0
compression: 1.8.1([email protected])
is-port-reachable: 4.0.0
serve-handler: 6.1.7
tinyclip: 0.1.15
update-check: 1.5.4
transitivePeerDependencies:
- supports-color
@@ -15343,6 +15397,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -15458,8 +15514,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
-7
View File
@@ -1,7 +0,0 @@
/** Where a value is in a config, e.g. `["actions", 0, "then", 1]`. */
export type ConfigPath = (string | number)[];
export const stringifyPath = (path: ConfigPath): string => path.join("/");
export const pathEquals = (a: ConfigPath, b: ConfigPath): boolean =>
a.length === b.length && a.every((segment, index) => segment === b[index]);
+2 -6
View File
@@ -401,13 +401,9 @@ export class HaDataTable extends LitElement {
}
if (properties.has("data")) {
// Clean up checked rows that no longer exist or can no longer be selected
// Clean up checked rows that no longer exist in the data
if (this._checkedRows.length) {
const validIds = new Set(
this.data
.filter((row) => row.selectable !== false)
.map((row) => String(row[this.id]))
);
const validIds = new Set(this.data.map((row) => String(row[this.id])));
const validCheckedRows = this._checkedRows.filter((id) =>
validIds.has(id)
);
@@ -97,8 +97,6 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
@state() private _dateValue?: string;
@state() private _rangeIncomplete = false;
@state() private _timeValue = {
from: { hours: 0, minutes: 0 },
to: { hours: 23, minutes: 59 },
@@ -197,7 +195,6 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
.locale=${this._i18n.locale.language}
.focusedDate=${this._focusDate}
@focusday=${this._focusChanged}
@rangestart=${this._handleRangeStart}
@change=${this._handleChange}
show-outside-days
.firstDayOfWeek=${firstWeekdayIndex(this._i18n.locale)}
@@ -256,9 +253,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
<ha-button appearance="plain" @click=${this._cancel}
>${this._i18n.localize("ui.common.cancel")}</ha-button
>
<ha-button
.disabled=${!this._dateValue || this._rangeIncomplete}
@click=${this._save}
<ha-button .disabled=${!this._dateValue} @click=${this._save}
>${this._i18n.localize(
"ui.components.date-range-picker.select"
)}</ha-button
@@ -285,7 +280,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
}
private _save() {
if (!this._dateValue || this._rangeIncomplete) {
if (!this._dateValue) {
return;
}
@@ -336,21 +331,9 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
this._focusDate = ev.target.focusedDate;
}
private _handleRangeStart(
ev: HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
) {
this._rangeIncomplete = matchMedia("(hover: hover)").matches;
if (this._rangeIncomplete) {
return;
}
const day = ev.target.tentative;
this._dateValue = `${day}/${day}`;
}
private _handleChange(
ev: HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
) {
this._rangeIncomplete = false;
const dateElement = ev.target as HTMLElementTagNameMap["calendar-range"];
this._dateValue = dateElement.value;
this._focusDate = dateElement.focusedDate;
+4
View File
@@ -62,6 +62,10 @@ export class HaControlSelect extends LitElement {
});
}
public override focus(): void {
this._focusOption(this._tabbableIndex);
}
private _handleBlur(ev: FocusEvent) {
// Only reset if focus is leaving the entire component
if (!this.contains(ev.relatedTarget as Node)) {
-1
View File
@@ -219,7 +219,6 @@ export class HaControlSlider extends LitElement {
this.pressed = false;
this._hideTooltip();
this.value = savedValue;
fireEvent(this, "slider-moved", { value: undefined });
});
this._mc.on("panmove", (e) => {
if (this.disabled) return;
-4
View File
@@ -214,19 +214,15 @@ class HaHsColorPicker extends LitElement {
this._mc.add(new Tap({ event: "singletap" }));
let savedPosition;
let savedValue: [number, number] | undefined;
this._mc.on("panstart", (e) => {
if (this.disabled) return;
this._pressed = e.pointerType;
savedPosition = this._cursorPosition;
savedValue = this._localValue;
});
this._mc.on("pancancel", () => {
if (this.disabled) return;
this._pressed = undefined;
this._cursorPosition = savedPosition;
this._localValue = savedValue;
fireEvent(this, "cursor-moved", { value: undefined });
});
this._mc.on("panmove", (e) => {
if (this.disabled) return;
@@ -21,8 +21,6 @@ export class HaActionSelector extends LitElement {
@property({ attribute: false }) public value?: Action;
@property() public name?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@@ -53,7 +51,6 @@ export class HaActionSelector extends LitElement {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-action
.pathKey=${this.name}
.disabled=${this.disabled}
.actions=${this._actions(this.value)}
.hass=${this.hass}
@@ -16,8 +16,6 @@ export class HaConditionSelector extends LitElement {
@property({ attribute: false }) public value?: Condition;
@property() public name?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@@ -29,7 +27,6 @@ export class HaConditionSelector extends LitElement {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-condition
.pathKey=${this.name}
.disabled=${this.disabled}
.conditions=${this.value || []}
.hass=${this.hass}
@@ -17,8 +17,6 @@ export class HaTriggerSelector extends LitElement {
@property({ attribute: false }) public value?: Trigger;
@property() public name?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@@ -34,7 +32,6 @@ export class HaTriggerSelector extends LitElement {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-trigger
.pathKey=${this.name}
.disabled=${this.disabled}
.triggers=${this._triggers(this.value)}
.hass=${this.hass}
-128
View File
@@ -1,128 +0,0 @@
import type { HassEntities } from "home-assistant-js-websocket";
import type { ConfigEntry } from "./config_entries";
import type { DataEntryFlowProgress } from "./data_entry_flow";
import type { EntityRegistryEntry } from "./entity/entity_registry";
import type { IntegrationSetup } from "./integration";
import type { RepairsIssue } from "./repairs";
import type { LoggedError } from "./system_log";
import { getLoggedErrorIntegration } from "./system_log";
export type HealthStatus = "ok" | "warning" | "error" | "info";
export type OverallHealthStatus = Exclude<HealthStatus, "info">;
export interface ConfigEntryHealth {
status: OverallHealthStatus;
state: HealthStatus;
entities: {
status: HealthStatus;
total: number;
unavailable: EntityRegistryEntry[];
};
repairs: { status: HealthStatus; issues: RepairsIssue[] };
logs: { status: HealthStatus; errors: number; warnings: number };
flows: { status: HealthStatus; flows: DataEntryFlowProgress[] };
setupSeconds?: number;
}
const ATTENTION_SOURCES = ["reauth", "reconfigure"];
const STATE_STATUS: Record<ConfigEntry["state"], HealthStatus> = {
loaded: "ok",
setup_in_progress: "info",
unload_in_progress: "info",
not_loaded: "info",
setup_retry: "warning",
setup_error: "error",
migration_error: "error",
failed_unload: "error",
};
const worst = (statuses: HealthStatus[]): OverallHealthStatus =>
statuses.includes("error")
? "error"
: statuses.includes("warning")
? "warning"
: "ok";
export const computeConfigEntryHealth = (
entry: ConfigEntry,
entities: EntityRegistryEntry[],
states: HassEntities,
issues: RepairsIssue[],
log: LoggedError[],
flows: DataEntryFlowProgress[],
setups: IntegrationSetup[],
// Library loggers from the manifest, so dependency errors count too.
loggers: string[] = []
): ConfigEntryHealth => {
const enabled = entities.filter((entity) => !entity.disabled_by);
// Entities without a state yet (e.g. while the entry is not loaded) count as unavailable.
const unavailable = enabled.filter(
(entity) =>
!states[entity.entity_id] ||
states[entity.entity_id].state === "unavailable"
);
const entryIssues = issues.filter(
(issue) =>
!issue.ignored && (issue.issue_domain ?? issue.domain) === entry.domain
);
const entryLog = log.filter(
(item) =>
getLoggedErrorIntegration(item) === entry.domain ||
loggers.some(
(logger) => item.name === logger || item.name.startsWith(`${logger}.`)
)
);
const sumCount = (levels: LoggedError["level"][]) =>
entryLog
.filter((item) => levels.includes(item.level))
.reduce((total, item) => total + item.count, 0);
const errors = sumCount(["error", "critical"]);
const warnings = sumCount(["warning"]);
const entryFlows = flows.filter(
(flow) =>
flow.handler === entry.domain &&
flow.context.entry_id === entry.entry_id &&
ATTENTION_SOURCES.includes(flow.context.source)
);
// A disabled entry that failed to unload still needs a restart.
const state: HealthStatus =
entry.disabled_by && entry.state !== "failed_unload"
? "info"
: STATE_STATUS[entry.state];
const health: ConfigEntryHealth = {
status: "ok",
state,
entities: {
status: entry.disabled_by || !unavailable.length ? "ok" : "warning",
total: enabled.length,
unavailable,
},
repairs: {
status: entryIssues.some((issue) => issue.severity !== "warning")
? "error"
: entryIssues.length
? "warning"
: "ok",
issues: entryIssues,
},
logs: {
status: errors ? "error" : warnings ? "warning" : "ok",
errors,
warnings,
},
flows: { status: entryFlows.length ? "error" : "ok", flows: entryFlows },
setupSeconds: setups.find((setup) => setup.domain === entry.domain)
?.seconds,
};
health.status = worst([
state,
health.entities.status,
health.repairs.status,
health.logs.status,
health.flows.status,
]);
return health;
};
+3 -3
View File
@@ -1,6 +1,6 @@
import type { Connection } from "home-assistant-js-websocket";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
import type {
DataEntryFlowProgress,
DataEntryFlowStep,
@@ -78,10 +78,10 @@ export const deleteConfigFlow = (
// The backend indexes a single key, and only these two exist.
export const getConfigFlowHandlers = (
callApi: HomeAssistantApi["callApi"],
hass: HomeAssistant,
type?: "helper" | "integration"
) =>
callApi<string[]>(
hass.callApi<string[]>(
"GET",
`config/config_entries/flow_handlers${type ? `?type=${type}` : ""}`
);
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
export interface Counter {
id: string;
@@ -25,10 +25,10 @@ export const fetchCounter = (hass: HomeAssistant) =>
hass.callWS<Counter[]>({ type: "counter/list" });
export const createCounter = (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
values: CounterMutableParams
) =>
callWS<Counter>({
hass.callWS<Counter>({
type: "counter/create",
...values,
});
+3 -3
View File
@@ -1,6 +1,6 @@
import type { Connection } from "home-assistant-js-websocket";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { HomeAssistant, HomeAssistantApi } from "../../types";
import type { HomeAssistant } from "../../types";
import type { ConfigEntry } from "../config_entries";
import type {
EntityRegistryDisplayEntry,
@@ -231,11 +231,11 @@ export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
devices.filter((device) => device.area_id === areaId);
export const updateDeviceRegistryEntry = (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
deviceId: string,
updates: Partial<DeviceRegistryEntryMutableParams>
) =>
callWS<DeviceRegistryEntry>({
hass.callWS<DeviceRegistryEntry>({
type: "config/device_registry/update",
device_id: deviceId,
...updates,
+9 -9
View File
@@ -5,7 +5,7 @@ import memoizeOne from "memoize-one";
import { computeDomain } from "../../common/entity/compute_domain";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { debounce } from "../../common/util/debounce";
import type { HomeAssistant, HomeAssistantApi } from "../../types";
import type { HomeAssistant } from "../../types";
import type { LightColor } from "../light";
import type { NextNamePart, RegistryEntry } from "../registry";
import type { Segment } from "../vacuum";
@@ -284,11 +284,11 @@ export const getExtendedEntityRegistryEntries = (
});
export const updateEntityRegistryEntry = (
callWS: HomeAssistantApi["callWS"],
hass: Pick<HomeAssistant, "callWS">,
entityId: string,
updates: Partial<EntityRegistryEntryUpdateParams>
): Promise<UpdateEntityRegistryEntryResult> =>
callWS({
hass.callWS({
type: "config/entity_registry/update",
entity_id: entityId,
...updates,
@@ -388,11 +388,11 @@ export interface AutomaticEntityIdOverrides {
}
export const getAutomaticEntityIds = (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
entity_ids: string[],
overrides?: Record<string, AutomaticEntityIdOverrides>
) =>
callWS<Record<string, string | null>>({
hass.callWS<Record<string, string | null>>({
type: "config/entity_registry/get_automatic_entity_ids",
entity_ids,
...(overrides && { overrides }),
@@ -403,22 +403,22 @@ export const getAutomaticEntityIds = (
* the area. Returns the entity ID of each entity after the change.
*/
export const updateEntitiesArea = async (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
entityIds: string[],
areaId: string
): Promise<Record<string, string>> => {
await Promise.all(
entityIds.map((entityId) =>
updateEntityRegistryEntry(callWS, entityId, { area_id: areaId })
updateEntityRegistryEntry(hass, entityId, { area_id: areaId })
)
);
const newEntityIds = await getAutomaticEntityIds(callWS, entityIds);
const newEntityIds = await getAutomaticEntityIds(hass, entityIds);
const result: Record<string, string> = {};
for (const entityId of entityIds) {
const newEntityId = newEntityIds[entityId];
if (newEntityId) {
// eslint-disable-next-line no-await-in-loop
await updateEntityRegistryEntry(callWS, entityId, {
await updateEntityRegistryEntry(hass, entityId, {
new_entity_id: newEntityId,
});
}
+9
View File
@@ -49,12 +49,20 @@ export interface SecurityAlertEntityConfig {
export interface SecurityFrontendSystemData {
alert_entities?: SecurityAlertEntityConfig[];
favorite_entities?: string[];
hidden_entities?: string[];
}
export interface MaintenanceFrontendSystemData {
hidden_entities?: string[];
}
export interface ClimateFrontendSystemData {
hidden_entities?: string[];
}
export type HomeSummaryPanelSystemDataKey =
"security" | "maintenance" | "climate";
export interface EnergyFrontendSystemData {
// Stable "<view>.<card-type>" keys of energy dashboard cards the user has
// hidden. An absent key or array means nothing is hidden (all cards visible),
@@ -74,6 +82,7 @@ declare global {
energy: EnergyFrontendSystemData;
security: SecurityFrontendSystemData;
maintenance: MaintenanceFrontendSystemData;
climate: ClimateFrontendSystemData;
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
export interface InputBoolean {
id: string;
@@ -17,10 +17,10 @@ export const fetchInputBoolean = (hass: HomeAssistant) =>
hass.callWS<InputBoolean[]>({ type: "input_boolean/list" });
export const createInputBoolean = (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
values: InputBooleanMutableParams
) =>
callWS<InputBoolean>({
hass.callWS<InputBoolean>({
type: "input_boolean/create",
...values,
});
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
export interface InputButton {
id: string;
@@ -15,10 +15,10 @@ export const fetchInputButton = (hass: HomeAssistant) =>
hass.callWS<InputButton[]>({ type: "input_button/list" });
export const createInputButton = (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
values: InputButtonMutableParams
) =>
callWS<InputButton>({
hass.callWS<InputButton>({
type: "input_button/create",
...values,
});
+2 -2
View File
@@ -45,10 +45,10 @@ export const fetchInputDateTime = (hass: HomeAssistant) =>
hass.callWS<InputDateTime[]>({ type: "input_datetime/list" });
export const createInputDateTime = (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
values: InputDateTimeMutableParams
) =>
callWS<InputDateTime>({
hass.callWS<InputDateTime>({
type: "input_datetime/create",
...values,
});
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
export interface InputNumber {
id: string;
@@ -29,10 +29,10 @@ export const fetchInputNumber = (hass: HomeAssistant) =>
hass.callWS<InputNumber[]>({ type: "input_number/list" });
export const createInputNumber = (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
values: InputNumberMutableParams
) =>
callWS<InputNumber>({
hass.callWS<InputNumber>({
type: "input_number/create",
...values,
});
+3 -3
View File
@@ -2,7 +2,7 @@ import type {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
interface InputSelectEntityAttributes extends HassEntityAttributeBase {
options: string[];
@@ -41,10 +41,10 @@ export const fetchInputSelect = (hass: HomeAssistant) =>
hass.callWS<InputSelect[]>({ type: "input_select/list" });
export const createInputSelect = (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
values: InputSelectMutableParams
) =>
callWS<InputSelect>({
hass.callWS<InputSelect>({
type: "input_select/create",
...values,
});
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
export interface InputText {
id: string;
@@ -36,10 +36,10 @@ export const fetchInputText = (hass: HomeAssistant) =>
hass.callWS<InputText[]>({ type: "input_text/list" });
export const createInputText = (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
values: InputTextMutableParams
) =>
callWS<InputText>({
hass.callWS<InputText>({
type: "input_text/create",
...values,
});
+2 -2
View File
@@ -196,8 +196,8 @@ export const fetchIntegrationManifest = (
.callWS<IntegrationManifest>({ type: "manifest/get", integration })
.then(sanitizeManifest);
export const fetchIntegrationSetups = (callWS: HomeAssistant["callWS"]) =>
callWS<IntegrationSetup[]>({ type: "integration/setup_info" });
export const fetchIntegrationSetups = (hass: HomeAssistant) =>
hass.callWS<IntegrationSetup[]>({ type: "integration/setup_info" });
export const fetchIntegrationLogInfo = (conn) =>
conn.sendMessagePromise({
+3 -3
View File
@@ -1,9 +1,9 @@
import type { Connection, HassEventBase } from "home-assistant-js-websocket";
import { getCollection } from "home-assistant-js-websocket";
import type { ConfigPath } from "../common/util/config-path";
import type { HuiBadge } from "../panels/lovelace/badges/hui-badge";
import type { HuiCard } from "../panels/lovelace/cards/hui-card";
import type { HuiSection } from "../panels/lovelace/sections/hui-section";
import type { LovelacePath } from "../panels/lovelace/editor/lovelace-path";
import type { Lovelace } from "../panels/lovelace/types";
import type { HomeAssistant } from "../types";
import type { LovelaceSectionConfig } from "./lovelace/config/section";
@@ -21,7 +21,7 @@ export interface LovelaceViewElement extends HTMLElement {
narrow?: boolean;
// Temporary compatibility: custom view layouts still read the view index
index?: number;
path?: ConfigPath;
path?: LovelacePath;
cards?: HuiCard[];
badges?: HuiBadge[];
sections?: HuiSection[];
@@ -34,7 +34,7 @@ export interface LovelaceSectionElement extends HTMLElement {
hass?: HomeAssistant;
lovelace?: Lovelace;
preview?: boolean;
path?: ConfigPath;
path?: LovelacePath;
cards?: HuiCard[];
badges?: HuiBadge[];
sections?: HuiSection[];
+2 -3
View File
@@ -21,9 +21,8 @@ export const findOTBRInfoForDataset = (
}
const preferredAddress = dataset.preferred_extended_address;
const preferred = preferredAddress ? otbrInfo[preferredAddress] : undefined;
if (preferred?.extended_pan_id === dataset.extended_pan_id) {
return preferred;
if (preferredAddress && otbrInfo[preferredAddress]) {
return otbrInfo[preferredAddress];
}
return Object.values(otbrInfo).find(
+2 -2
View File
@@ -15,7 +15,7 @@ export const regenerateEntityIds = async (
hass: HomeAssistant,
entities: string[]
): Promise<void> => {
const entityIdsMapping = await getAutomaticEntityIds(hass.callWS, entities);
const entityIdsMapping = await getAutomaticEntityIds(hass, entities);
const entityIdsEntries = Object.entries(entityIdsMapping);
@@ -95,7 +95,7 @@ export const regenerateEntityIds = async (
entityIdsEntries
.filter(([oldId, newId]) => newId && oldId !== newId)
.forEach(([oldEntityId, newEntityId]) =>
updateEntityRegistryEntry(hass.callWS, oldEntityId, {
updateEntityRegistryEntry(hass, oldEntityId, {
new_entity_id: newEntityId!,
}).catch((err: any) => {
showAlertDialog(element, {
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
export const weekdays = [
"sunday",
@@ -33,10 +33,10 @@ export const fetchSchedule = (hass: HomeAssistant) =>
hass.callWS<Schedule[]>({ type: "schedule/list" });
export const createSchedule = (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
values: ScheduleMutableParams
) =>
callWS<Schedule>({
hass.callWS<Schedule>({
type: "schedule/create",
...values,
});
+2 -2
View File
@@ -23,8 +23,8 @@ export interface LoggedError {
first_occurred: number;
}
export const fetchSystemLog = async (callWS: HomeAssistant["callWS"]) => {
const log = await callWS<LoggedError[]>({ type: "system_log/list" });
export const fetchSystemLog = async (hass: HomeAssistant) => {
const log = await hass.callWS<LoggedError[]>({ type: "system_log/list" });
for (const error of log) {
error.level = error.level.toLowerCase() as LoggedError["level"];
}
+3 -6
View File
@@ -11,7 +11,7 @@ import secondsToDuration from "../common/datetime/seconds_to_duration";
import { formatNumericDuration } from "../common/datetime/format_duration";
import type { FormatEntityStateFunc } from "../common/translations/entity-state";
import type { HaDurationData } from "../components/ha-duration-input";
import type { HomeAssistant, HomeAssistantApi } from "../types";
import type { HomeAssistant } from "../types";
import type { FrontendLocaleData } from "./translation";
export type TimerEntity = HassEntityBase & {
@@ -48,11 +48,8 @@ export interface TimerMutableParams {
export const fetchTimer = (hass: HomeAssistant) =>
hass.callWS<Timer[]>({ type: "timer/list" });
export const createTimer = (
callWS: HomeAssistantApi["callWS"],
values: TimerMutableParams
) =>
callWS<Timer>({
export const createTimer = (hass: HomeAssistant, values: TimerMutableParams) =>
hass.callWS<Timer>({
type: "timer/create",
...values,
});
@@ -303,7 +303,7 @@ class StepFlowCreateEntry extends LitElement {
if (Object.keys(this._deviceUpdate).length) {
const deviceUpdates = Object.entries(this._deviceUpdate).map(
([deviceId, update]) =>
updateDeviceRegistryEntry(this.hass.callWS, deviceId, {
updateDeviceRegistryEntry(this.hass, deviceId, {
name_by_user: update.name,
area_id: update.area,
}).catch((err: unknown) => {
@@ -333,14 +333,14 @@ class StepFlowCreateEntry extends LitElement {
});
const entityIdsMapping = await getAutomaticEntityIds(
this.hass.callWS,
this.hass,
entityIds
);
Object.entries(entityIdsMapping).forEach(([oldEntityId, newEntityId]) => {
if (newEntityId) {
entityUpdates.push(
updateEntityRegistryEntry(this.hass.callWS, oldEntityId, {
updateEntityRegistryEntry(this.hass, oldEntityId, {
new_entity_id: newEntityId,
}).catch((err) =>
showAlertDialog(this, {
@@ -423,7 +423,7 @@ class StepFlowCreateEntry extends LitElement {
): Promise<ExtEntityRegistryEntry[]> {
try {
const newEntityIds = await updateEntitiesArea(
this.hass.callWS,
this.hass,
this._entitiesWithoutDevice(entities).map((entity) => entity.entity_id),
areaId
);
@@ -146,7 +146,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
}
const result = await updateEntityRegistryEntry(
this._api.callWS,
this._api,
this.entry.entity_id,
{
options_domain: "cover",
@@ -75,7 +75,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
private async _save(newFavoriteColors: LightColor[]): Promise<void> {
const result = await updateEntityRegistryEntry(
this._api.callWS,
this._api,
this.entry!.entity_id,
{
options_domain: "light",
@@ -15,7 +15,6 @@ import {
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { PendingValueSync } from "../../../../common/util/pending-value-sync";
import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-hs-color-picker";
import "../../../../components/ha-icon";
@@ -60,8 +59,6 @@ class LightRgbColorPicker extends LitElement {
@state() private _isInteracting?: boolean;
private _pendingSync = new PendingValueSync();
protected render() {
if (!this.stateObj) {
return nothing;
@@ -184,29 +181,23 @@ class LightRgbColorPicker extends LitElement {
`;
}
private _updateBrightnessAdjusted() {
const stateObj = this.stateObj;
this._brightnessAdjusted = undefined;
if (
stateObj.attributes.color_mode === LightColorMode.RGB &&
stateObj.attributes.rgb_color &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBWW) &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBW)
) {
const maxVal = Math.max(...stateObj.attributes.rgb_color);
if (maxVal < 255) {
this._brightnessAdjusted = maxVal;
}
}
}
private _updateSliderValues() {
const stateObj = this.stateObj;
if (stateObj.state === "on") {
this._updateBrightnessAdjusted();
this._brightnessAdjusted = undefined;
if (
stateObj.attributes.color_mode === LightColorMode.RGB &&
stateObj.attributes.rgb_color &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBWW) &&
!lightSupportsColorMode(stateObj, LightColorMode.RGBW)
) {
const maxVal = Math.max(...stateObj.attributes.rgb_color);
if (maxVal < 255) {
this._brightnessAdjusted = maxVal;
}
}
this._wvSliderValue =
stateObj.attributes.color_mode === LightColorMode.RGBW &&
@@ -244,23 +235,11 @@ class LightRgbColorPicker extends LitElement {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("stateObj")) {
if (this._isInteracting || !changedProps.has("stateObj")) {
return;
}
const oldStateObj = changedProps.get("stateObj") as LightEntity | undefined;
if (oldStateObj?.entity_id !== this.stateObj?.entity_id) {
this._throttleUpdateColor.cancel();
this._pendingSync.reset();
this._isInteracting = false;
this._updateSliderValues();
} else if (!this._isInteracting && this._pendingSync.shouldSync()) {
this._updateSliderValues();
} else if (this.stateObj.state === "on") {
// Keep the picker values, but the brightness normalization has to
// follow the reported state, as it is applied to the next color sent.
this._updateBrightnessAdjusted();
}
this._updateSliderValues();
}
private _hsColorCursorMoved(ev: CustomEvent) {
@@ -271,7 +250,6 @@ class LightRgbColorPicker extends LitElement {
return;
}
this._hsPickerValue = color;
this._pendingSync.schedule();
this._throttleUpdateColor();
}
@@ -334,23 +312,13 @@ class LightRgbColorPicker extends LitElement {
this._colorBrightnessSliderValue = hsv[2] / 2.55;
}
this._pendingSync.schedule();
this._throttleUpdateColor();
}
private _hsColorChanged(ev: CustomEvent) {
this._isInteracting = false;
if (!ev.detail.value) {
// Nothing is sent, so stop blocking state updates.
this._throttleUpdateColor.cancel();
this._pendingSync.reset();
this._updateSliderValues();
return;
}
// A pending throttled call would otherwise send an older color after the
// final one.
this._throttleUpdateColor.cancel();
this._hsPickerValue = ev.detail.value;
this._updateColor();
@@ -398,15 +366,11 @@ class LightRgbColorPicker extends LitElement {
private _applyColor(color: LightColor, params?: Record<string, any>) {
fireEvent(this, "color-changed", color);
return this._pendingSync.run(
() =>
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
...color,
...params,
}),
() => this._updateSliderValues()
);
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
...color,
...params,
});
}
private _colorBrightnessSliderChanged(ev: CustomEvent) {
@@ -13,7 +13,6 @@ import {
} from "../../../../common/color/convert-light-color";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stateColorCss } from "../../../../common/entity/state_color";
import { PendingValueSync } from "../../../../common/util/pending-value-sync";
import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-control-slider";
import {
@@ -67,8 +66,6 @@ class LightColorTempPicker extends LitElement {
@state() private _isInteracting?: boolean;
private _pendingSync = new PendingValueSync();
protected render() {
if (!this.stateObj) {
return nothing;
@@ -128,19 +125,11 @@ class LightColorTempPicker extends LitElement {
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
if (!changedProps.has("stateObj")) {
if (this._isInteracting || !changedProps.has("stateObj")) {
return;
}
const oldStateObj = changedProps.get("stateObj");
if (oldStateObj?.entity_id !== this.stateObj?.entity_id) {
this._throttleUpdateColorTemp.cancel();
this._pendingSync.reset();
this._isInteracting = false;
this._updateSliderValues();
} else if (!this._isInteracting && this._pendingSync.shouldSync()) {
this._updateSliderValues();
}
this._updateSliderValues();
}
private _ctColorCursorMoved(ev: CustomEvent) {
@@ -153,7 +142,6 @@ class LightColorTempPicker extends LitElement {
}
this._ctPickerValue = ct;
this._pendingSync.schedule();
this._throttleUpdateColorTemp();
}
@@ -165,16 +153,7 @@ class LightColorTempPicker extends LitElement {
private _ctColorChanged(ev: CustomEvent) {
const ct = ev.detail.value;
this._isInteracting = false;
// A pending throttled call would otherwise send an older value after the
// final one.
this._throttleUpdateColorTemp.cancel();
if (isNaN(ct)) {
// Nothing is sent, so stop blocking state updates.
this._pendingSync.reset();
this._updateSliderValues();
if (isNaN(ct) || this._ctPickerValue === ct) {
return;
}
@@ -191,15 +170,11 @@ class LightColorTempPicker extends LitElement {
private _applyColor(color: LightColor, params?: Record<string, any>) {
fireEvent(this, "color-changed", color);
return this._pendingSync.run(
() =>
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
...color,
...params,
}),
() => this._updateSliderValues()
);
this._api.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
...color,
...params,
});
}
static get styles(): CSSResultGroup {
@@ -112,7 +112,7 @@ export class HaMoreInfoTimerPresets extends LitElement {
};
const result = await updateEntityRegistryEntry(
this._api.callWS,
this._api,
this.entry.entity_id,
{
options_domain: "timer",
@@ -82,7 +82,7 @@ export class HaMoreInfoViewVacuumSegmentMapping extends LitElement {
};
try {
await updateEntityRegistryEntry(this.hass.callWS, this.params.entityId, {
await updateEntityRegistryEntry(this.hass, this.params.entityId, {
options_domain: "vacuum",
options: options,
});
@@ -110,7 +110,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
currentOptions.favorite_positions = this._favoritePositions;
const result = await updateEntityRegistryEntry(
this._api.callWS,
this._api,
this.entry.entity_id,
{
options_domain: "valve",
@@ -124,12 +124,7 @@ class MoreInfoMediaPlayer extends LitElement {
private _volumeController = new VolumeSliderController({
getSlider: () => this._volumeSlider,
step: this._volumeStep,
onSetVolume: (value) => {
// The final value is set right away, so drop the pending update from
// the input events before it, or the same value is sent twice
this._debouncedVolumeSet.cancel();
this._setVolume(value);
},
onSetVolume: (value) => this._setVolume(value),
onSetVolumeDebounced: (value) => this._debouncedVolumeSet(value),
});
+1 -1
View File
@@ -132,7 +132,7 @@ const copyFavoriteOptionsToEntities = async (
if (selected?.entity) {
const result = await Promise.allSettled(
selected.entity.map((entityId: string) =>
updateEntityRegistryEntry(hass.callWS, entityId, {
updateEntityRegistryEntry(hass, entityId, {
options_domain: domain,
options,
})
+1 -1
View File
@@ -531,7 +531,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
}
const result = await updateEntityRegistryEntry(
this.hass.callWS,
this.hass,
favoritesContext.entry.entity_id,
{
options_domain: favoritesHandler.domain,
@@ -50,7 +50,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
});
return;
}
await updateDeviceRegistryEntry(this.hass.callWS, this.deviceId, {
await updateDeviceRegistryEntry(this.hass, this.deviceId, {
area_id: area,
});
this._nextStep();
@@ -351,7 +351,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
private async _done() {
if (this._deviceName) {
try {
updateDeviceRegistryEntry(this.hass.callWS, this.deviceId, {
updateDeviceRegistryEntry(this.hass, this.deviceId, {
name_by_user: this._deviceName,
});
} catch (error: any) {
+2 -39
View File
@@ -45,42 +45,15 @@
}
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: 142px;
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
flex: 1;
padding-top: 16px;
}
#ha-launch-screen-info-box p {
max-width: 400px;
margin: 0 16px 16px;
text-align: center;
}
/* Margins, not flex `gap` (Safari 14.1). */
#ha-launch-screen .ha-launch-screen-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: -4px 16px;
}
#ha-launch-screen .ha-launch-screen-actions button {
margin: 4px;
padding: 8px 16px;
border: 1px solid transparent;
border-radius: 18px;
background: none;
color: #009ac7;
font: inherit;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
#ha-launch-screen .ha-launch-screen-actions .ha-launch-screen-continue {
border-color: currentColor;
}
.ohf-logo {
margin: 48px 0;
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
display: flex;
flex-direction: column;
align-items: center;
@@ -100,16 +73,6 @@
aspect-ratio: 237 / 24;
height: auto;
}
/* max() with var() fails after the cascade, so plain fallbacks above
need @supports (Chrome below 79). `body`: Jinja2, see below. */
@supports (margin: max(1px, 2px)) {
body #ha-launch-screen .ha-launch-screen-spacer-top {
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px );
}
.ohf-logo {
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
}
}
@media (prefers-color-scheme: dark) {
/*
* Core renders the app's index.html through Jinja2, where the minified
+17 -77
View File
@@ -65,83 +65,23 @@
</script>
<script>
if (!window.latestJS) {
// Floor check: BigInt for Chrome 67, <dialog> for Safari 15.4 and
// Firefox 98. Not transpiled, so keep this ES5.
(function () {
var ACCEPTED_KEY = "haUnsupportedBrowserAccepted";
var REMIND_AFTER = 30 * 24 * 60 * 60 * 1000;
function loadApp() {
window.customPanelJS = "<%= es5CustomPanelJS %>";
<% for (const entry of es5EntryJS) { %>
_ls("<%= entry %>", true);
<% } %>
}
if (
typeof BigInt !== "undefined" &&
typeof document.createElement("dialog").showModal === "function"
) {
loadApp();
return;
}
// Shown while the app loads too.
var box = document.getElementById("ha-launch-screen-info-box");
var message = document.createElement("p");
message.textContent =
"This browser is no longer supported. Some parts of Home Assistant may not work.";
box.appendChild(message);
// Stored: "always", or when the user last continued (ms).
var stored = null;
try {
stored = localStorage.getItem(ACCEPTED_KEY);
} catch (_err) {
// Ask on every load.
}
if (stored === "always") {
loadApp();
return;
}
var acceptedAt = Number(stored) || 0;
// A clock behind the stored time asks again.
var age = Date.now() - acceptedAt;
if (acceptedAt && age >= 0 && age < REMIND_AFTER) {
loadApp();
return;
}
var actions = document.createElement("div");
actions.className = "ha-launch-screen-actions";
box.appendChild(actions);
function addButton(label, value) {
var button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.addEventListener("click", function () {
try {
localStorage.setItem(ACCEPTED_KEY, value());
} catch (_err) {
// Continue this time only.
}
box.removeChild(actions);
loadApp();
});
actions.appendChild(button);
return button;
}
addButton("Continue anyway", function () {
return String(Date.now());
}).className = "ha-launch-screen-continue";
if (acceptedAt) {
addButton("Don't show again", function () {
return "always";
});
}
})();
// Below the [legacy] browserslist floor the entry bundles cannot be
// parsed, or dialogs cannot open, and nothing would ever leave the
// launch screen. Test the features that set the floors: BigInt for
// Chrome 67, native <dialog> for Safari 15.4 and Firefox 98. Not
// transpiled, so keep this ES5.
if (
typeof BigInt === "undefined" ||
typeof document.createElement("dialog").showModal !== "function"
) {
document.getElementById("ha-launch-screen-info-box").textContent =
"This browser is too old to run Home Assistant.";
} else {
window.customPanelJS = "<%= es5CustomPanelJS %>";
<% for (const entry of es5EntryJS) { %>
_ls("<%= entry %>", true);
<% } %>
}
}
</script>
+4 -166
View File
@@ -1,171 +1,9 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import { ChildPanelReady } from "../../layouts/panel-ready";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-background";
import "../lovelace/views/hui-view-container";
import "../../components/ha-top-app-bar-fixed";
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
type: "climate",
},
};
import { customElement } from "lit/decorators";
import { HaHomeSummaryPanelBase } from "../home/ha-home-summary-panel-base";
@customElement("ha-panel-climate")
class PanelClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParams = new URLSearchParams(window.location.search);
private _childPanelReady?: ChildPanelReady;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
// Initial setup
if (!this.hasUpdated) {
this._setup();
return;
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) {
this._setLovelace();
return;
}
if (oldHass && this.hass) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors ||
oldHass.panels !== this.hass.panels
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
return;
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
this._setLovelace();
}
}
}
private async _setup() {
await this.hass.loadFragmentTranslation("lovelace");
this._setLovelace();
}
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
200
);
private _registriesChanged = async () => {
this._setLovelace();
};
protected render() {
return html`
<ha-top-app-bar-fixed
.narrow=${this.narrow}
.backButton=${this._searchParams.has("historyBack")}
>
<div slot="title">${this.hass.localize("panel.climate")}</div>
${
this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view-background .hass=${this.hass}>
</hui-view-background>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view
></hui-view-container>
`
: nothing
}
</ha-top-app-bar-fixed>
`;
}
private async _setLovelace() {
const viewConfig = await generateLovelaceViewStrategy(
CLIMATE_LOVELACE_VIEW_CONFIG,
this.hass
);
const config = { views: [viewConfig] };
const rawConfig = { views: [CLIMATE_LOVELACE_VIEW_CONFIG] };
if (deepEqual(config, this._lovelace?.config)) {
return;
}
this._childPanelReady ??= new ChildPanelReady(this);
this._lovelace = {
config: config,
rawConfig: rawConfig,
editMode: false,
urlPath: "climate",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
user-select: none;
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}
class PanelClimate extends HaHomeSummaryPanelBase<"climate"> {
protected readonly _systemDataKey = "climate";
}
declare global {
@@ -2,9 +2,10 @@ import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy";
import {
findEntities,
findFilteredEntities,
generateEntityFilter,
type EntityFilter,
type EntityFilterSources,
} from "../../../common/entity/entity_filter";
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@@ -12,9 +13,15 @@ import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/sec
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import {
wrapEntityVisibility,
type ComputeEntityCard,
} from "../../lovelace/strategies/helpers/entity-visibility";
export interface ClimateViewStrategyConfig {
type: "climate";
hidden_entities?: string[];
edit_visibility?: boolean;
}
export const climateEntityFilters: EntityFilter[] = [
@@ -42,40 +49,53 @@ export const climateEntityFilters: EntityFilter[] = [
},
];
export const hasClimateEntities = (hass: HomeAssistant): boolean => {
const hasAreaSensor = Object.values(hass.areas).some(
(area) =>
(area.temperature_entity_id && hass.states[area.temperature_entity_id]) ||
(area.humidity_entity_id && hass.states[area.humidity_entity_id])
);
export const findClimateEntities = (
sources: EntityFilterSources,
hiddenEntities?: string[]
): string[] =>
findFilteredEntities(sources, climateEntityFilters, hiddenEntities);
if (hasAreaSensor) {
return true;
type ClimateAreaSensorKind = "temperature" | "humidity";
export const findClimateAreaSensors = (
sources: EntityFilterSources,
hiddenEntities?: string[]
): Map<string, ClimateAreaSensorKind> => {
const hidden = new Set(hiddenEntities);
const sensors = new Map<string, ClimateAreaSensorKind>();
for (const area of Object.values(sources.areas)) {
const slots = [
[area.temperature_entity_id, "temperature"],
[area.humidity_entity_id, "humidity"],
] as const;
for (const [entityId, kind] of slots) {
if (entityId && entityId in sources.states && !hidden.has(entityId)) {
sensors.set(entityId, kind);
}
}
}
const entityIds = Object.keys(hass.states);
return climateEntityFilters.some((filter) =>
entityIds.some(
generateEntityFilter(
hass.states,
hass.entities,
hass.devices,
hass.areas,
hass.floors,
filter
)
)
);
return sensors;
};
export const hasClimateEntities = (
hass: HomeAssistant,
hiddenEntities?: string[]
): boolean =>
findClimateAreaSensors(hass, hiddenEntities).size > 0 ||
findClimateEntities(hass, hiddenEntities).length > 0;
interface ClimateCardOptions {
entities: string[];
areaSensors: Map<string, ClimateAreaSensorKind>;
computeCard: ComputeEntityCard;
}
const processAreasForClimate = (
areaIds: string[],
hass: HomeAssistant,
entities: string[]
{ entities, areaSensors, computeCard }: ClimateCardOptions
): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, true);
for (const areaId of areaIds) {
const area = hass.areas[areaId];
@@ -96,25 +116,13 @@ const processAreasForClimate = (
// Add temperature and humidity sensors with trend graphs for areas
const temperatureEntityId = area.temperature_entity_id;
if (temperatureEntityId && hass.states[temperatureEntityId]) {
areaCards.push({
...computeTileCard(temperatureEntityId),
name:
hass.localize("component.sensor.entity_component.temperature.name") ||
"Temperature",
features: [{ type: "trend-graph" }],
});
if (temperatureEntityId && areaSensors.has(temperatureEntityId)) {
areaCards.push(computeCard(temperatureEntityId));
}
const humidityEntityId = area.humidity_entity_id;
if (humidityEntityId && hass.states[humidityEntityId]) {
areaCards.push({
...computeTileCard(humidityEntityId),
name:
hass.localize("component.sensor.entity_component.humidity.name") ||
"Humidity",
features: [{ type: "trend-graph" }],
});
if (humidityEntityId && areaSensors.has(humidityEntityId)) {
areaCards.push(computeCard(humidityEntityId));
}
// Add other climate entities
@@ -124,18 +132,7 @@ const processAreasForClimate = (
continue;
}
const state = hass.states[entityId];
if (
state?.attributes.device_class === "temperature" ||
state?.attributes.device_class === "humidity"
) {
areaCards.push({
...computeTileCard(entityId),
features: [{ type: "trend-graph" }],
});
} else {
areaCards.push(computeTileCard(entityId));
}
areaCards.push(computeCard(entityId));
}
if (areaCards.length > 0) {
@@ -159,7 +156,7 @@ const processAreasForClimate = (
const processUnassignedEntities = (
hass: HomeAssistant,
entities: string[]
{ entities, computeCard }: ClimateCardOptions
): LovelaceCardConfig[] => {
const unassignedFilter = generateEntityFilter(
hass.states,
@@ -173,10 +170,9 @@ const processUnassignedEntities = (
);
const unassignedEntities = entities.filter(unassignedFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, true);
for (const entityId of unassignedEntities) {
areaCards.push(computeTileCard(entityId));
areaCards.push(computeCard(entityId));
}
return areaCards;
@@ -185,7 +181,7 @@ const processUnassignedEntities = (
@customElement("climate-view-strategy")
export class ClimateViewStrategy extends ReactiveElement {
static async generate(
_config: ClimateViewStrategyConfig,
config: ClimateViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = Object.values(hass.areas);
@@ -194,20 +190,40 @@ export class ClimateViewStrategy extends ReactiveElement {
const sections: LovelaceSectionRawConfig[] = [];
const allEntities = Object.keys(hass.states);
const climateFilters = climateEntityFilters.map((filter) =>
generateEntityFilter(
hass.states,
hass.entities,
hass.devices,
hass.areas,
hass.floors,
filter
)
const hiddenEntities = new Set(config.hidden_entities);
const editVisibility = Boolean(config.edit_visibility);
const entities = findClimateEntities(
hass,
editVisibility ? undefined : config.hidden_entities
);
const entities = findEntities(allEntities, climateFilters);
const areaSensors = findClimateAreaSensors(
hass,
editVisibility ? undefined : config.hidden_entities
);
const computeTileCard = computeAreaTileCardConfig(hass, true);
const computeClimateCard = (entityId: string): LovelaceCardConfig => {
const tileCard = computeTileCard(entityId);
const areaSensor = areaSensors.get(entityId);
if (!areaSensor) {
return tileCard;
}
return {
...tileCard,
name:
hass.localize(
`component.sensor.entity_component.${areaSensor}.name`
) || undefined,
features: [{ type: "trend-graph" }],
};
};
const cardOptions: ClimateCardOptions = {
entities,
areaSensors,
computeCard: editVisibility
? wrapEntityVisibility(hiddenEntities, computeClimateCard)
: computeClimateCard,
};
const floorCount =
hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0);
@@ -233,7 +249,7 @@ export class ClimateViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForClimate(areaIds, hass, entities);
const areaCards = processAreasForClimate(areaIds, hass, cardOptions);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -257,7 +273,11 @@ export class ClimateViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForClimate(hierarchy.areas, hass, entities);
const areaCards = processAreasForClimate(
hierarchy.areas,
hass,
cardOptions
);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -266,7 +286,7 @@ export class ClimateViewStrategy extends ReactiveElement {
}
// Process unassigned entities
const unassignedCards = processUnassignedEntities(hass, entities);
const unassignedCards = processUnassignedEntities(hass, cardOptions);
if (unassignedCards.length > 0) {
const section: LovelaceSectionRawConfig = {
@@ -857,8 +857,6 @@ class HaConfigAreaPage extends LitElement {
},
this.hass.auth.data.hassUrl
)}
width="24"
height="24"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>`;
@@ -1063,7 +1061,7 @@ class HaConfigAreaPage extends LitElement {
justify-content: space-around;
}
.img-container img {
img {
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
@@ -1117,6 +1115,11 @@ class HaConfigAreaPage extends LitElement {
ha-list-item-button ha-icon-next {
color: var(--secondary-text-color);
}
img[slot="start"] {
width: 24px;
height: 24px;
object-fit: contain;
}
ha-card > *:first-child {
margin-top: -16px;
}
@@ -128,7 +128,6 @@ import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
import { scrollRowIntoView } from "../scroll-row-into-view";
import { AutomationPathController } from "../automation-path";
export interface ActionElement extends LitElement {
action: Action;
@@ -183,8 +182,6 @@ export default class HaAutomationActionRow extends LitElement {
@property({ type: Boolean }) public root = false;
@property({ attribute: false }) public index!: number;
@property({ type: Boolean }) public first?: boolean;
@property({ type: Boolean }) public last?: boolean;
@@ -197,11 +194,6 @@ export default class HaAutomationActionRow extends LitElement {
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
protected readonly pathController = new AutomationPathController(this, {
segment: () => [this.index],
row: () => (this.optionsInSidebar ? this : undefined),
});
@storage({
key: "automationClipboard",
state: false,
@@ -43,7 +43,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
return html`
<ha-automation-option
path-key="choose"
.options=${options}
.disabled=${this.disabled}
@value-changed=${this._optionsChanged}
@@ -46,7 +46,6 @@ export class HaIfAction extends LitElement implements ActionElement {
)}:
</h4>
<ha-automation-condition
path-key="if"
.conditions=${action.if ?? []}
.disabled=${this.disabled}
@value-changed=${this._ifChanged}
@@ -61,7 +60,6 @@ export class HaIfAction extends LitElement implements ActionElement {
)}:
</h4>
<ha-automation-action
path-key="then"
.actions=${action.then ?? []}
.disabled=${this.disabled}
@value-changed=${this._thenChanged}
@@ -75,7 +73,6 @@ export class HaIfAction extends LitElement implements ActionElement {
)}:
</h4>
<ha-automation-action
path-key="else"
.actions=${action.else || []}
.disabled=${this.disabled}
@value-changed=${this._elseChanged}
@@ -35,7 +35,6 @@ export class HaParallelAction extends LitElement implements ActionElement {
return html`
<ha-automation-action
path-key="parallel"
.actions=${action.parallel}
.narrow=${this.narrow}
.disabled=${this.disabled}
@@ -8,7 +8,6 @@ import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
import { AutomationPathController } from "../../automation-path";
import { isTemplate } from "../../../../../common/string/has-template";
import "../../../../../components/ha-form/ha-form";
@@ -41,11 +40,6 @@ export class HaRepeatAction extends LitElement implements ActionElement {
@property({ type: Boolean, attribute: "indent" }) public indent = false;
// The form edits the repeat options, so its fields are keyed below "repeat"
protected readonly pathController = new AutomationPathController(this, {
segment: () => ["repeat"],
});
@query("ha-form")
private _formElement?: HaForm;
@@ -35,7 +35,6 @@ export class HaSequenceAction extends LitElement implements ActionElement {
return html`
<ha-automation-action
path-key="sequence"
.actions=${action.sequence ?? []}
.narrow=${this.narrow}
.disabled=${this.disabled}
@@ -90,7 +90,6 @@ export class HaWaitForTriggerAction
${
this.indent || (!this.inSidebar && !this.indent)
? html`<ha-automation-trigger
path-key="wait_for_trigger"
class=${!this.inSidebar && !this.indent ? "expansion-panel" : ""}
.triggers=${ensureArray(this.action.wait_for_trigger)}
.hass=${this.hass}
@@ -1,167 +0,0 @@
import { ContextProvider, createContext } from "@lit/context";
import type { ReactiveElement } from "lit";
import {
consumeContext,
ContextController,
} from "../../../common/decorators/consume";
import {
pathEquals,
stringifyPath,
type ConfigPath,
} from "../../../common/util/config-path";
export const automationPathContext =
createContext<ConfigPath>("automation-path");
export interface AutomationPathRow extends HTMLElement {
readonly selected: boolean;
openSidebar(): void;
expand(): void;
}
const owner = (rows?: AutomationPathRow[]) => rows?.[rows.length - 1];
/**
* Rows that open in the sidebar, by path. Lets the editor find the row a
* config path belongs to, e.g. to show it or point at an error in it.
*/
export class AutomationRowRegistry {
// Rows claiming each path, the latest one owns it. While a row is dragged
// over another list it claims the path of a row there, which takes the path
// back once the dragged row leaves.
private _rows = new Map<
string,
{ path: ConfigPath; rows: AutomationPathRow[] }
>();
public register(path: ConfigPath, row: AutomationPathRow) {
const key = stringifyPath(path);
const entry = this._rows.get(key);
if (!entry) {
this._rows.set(key, { path, rows: [row] });
return;
}
entry.rows = [...entry.rows.filter((claimed) => claimed !== row), row];
}
public unregister(path: ConfigPath, row: AutomationPathRow) {
const key = stringifyPath(path);
const entry = this._rows.get(key);
if (!entry) {
return;
}
entry.rows = entry.rows.filter((claimed) => claimed !== row);
if (!entry.rows.length) {
this._rows.delete(key);
}
}
public pathOf(row: AutomationPathRow): ConfigPath | undefined {
return [...this._rows.values()].find((entry) => owner(entry.rows) === row)
?.path;
}
/** The rows containing `path`, outermost first. */
public rowsAlong(path: ConfigPath): AutomationPathRow[] {
return path
.map((_segment, index) =>
owner(this._rows.get(stringifyPath(path.slice(0, index + 1)))?.rows)
)
.filter((row): row is AutomationPathRow => row !== undefined);
}
}
export const automationRowRegistryContext =
createContext<AutomationRowRegistry>("automation-row-registry");
interface AutomationPathControllerOptions {
/** Path segments this element adds to the path of its ancestor. */
segment: () => ConfigPath;
/** The host while it is a row that opens in the sidebar. */
row?: () => AutomationPathRow | undefined;
}
/**
* Consumes the path of the closest ancestor and provides it extended by the
* host's own segment to its descendants.
*/
export class AutomationPathController extends ContextController {
public path: ConfigPath = [];
@consumeContext({ context: automationPathContext, subscribe: true })
private _parentPath: ConfigPath = [];
@consumeContext({ context: automationRowRegistryContext })
private _registry?: AutomationRowRegistry;
private _registeredPath?: ConfigPath;
private _registeredRow?: AutomationPathRow;
private readonly _provider: ContextProvider<typeof automationPathContext>;
constructor(
host: ReactiveElement,
private readonly _options: AutomationPathControllerOptions
) {
super(host);
this._provider = new ContextProvider(host, {
context: automationPathContext,
initialValue: this.path,
});
}
// The path is not rendered, so a changed ancestor path is passed on without
// updating the host. Only the host's own segment changes go through an update.
protected contextUpdated() {
this._sync();
}
public hostUpdate() {
this._sync();
}
public hostConnected() {
// A row moved by a drag reconnects without an update
this._sync();
}
private _sync() {
const path = [...this._parentPath, ...this._options.segment()];
if (!pathEquals(path, this.path)) {
this.path = path;
this._provider.setValue(path);
}
this._syncRegistration();
}
public hostDisconnected() {
this._unregister();
}
private _syncRegistration() {
if (!this._registry || !this._options.row) {
return;
}
const row = this.host.isConnected ? this._options.row() : undefined;
if (
this._registeredPath &&
(!row || !pathEquals(this._registeredPath, this.path))
) {
this._unregister();
}
if (row && !this._registeredPath) {
this._registry.register(this.path, row);
this._registeredPath = this.path;
this._registeredRow = row;
}
}
private _unregister() {
if (this._registeredPath && this._registeredRow) {
this._registry?.unregister(this._registeredPath, this._registeredRow);
}
this._registeredPath = undefined;
this._registeredRow = undefined;
}
}
@@ -96,7 +96,6 @@ import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
import { scrollRowIntoView } from "../scroll-row-into-view";
import { AutomationPathController } from "../automation-path";
export interface ConditionElement extends LitElement {
condition: Condition;
@@ -114,8 +113,6 @@ export default class HaAutomationConditionRow extends LitElement {
@property({ type: Boolean }) public root = false;
@property({ attribute: false }) public index!: number;
@property({ type: Boolean }) public first?: boolean;
@property({ type: Boolean }) public last?: boolean;
@@ -127,11 +124,6 @@ export default class HaAutomationConditionRow extends LitElement {
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
protected readonly pathController = new AutomationPathController(this, {
segment: () => [this.index],
row: () => (this.optionsInSidebar ? this : undefined),
});
@state() private _collapsed = true;
@state() private _isNew = false;
@@ -29,7 +29,6 @@ export abstract class HaLogicalCondition
protected render() {
return html`
<ha-automation-condition
path-key="conditions"
.conditions=${this.condition.conditions || []}
@value-changed=${this._valueChanged}
.hass=${this.hass}
@@ -1162,7 +1162,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
if (entityId) {
await updateEntityRegistryEntry(this.hass.callWS, entityId, {
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
automation: this.entityRegistryUpdate.category || null,
},
@@ -1267,7 +1267,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass.callWS, entityId, {
updateEntityRegistryEntry(this.hass, entityId, {
categories: { automation: category },
})
);
@@ -1306,7 +1306,7 @@ ${rejected
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass.callWS, entityId, {
updateEntityRegistryEntry(this.hass, entityId, {
labels:
action === "add"
? this.hass.entities[entityId].labels.concat(label)
@@ -1351,7 +1351,7 @@ ${rejected
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass.callWS, entityId, {
updateEntityRegistryEntry(this.hass, entityId, {
area_id: area,
})
);
@@ -7,7 +7,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { nextRender } from "../../../common/util/render-status";
import type { AutomationClipboard } from "../../../data/automation";
import type { Constructor, HomeAssistant } from "../../../types";
import { AutomationPathController } from "./automation-path";
const rowKeys = new WeakMap<object, string>();
@@ -76,13 +75,6 @@ export const AutomationSortableListMixin = <T extends object>(
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
/** Key of this list in the config of the row or editor rendering it. */
@property({ attribute: "path-key" }) public pathKey?: string;
protected readonly pathController = new AutomationPathController(this, {
segment: () => (this.pathKey === undefined ? [] : [this.pathKey]),
});
@state() protected rowSortSelected?: number;
@state()
@@ -1,4 +1,3 @@
import { provide } from "@lit/context";
import { mdiContentSave } from "@mdi/js";
import {
html,
@@ -34,10 +33,6 @@ import type {
ValueChangedEvent,
} from "../../../types";
import { showToast } from "../../../util/toast";
import {
AutomationRowRegistry,
automationRowRegistryContext,
} from "./automation-path";
import "./ha-automation-sidebar";
import type HaAutomationSidebar from "./ha-automation-sidebar";
@@ -87,9 +82,6 @@ export const ManualEditorMixin = <TConfig>(
@query("ha-automation-sidebar")
protected sidebarElement?: HaAutomationSidebar;
@provide({ context: automationRowRegistryContext })
protected rowRegistry = new AutomationRowRegistry();
declare protected collapsableElements?: NodeListOf<
Element & {
expandAll(): void;
@@ -111,7 +111,6 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
<ha-automation-trigger
role="region"
path-key="triggers"
aria-labelledby="triggers-heading"
.triggers=${this.config.triggers || []}
.highlightedTriggers=${this.pastedConfig?.triggers}
@@ -150,7 +149,6 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
<ha-automation-condition
role="region"
path-key="conditions"
aria-labelledby="conditions-heading"
.conditions=${this.config.conditions || []}
.highlightedConditions=${this.pastedConfig?.conditions}
@@ -185,7 +183,6 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
<ha-automation-action
role="region"
path-key="actions"
aria-labelledby="actions-heading"
.actions=${this.config.actions || []}
.highlightedActions=${this.pastedConfig?.actions}
@@ -64,7 +64,6 @@ import {
} from "../styles";
import "../trigger/ha-automation-trigger-references";
import { getUiSupportWarnings, isConfigObject } from "../ui-support";
import { AutomationPathController } from "../automation-path";
@customElement("ha-automation-option-row")
export default class HaAutomationOptionRow extends LitElement {
@@ -90,11 +89,6 @@ export default class HaAutomationOptionRow extends LitElement {
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
protected readonly pathController = new AutomationPathController(this, {
segment: () => (this.defaultActions ? ["default"] : [this.index]),
row: () => (this.optionsInSidebar ? this : undefined),
});
@state() private _expanded = false;
@state() private _selected = false;
@@ -454,7 +448,6 @@ export default class HaAutomationOptionRow extends LitElement {
)}:
</h4>
<ha-automation-condition
path-key="conditions"
.conditions=${ensureArray<string | Condition>(
this.option.conditions
)}
@@ -473,7 +466,6 @@ export default class HaAutomationOptionRow extends LitElement {
)}:
</h4>
<ha-automation-action
.pathKey=${this.option ? "sequence" : undefined}
.actions=${
(this.option
? ensureArray(this.option.sequence) || []
@@ -120,7 +120,6 @@ import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
import { scrollRowIntoView } from "../scroll-row-into-view";
import { AutomationPathController } from "../automation-path";
import { targetExpandGroupContext } from "../../../../components/target-picker/target-expand-group-context";
export interface TriggerElement extends LitElement {
@@ -165,7 +164,7 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ type: Boolean }) public last?: boolean;
@property({ attribute: false }) public index!: number;
@property({ type: Number }) public index?: number;
@property({ type: Boolean }) public highlight?: boolean;
@@ -175,11 +174,6 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
protected readonly pathController = new AutomationPathController(this, {
segment: () => [this.index],
row: () => (this.optionsInSidebar ? this : undefined),
});
@state() private _yamlMode = false;
@state() private _triggered = false;
@@ -26,7 +26,6 @@ export class HaTriggerList extends LitElement implements TriggerElement {
return html`
<ha-automation-trigger
path-key="triggers"
.triggers=${triggers}
.hass=${this.hass}
.disabled=${this.disabled}
@@ -216,7 +216,7 @@ class HaConfigBackupOverview extends LitElement {
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.backup.caption")}
.header=${this.hass.localize("ui.panel.config.backup.overview.header")}
>
<ha-dropdown
slot="toolbar-icon"
@@ -123,7 +123,7 @@ class DialogAssignCategory extends DirtyStateProviderMixin<AssignCategoryFormSta
this._error = undefined;
try {
await updateEntityRegistryEntry(
this.hass.callWS,
this.hass,
this._params!.entityReg.entity_id,
{
categories: { [this._scope!]: this._category || null },
@@ -17,8 +17,6 @@ import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
const ADDRESS_TYPES = ["mac", "bluetooth", "zigbee"];
@customElement("ha-device-info-card")
export class HaDeviceCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -236,13 +234,9 @@ export class HaDeviceCard extends LitElement {
}
protected _getAddresses() {
return this.device.connections
.filter((conn) => ADDRESS_TYPES.includes(conn[0]))
.sort(
(a, b) =>
ADDRESS_TYPES.indexOf(a[0]) - ADDRESS_TYPES.indexOf(b[0]) ||
stringCompare(a[1], b[1])
);
return this.device.connections.filter((conn) =>
["mac", "bluetooth", "zigbee"].includes(conn[0])
);
}
private _computeDeviceNameDisplay(deviceId: string) {
@@ -1600,11 +1600,7 @@ export class HaConfigDevicePage extends LitElement {
delete updates.disabled_by;
}
try {
await updateDeviceRegistryEntry(
this.hass.callWS,
this.deviceId,
updates
);
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
} catch (err: unknown) {
showAlertDialog(this, {
title: this.hass.localize(
@@ -1620,7 +1616,7 @@ export class HaConfigDevicePage extends LitElement {
};
private async _enableDevice(): Promise<void> {
await updateDeviceRegistryEntry(this.hass.callWS, this.deviceId, {
await updateDeviceRegistryEntry(this.hass, this.deviceId, {
disabled_by: null,
});
}
@@ -1167,7 +1167,7 @@ export class HaConfigDeviceDashboard extends LitElement {
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
promises.push(
updateDeviceRegistryEntry(this.hass.callWS, deviceId, {
updateDeviceRegistryEntry(this.hass, deviceId, {
area_id: area,
})
);
@@ -1219,7 +1219,7 @@ ${rejected
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
promises.push(
updateDeviceRegistryEntry(this.hass.callWS, deviceId, {
updateDeviceRegistryEntry(this.hass, deviceId, {
labels:
action === "add"
? this.hass.devices[deviceId].labels.concat(label)
@@ -114,7 +114,7 @@ export class DialogVacuumSegmentMapping
last_seen_segments: mapper.lastSeenSegments,
};
await updateEntityRegistryEntry(this.hass.callWS, this._params.entityId, {
await updateEntityRegistryEntry(this.hass, this._params.entityId, {
options_domain: "vacuum",
options: options,
});
@@ -100,6 +100,7 @@ export class EntitySettingsHelperTab extends LitElement {
: html`
<span @value-changed=${this._valueChanged}>
${dynamicElement(`ha-${this.entry.platform}-form`, {
hass: this.hass,
item: this._item,
entry: this.entry,
disabled: this._item === null,
@@ -1414,7 +1414,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
const result = await updateEntityRegistryEntry(
this.hass!.callWS,
this.hass!,
this.entry.entity_id,
params
);
@@ -1593,7 +1593,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
private async _restoreEntityId(): Promise<void> {
// use the unsaved name and area, as they are part of the entity ID
const entityIds = await getAutomaticEntityIds(
this.hass.callWS,
this.hass,
[this._origEntityId],
{
[this._origEntityId]: {
@@ -1821,11 +1821,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
showDeviceRegistryDetailDialog(this, {
device: this._device!,
updateEntry: async (updates) => {
await updateDeviceRegistryEntry(
this.hass.callWS,
this._device!.id,
updates
);
await updateDeviceRegistryEntry(this.hass, this._device!.id, updates);
},
});
}
@@ -179,7 +179,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
showDeviceRegistryDetailDialog(this, {
device: device,
updateEntry: async (updates) => {
await updateDeviceRegistryEntry(this.hass.callWS, device.id, updates);
await updateDeviceRegistryEntry(this.hass, device.id, updates);
},
});
}
@@ -189,7 +189,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._submitting = true;
try {
const result = await updateEntityRegistryEntry(
this.hass!.callWS,
this.hass!,
this.entry.entity_id,
{ disabled_by: null }
);
@@ -1388,7 +1388,7 @@ export class HaConfigEntities extends LitElement {
const result = await Promise.allSettled(
this._selected.map(async (entity) => {
const updateResult = await updateEntityRegistryEntry(
this.hass.callWS,
this.hass,
entity,
{
disabled_by: null,
@@ -1453,7 +1453,7 @@ export class HaConfigEntities extends LitElement {
dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => {
this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass.callWS, entity, {
updateEntityRegistryEntry(this.hass, entity, {
disabled_by: "user",
})
);
@@ -1475,7 +1475,7 @@ export class HaConfigEntities extends LitElement {
dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => {
this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass.callWS, entity, {
updateEntityRegistryEntry(this.hass, entity, {
hidden_by: "user",
})
);
@@ -1486,7 +1486,7 @@ export class HaConfigEntities extends LitElement {
private _unhideSelected = () => {
this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass.callWS, entity, {
updateEntityRegistryEntry(this.hass, entity, {
hidden_by: null,
})
);
@@ -1523,7 +1523,7 @@ export class HaConfigEntities extends LitElement {
return;
}
promises.push(
updateEntityRegistryEntry(this.hass.callWS, entityId, {
updateEntityRegistryEntry(this.hass, entityId, {
labels:
action === "add"
? entityReg.labels.concat(label)
@@ -1,13 +1,11 @@
import { mdiAlertOutline } from "@mdi/js";
import Fuse from "fuse.js";
import type { Connection, HassConfig } from "home-assistant-js-websocket";
import type { HassConfig } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, query, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { consume } from "../../../common/decorators/consume";
import { transform } from "../../../common/decorators/transform";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -31,12 +29,6 @@ import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import {
apiContext,
configContext,
connectionContext,
internationalizationContext,
} from "../../../data/context";
import { createCounter } from "../../../data/counter";
import {
fetchEntityRegistry,
@@ -66,13 +58,7 @@ import {
type FuseWeightedKey,
} from "../../../resources/fuseMultiTerm";
import { haStyleDialog } from "../../../resources/styles";
import type {
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantInternationalization,
ValueChangedEvent,
} from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type {
Helper,
HelperCategory,
@@ -91,7 +77,7 @@ type HelperCreators = Record<
HelperDomain,
{
create: (
callWS: HomeAssistantApi["callWS"],
hass: HomeAssistant,
// Not properly typed because there is currently a mismatch for this._item between:
// 1. Type passed to form should be Helper
// 2. Type received by creator should be MutableParams version
@@ -161,25 +147,7 @@ const HELPER_SEARCH_KEYS: FuseWeightedKey[] = [
export class DialogHelperDetail extends DirtyStateProviderMixin<
{ item?: HelperFormItem; area?: string } | undefined
>()(LitElement) {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: HomeAssistantInternationalization;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
@consume({ context: apiContext, subscribe: true })
private _api!: HomeAssistantApi;
@consume({ context: connectionContext, subscribe: true })
@transform<HomeAssistantConnection, Connection>({
transformer: ({ connection }) => connection,
})
private _connection!: Connection;
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _item?: HelperFormItem;
@@ -235,9 +203,9 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
}
this._open = true;
await this.updateComplete;
this._i18n.loadFragmentTranslation("config");
const flows = await getConfigFlowHandlers(this._api.callApi, "helper");
await this._i18n.loadBackendTranslation("title", flows, true);
this.hass.loadFragmentTranslation("config");
const flows = await getConfigFlowHandlers(this.hass, "helper");
await this.hass.loadBackendTranslation("title", flows, true);
// Ensure the titles are loaded before we render the flows.
this._helperFlows = flows;
@@ -246,13 +214,13 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
this._focusPicker();
}
protected updated(changedProps: PropertyValues): void {
protected updated(changedProps: PropertyValues<this>): void {
super.updated(changedProps);
const oldI18n = changedProps.get("_i18n") as
HomeAssistantInternationalization | undefined;
const oldHass = changedProps.get("hass");
if (
(oldI18n && oldI18n.localize !== this._i18n.localize) ||
changedProps.get("_hassConfig") !== undefined
oldHass &&
(oldHass.localize !== this.hass.localize ||
oldHass.config !== this.hass.config)
) {
this._comboBox?.refreshItems();
}
@@ -304,6 +272,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
<div class="form" @value-changed=${this._valueChanged}>
${this._error ? html`<div class="error">${this._error}</div>` : ""}
${dynamicElement(`ha-${this._domain}-form`, {
hass: this.hass,
item: this._item,
new: true,
autofocus: true,
@@ -326,7 +295,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
@click=${this._goBack}
.disabled=${this._submitting}
>
${this._i18n.localize("ui.common.back")}
${this.hass!.localize("ui.common.back")}
</ha-button>`
}
<ha-button
@@ -334,7 +303,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
@click=${this._createItem}
.disabled=${this._submitting}
>
${this._i18n.localize("ui.panel.config.helpers.dialog.create")}
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
</ha-button>
</ha-dialog-footer>
`;
@@ -348,16 +317,16 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
mode="dialog"
.shown=${this._shown}
no-sort
.label=${this._i18n.localize(
.label=${this.hass.localize(
"ui.panel.config.integrations.search_helper"
)}
.getItems=${this._getItems}
.rowRenderer=${this._renderHelperRow}
.sections=${this._getSections(
this._helperFlows,
this._i18n.localize,
this._hassConfig,
this._i18n.locale.language
this.hass.localize,
this.hass.config,
this.hass.locale.language
)}
.selectedSection=${this._category}
@index-selected=${this._helperPicked}
@@ -373,12 +342,12 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
.preventScrimClose=${this.isDirtyState}
header-title=${
this._domain
? this._i18n.localize(
? this.hass.localize(
"ui.panel.config.helpers.dialog.create_platform",
{
platform:
(isHelperDomain(this._domain) &&
this._i18n.localize(
this.hass.localize(
`ui.panel.config.helpers.types.${
this._domain as HelperDomain
}`
@@ -386,9 +355,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
this._domain,
}
)
: this._i18n.localize(
"ui.panel.config.helpers.dialog.create_helper"
)
: this.hass.localize("ui.panel.config.helpers.dialog.create_helper")
}
@after-show=${this._dialogShown}
@closed=${this._dialogClosed}
@@ -474,9 +441,9 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
this._search = search;
const allItems = this._getHelperItems(
this._helperFlows ?? [],
this._i18n.localize,
this._hassConfig,
this._i18n.locale.language
this.hass.localize,
this.hass.config,
this.hass.locale.language
);
const items = search
? multiTermSortedSearch(
@@ -496,7 +463,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
const categoryItems = items.filter((item) => item.category === category);
return categoryItems.length
? [
this._i18n.localize(
this.hass.localize(
`ui.panel.config.helpers.dialog.categories.${category}`
),
...categoryItems,
@@ -531,7 +498,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
@click=${stopPropagation}
></ha-svg-icon>
<ha-tooltip slot="end" .for="icon-${domain}">
${this._i18n.localize(
${this.hass.localize(
"ui.dialogs.helper_settings.platform_not_loaded",
{ platform: domain }
)}
@@ -556,10 +523,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
this._submitting = true;
this._error = "";
try {
const createdEntity = await HELPERS[domain].create(
this._api.callWS,
this._item
);
const createdEntity = await HELPERS[domain].create(this.hass, this._item);
// The dialog was closed or reopened while waiting
if (this._params !== params) {
return;
@@ -601,13 +565,13 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
try {
// The entity ID usually matches the helper ID, unless it was taken
const entry = await getExtendedEntityRegistryEntry(
this._api,
this.hass,
`${domain}.${uniqueId}`
).catch(() => undefined);
if (entry && isCreated(entry)) {
return entry.entity_id;
}
return (await fetchEntityRegistry(this._connection)).find(isCreated)
return (await fetchEntityRegistry(this.hass.connection)).find(isCreated)
?.entity_id;
} catch (_err) {
return undefined;
@@ -621,7 +585,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
let error = "Entity not found";
if (entityId) {
try {
return (await updateEntitiesArea(this._api.callWS, [entityId], areaId))[
return (await updateEntitiesArea(this.hass, [entityId], areaId))[
entityId
];
} catch (err: unknown) {
@@ -629,7 +593,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
}
}
showAlertDialog(this, {
text: this._i18n.localize(
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_entity",
{ error }
),
@@ -650,7 +614,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
const { id: domain, loaded } = ev.detail.item as HelperPickerItem;
if (!loaded) {
showAlertDialog(this, {
text: this._i18n.localize(
text: this.hass.localize(
"ui.dialogs.helper_settings.platform_not_loaded",
{ platform: domain }
),
@@ -673,7 +637,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
} else {
this._pendingConfigFlow = {
startFlowHandler: domain,
manifest: await fetchIntegrationManifest(this._api, domain),
manifest: await fetchIntegrationManifest(this.hass, domain),
dialogClosedCallback: this._params?.dialogClosedCallback,
navigateToResult: this._params?.navigateToResult,
};
@@ -1,9 +1,7 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-switch";
@@ -11,12 +9,11 @@ import type { HaSwitch } from "../../../../components/ha-switch";
import "../../../../components/input/ha-input";
import type { Counter } from "../../../../data/counter";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-counter-form")
class HaCounterForm extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public new = false;
@@ -66,7 +63,7 @@ class HaCounterForm extends LitElement {
}
protected render() {
if (!this._localize) {
if (!this.hass) {
return nothing;
}
@@ -76,10 +73,12 @@ class HaCounterForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
auto-validate
required
.validationMessage=${this._localize(
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -89,7 +88,9 @@ class HaCounterForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-input
@@ -97,7 +98,9 @@ class HaCounterForm extends LitElement {
.configValue=${"minimum"}
type="number"
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.counter.minimum")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.minimum"
)}
.disabled=${this.disabled}
></ha-input>
<ha-input
@@ -105,7 +108,9 @@ class HaCounterForm extends LitElement {
.configValue=${"maximum"}
type="number"
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.counter.maximum")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.maximum"
)}
.disabled=${this.disabled}
></ha-input>
<ha-input
@@ -113,11 +118,13 @@ class HaCounterForm extends LitElement {
.configValue=${"initial"}
type="number"
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.counter.initial")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.initial"
)}
.disabled=${this.disabled}
></ha-input>
<ha-expansion-panel
header=${this._localize(
header=${this.hass.localize(
"ui.dialogs.helper_settings.generic.more_options"
)}
outlined
@@ -127,7 +134,9 @@ class HaCounterForm extends LitElement {
.configValue=${"step"}
type="number"
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.counter.step")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.step"
)}
.disabled=${this.disabled}
></ha-input>
<div class="row">
@@ -139,7 +148,9 @@ class HaCounterForm extends LitElement {
>
</ha-switch>
<div>
${this._localize("ui.dialogs.helper_settings.counter.restore")}
${this.hass.localize(
"ui.dialogs.helper_settings.counter.restore"
)}
</div>
</div>
</ha-expansion-panel>
@@ -2,9 +2,7 @@ import { mdiInformationOutline } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-picker";
@@ -16,12 +14,11 @@ import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
import "../../../../components/radio/ha-radio-option";
import type { InputBoolean } from "../../../../data/input_boolean";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-input_boolean-form")
class HaInputBooleanForm extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public new = false;
@@ -55,7 +52,7 @@ class HaInputBooleanForm extends LitElement {
}
protected render() {
if (!this._localize) {
if (!this.hass) {
return nothing;
}
@@ -65,10 +62,12 @@ class HaInputBooleanForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
auto-validate
required
.validationMessage=${this._localize(
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -78,11 +77,13 @@ class HaInputBooleanForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-expansion-panel
header=${this._localize(
header=${this.hass.localize(
"ui.dialogs.helper_settings.generic.more_options"
)}
outlined
@@ -100,7 +101,7 @@ class HaInputBooleanForm extends LitElement {
@change=${this._initialChanged}
>
<span slot="label">
${this._localize(
${this.hass.localize(
"ui.dialogs.helper_settings.input_boolean.initial"
)}
<ha-svg-icon
@@ -112,23 +113,23 @@ class HaInputBooleanForm extends LitElement {
></ha-svg-icon>
</span>
<ha-radio-option value="restore">
${this._localize(
${this.hass.localize(
"ui.dialogs.helper_settings.input_boolean.restore"
)}
</ha-radio-option>
<ha-radio-option value="on">
${this._localize(
${this.hass.localize(
"ui.dialogs.helper_settings.input_boolean.turn_on"
)}
</ha-radio-option>
<ha-radio-option value="off">
${this._localize(
${this.hass.localize(
"ui.dialogs.helper_settings.input_boolean.turn_off"
)}
</ha-radio-option>
</ha-radio-group>
<ha-tooltip for="initial-note" placement="top">
${this._localize(
${this.hass.localize(
"ui.dialogs.helper_settings.input_boolean.initial_helper"
)}
</ha-tooltip>
@@ -1,19 +1,16 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-icon-picker";
import "../../../../components/input/ha-input";
import type { InputButton } from "../../../../data/input_button";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-input_button-form")
class HaInputButtonForm extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public new = false;
@@ -43,7 +40,7 @@ class HaInputButtonForm extends LitElement {
}
protected render() {
if (!this._localize) {
if (!this.hass) {
return nothing;
}
@@ -53,10 +50,12 @@ class HaInputButtonForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
auto-validate
required
.validationMessage=${this._localize(
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -66,7 +65,9 @@ class HaInputButtonForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
</div>
@@ -1,9 +1,7 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-icon-picker";
import "../../../../components/input/ha-input";
import "../../../../components/radio/ha-radio-group";
@@ -11,12 +9,11 @@ import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
import "../../../../components/radio/ha-radio-option";
import type { InputDateTime } from "../../../../data/input_datetime";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-input_datetime-form")
class HaInputDateTimeForm extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public new = false;
@@ -57,7 +54,7 @@ class HaInputDateTimeForm extends LitElement {
}
protected render() {
if (!this._localize) {
if (!this.hass) {
return nothing;
}
@@ -67,10 +64,12 @@ class HaInputDateTimeForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
auto-validate
required
.validationMessage=${this._localize(
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -80,11 +79,13 @@ class HaInputDateTimeForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-radio-group
.label=${this._localize(
.label=${this.hass.localize(
"ui.dialogs.helper_settings.input_datetime.mode"
)}
.value=${this._mode}
@@ -93,13 +94,17 @@ class HaInputDateTimeForm extends LitElement {
@change=${this._modeChanged}
>
<ha-radio-option value="date">
${this._localize("ui.dialogs.helper_settings.input_datetime.date")}
${this.hass.localize(
"ui.dialogs.helper_settings.input_datetime.date"
)}
</ha-radio-option>
<ha-radio-option value="time">
${this._localize("ui.dialogs.helper_settings.input_datetime.time")}
${this.hass.localize(
"ui.dialogs.helper_settings.input_datetime.time"
)}
</ha-radio-option>
<ha-radio-option value="datetime">
${this._localize(
${this.hass.localize(
"ui.dialogs.helper_settings.input_datetime.datetime"
)}
</ha-radio-option>
@@ -2,9 +2,7 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-device-class-picker";
import "../../../../components/ha-icon-picker";
import "../../../../components/radio/ha-radio-group";
@@ -15,7 +13,7 @@ import "../../../../components/input/ha-input";
import type { InputNumber } from "../../../../data/input_number";
import type { UnitOfMeasurementSelector } from "../../../../data/selector";
import { haStyle } from "../../../../resources/styles";
import type { ValueChangedEvent } from "../../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
const UNIT_SELECTOR: UnitOfMeasurementSelector = {
unit_of_measurement: {},
@@ -23,9 +21,7 @@ const UNIT_SELECTOR: UnitOfMeasurementSelector = {
@customElement("ha-input_number-form")
class HaInputNumberForm extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public new = false;
@@ -85,7 +81,7 @@ class HaInputNumberForm extends LitElement {
}
protected render() {
if (!this._localize) {
if (!this.hass) {
return nothing;
}
@@ -95,10 +91,12 @@ class HaInputNumberForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
auto-validate
required
.validationMessage=${this._localize(
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -108,7 +106,9 @@ class HaInputNumberForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-input
@@ -117,7 +117,7 @@ class HaInputNumberForm extends LitElement {
type="number"
step="any"
@input=${this._valueChanged}
.label=${this._localize(
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.min"
)}
.disabled=${this.disabled}
@@ -128,7 +128,7 @@ class HaInputNumberForm extends LitElement {
type="number"
step="any"
@input=${this._valueChanged}
.label=${this._localize(
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.max"
)}
.disabled=${this.disabled}
@@ -137,7 +137,7 @@ class HaInputNumberForm extends LitElement {
<ha-radio-group
orientation="horizontal"
class="mode"
.label=${this._localize(
.label=${this.hass.localize(
"ui.dialogs.helper_settings.input_number.mode"
)}
.value=${this._mode}
@@ -146,10 +146,12 @@ class HaInputNumberForm extends LitElement {
@change=${this._modeChanged}
>
<ha-radio-option value="slider">
${this._localize("ui.dialogs.helper_settings.input_number.slider")}
${this.hass.localize(
"ui.dialogs.helper_settings.input_number.slider"
)}
</ha-radio-option>
<ha-radio-option value="box">
${this._localize("ui.dialogs.helper_settings.input_number.box")}
${this.hass.localize("ui.dialogs.helper_settings.input_number.box")}
</ha-radio-option>
</ha-radio-group>
<ha-input
@@ -158,7 +160,7 @@ class HaInputNumberForm extends LitElement {
type="number"
step="any"
@input=${this._valueChanged}
.label=${this._localize(
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.step"
)}
.disabled=${this.disabled}
@@ -172,7 +174,7 @@ class HaInputNumberForm extends LitElement {
@value-changed=${this._pickerValueChanged}
></ha-device-class-picker>
<ha-selector-unit_of_measurement
.label=${this._localize(
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.unit_of_measurement"
)}
.selector=${UNIT_SELECTOR}
@@ -3,9 +3,7 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { afterNextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
@@ -19,12 +17,11 @@ import type { HaInput } from "../../../../components/input/ha-input";
import type { InputSelect } from "../../../../data/input_select";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-input_select-form")
class HaInputSelectForm extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public new = false;
@@ -96,7 +93,7 @@ class HaInputSelectForm extends LitElement {
}
protected render() {
if (!this._localize) {
if (!this.hass) {
return nothing;
}
@@ -106,11 +103,13 @@ class HaInputSelectForm extends LitElement {
dialogInitialFocus
auto-validate
required
.validationMessage=${this._localize(
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
.value=${this._name}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
.configValue=${"name"}
@input=${this._valueChanged}
.disabled=${this.disabled}
@@ -119,11 +118,15 @@ class HaInputSelectForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<div class="header">
${this._localize("ui.dialogs.helper_settings.input_select.options")}:
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.options"
)}:
</div>
<ha-sortable
@item-moved=${this._optionMoved}
@@ -132,7 +135,7 @@ class HaInputSelectForm extends LitElement {
>
<ha-list-base
class="options"
aria-label=${this._localize(
aria-label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.options"
)}
>
@@ -157,7 +160,7 @@ class HaInputSelectForm extends LitElement {
<ha-icon-button
slot="end"
.index=${index}
.label=${this._localize(
.label=${this.hass.localize(
"ui.dialogs.helper_settings.input_select.remove_option"
)}
@click=${this._removeOption}
@@ -175,7 +178,7 @@ class HaInputSelectForm extends LitElement {
<ha-input
class="flex-auto"
id="option_input"
.label=${this._localize(
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.add_option"
)}
@keydown=${this._handleKeyAdd}
@@ -186,7 +189,9 @@ class HaInputSelectForm extends LitElement {
appearance="filled"
@click=${this._addOption}
.disabled=${this.disabled}
>${this._localize("ui.dialogs.helper_settings.input_select.add")}
>${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.add"
)}
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
</ha-button>
</div>
@@ -217,10 +222,10 @@ class HaInputSelectForm extends LitElement {
const index = (ev.target as any).index;
if (
!(await showConfirmationDialog(this, {
title: this._localize(
title: this.hass.localize(
"ui.dialogs.helper_settings.input_select.confirm_delete.delete"
),
text: this._localize(
text: this.hass.localize(
"ui.dialogs.helper_settings.input_select.confirm_delete.prompt"
),
destructive: true,
@@ -1,9 +1,7 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-icon-picker";
@@ -13,12 +11,11 @@ import "../../../../components/radio/ha-radio-option";
import "../../../../components/input/ha-input";
import type { InputText } from "../../../../data/input_text";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-input_text-form")
class HaInputTextForm extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public new = false;
@@ -63,7 +60,7 @@ class HaInputTextForm extends LitElement {
}
protected render() {
if (!this._localize) {
if (!this.hass) {
return nothing;
}
@@ -73,10 +70,12 @@ class HaInputTextForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
auto-validate
required
.validationMessage=${this._localize(
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -86,11 +85,13 @@ class HaInputTextForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-expansion-panel
header=${this._localize(
header=${this.hass.localize(
"ui.dialogs.helper_settings.generic.more_options"
)}
outlined
@@ -102,7 +103,7 @@ class HaInputTextForm extends LitElement {
min="0"
max="255"
@input=${this._valueChanged}
.label=${this._localize(
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.min"
)}
.disabled=${this.disabled}
@@ -114,13 +115,13 @@ class HaInputTextForm extends LitElement {
max="255"
type="number"
@input=${this._valueChanged}
.label=${this._localize(
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.max"
)}
></ha-input>
<ha-radio-group
orientation="horizontal"
.label=${this._localize(
.label=${this.hass.localize(
"ui.dialogs.helper_settings.input_text.mode"
)}
.value=${this._mode}
@@ -129,10 +130,12 @@ class HaInputTextForm extends LitElement {
@change=${this._modeChanged}
>
<ha-radio-option value="text">
${this._localize("ui.dialogs.helper_settings.input_text.text")}
${this.hass.localize(
"ui.dialogs.helper_settings.input_text.text"
)}
</ha-radio-option>
<ha-radio-option value="password">
${this._localize(
${this.hass.localize(
"ui.dialogs.helper_settings.input_text.password"
)}
</ha-radio-option>
@@ -141,10 +144,10 @@ class HaInputTextForm extends LitElement {
.value=${this._pattern || ""}
.configValue=${"pattern"}
@input=${this._valueChanged}
.label=${this._localize(
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.pattern_label"
)}
.hint=${this._localize(
.hint=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.pattern_helper"
)}
.disabled=${this.disabled}
@@ -9,7 +9,6 @@ import allLocales from "fullcalendar/locales-all";
import interactionPlugin from "fullcalendar/interaction";
import timeGridPlugin from "fullcalendar/timegrid";
import type { Day } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import { addDays, isSameDay, isSameWeek, nextDay } from "date-fns";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -17,15 +16,9 @@ import { customElement, property, query, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../../../common/datetime/first_weekday";
import { formatTime24h } from "../../../../common/datetime/format_time";
import { useAmPm } from "../../../../common/datetime/use_am_pm";
import { consume } from "../../../../common/decorators/consume";
import { transform } from "../../../../common/decorators/transform";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-picker";
import "../../../../components/input/ha-input";
import {
configContext,
internationalizationContext,
} from "../../../../data/context";
import type { Schedule, ScheduleDay } from "../../../../data/schedule";
import { weekdays } from "../../../../data/schedule";
import { TimeZone } from "../../../../data/translation";
@@ -34,10 +27,7 @@ import {
fullCalendarTheme,
} from "../../../../resources/fullcalendar-theme";
import { haStyle } from "../../../../resources/styles";
import type {
HomeAssistantConfig,
HomeAssistantInternationalization,
} from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
const defaultFullCalendarConfig: CalendarOptions = {
@@ -62,15 +52,7 @@ const defaultFullCalendarConfig: CalendarOptions = {
@customElement("ha-schedule-form")
class HaScheduleForm extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: HomeAssistantInternationalization;
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public new = false;
@@ -143,7 +125,7 @@ class HaScheduleForm extends LitElement {
}
protected render() {
if (!this._i18n) {
if (!this.hass) {
return nothing;
}
@@ -153,12 +135,12 @@ class HaScheduleForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this._i18n.localize(
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
auto-validate
required
.validationMessage=${this._i18n.localize(
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -168,7 +150,7 @@ class HaScheduleForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this._i18n.localize(
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
@@ -198,11 +180,10 @@ class HaScheduleForm extends LitElement {
this.calendar.addEventSource(this._events);
}
const oldI18n = changedProps.get("_i18n") as
HomeAssistantInternationalization | undefined;
const oldHass = changedProps.get("hass") as HomeAssistant;
if (oldI18n && oldI18n.language !== this._i18n.language) {
this.calendar.setOption("locale", this._i18n.language);
if (oldHass && oldHass.language !== this.hass.language) {
this.calendar.setOption("locale", this.hass.language);
}
}
@@ -215,19 +196,19 @@ class HaScheduleForm extends LitElement {
private _setupCalendar(): void {
const config: CalendarOptions = {
...defaultFullCalendarConfig,
locale: this._i18n.language,
firstDay: firstWeekdayIndex(this._i18n.locale),
locale: this.hass.language,
firstDay: firstWeekdayIndex(this.hass.locale),
slotHeaderFormat: {
hour: "numeric",
minute: undefined,
hour12: useAmPm(this._i18n.locale),
meridiem: useAmPm(this._i18n.locale) ? "narrow" : false,
hour12: useAmPm(this.hass.locale),
meridiem: useAmPm(this.hass.locale) ? "narrow" : false,
},
eventTimeFormat: {
hour: useAmPm(this._i18n.locale) ? "numeric" : "2-digit",
minute: useAmPm(this._i18n.locale) ? "numeric" : "2-digit",
hour12: useAmPm(this._i18n.locale),
meridiem: useAmPm(this._i18n.locale) ? "narrow" : false,
hour: useAmPm(this.hass.locale) ? "numeric" : "2-digit",
minute: useAmPm(this.hass.locale) ? "numeric" : "2-digit",
hour12: useAmPm(this.hass.locale),
meridiem: useAmPm(this.hass.locale) ? "narrow" : false,
},
};
@@ -256,7 +237,7 @@ class HaScheduleForm extends LitElement {
let date = nextDay(new Date(), i as Day);
if (
!isSameWeek(date, new Date(), {
weekStartsOn: firstWeekdayIndex(this._i18n.locale),
weekStartsOn: firstWeekdayIndex(this.hass.locale),
})
) {
date = addDays(date, -7);
@@ -295,14 +276,14 @@ class HaScheduleForm extends LitElement {
// Schedule is timezone unaware, we need to format it in local time
const endFormatted = formatTime24h(
end,
{ ...this._i18n.locale, time_zone: TimeZone.local },
this._hassConfig
{ ...this.hass.locale, time_zone: TimeZone.local },
this.hass.config
);
value.push({
from: formatTime24h(
start,
{ ...this._i18n.locale, time_zone: TimeZone.local },
this._hassConfig
{ ...this.hass.locale, time_zone: TimeZone.local },
this.hass.config
),
to:
!isSameDay(start, end) || endFormatted === "0:00"
@@ -328,11 +309,7 @@ class HaScheduleForm extends LitElement {
const value = this[`_${day}`][parseInt(index)];
const newValue = { ...this._item };
const endFormatted = formatTime24h(
end,
this._i18n.locale,
this._hassConfig
);
const endFormatted = formatTime24h(end, this.hass.locale, this.hass.config);
newValue[day] = [...newValue[day]];
newValue[day][index] = {
...newValue[day][index],
@@ -360,14 +337,10 @@ class HaScheduleForm extends LitElement {
const newDay = weekdays[start.getDay()];
const newValue = { ...this._item };
const endFormatted = formatTime24h(
end,
this._i18n.locale,
this._hassConfig
);
const endFormatted = formatTime24h(end, this.hass.locale, this.hass.config);
const event = {
...newValue[day][index],
from: formatTime24h(start, this._i18n.locale, this._hassConfig),
from: formatTime24h(start, this.hass.locale, this.hass.config),
to:
!isSameDay(start, end) || endFormatted === "0:00"
? "24:00"
@@ -2,9 +2,7 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { createDurationData } from "../../../../common/datetime/create_duration_data";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-checkbox";
import "../../../../components/ha-duration-input";
import type { HaDurationData } from "../../../../components/ha-duration-input";
@@ -13,12 +11,11 @@ import "../../../../components/input/ha-input";
import type { ForDict } from "../../../../data/automation";
import type { DurationDict, Timer } from "../../../../data/timer";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-timer-form")
class HaTimerForm extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public new = false;
@@ -60,7 +57,7 @@ class HaTimerForm extends LitElement {
}
protected render() {
if (!this._localize) {
if (!this.hass) {
return nothing;
}
@@ -70,10 +67,12 @@ class HaTimerForm extends LitElement {
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.name")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
auto-validate
required
.validationMessage=${this._localize(
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
@@ -83,7 +82,9 @@ class HaTimerForm extends LitElement {
.value=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
.label=${this._localize("ui.dialogs.helper_settings.generic.icon")}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-duration-input
@@ -98,7 +99,7 @@ class HaTimerForm extends LitElement {
@change=${this._toggleRestore}
.disabled=${this.disabled}
>
${this._localize("ui.dialogs.helper_settings.timer.restore")}
${this.hass.localize("ui.dialogs.helper_settings.timer.restore")}
</ha-checkbox>
</div>
`;
@@ -747,7 +747,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
>
<ha-filter-floor-areas
.type=${"entity"}
include-disabled-entities
.value=${this._filters["ha-filter-floor-areas"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -1163,7 +1162,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass.callWS, entityId, {
updateEntityRegistryEntry(this.hass, entityId, {
categories: { helpers: category },
})
);
@@ -1201,7 +1200,7 @@ ${rejected
this._selected.forEach((entityId) => {
const labels = this._labelsForEntity(entityId);
promises.push(
updateEntityRegistryEntry(this.hass.callWS, entityId, {
updateEntityRegistryEntry(this.hass, entityId, {
labels:
action === "add"
? labels.concat(label)
@@ -1307,11 +1306,11 @@ ${rejected
});
return;
}
const handlers = await getConfigFlowHandlers(this.hass.callApi, "helper");
const handlers = await getConfigFlowHandlers(this.hass, "helper");
if (!handlers.includes(domain)) {
const integrations = await getConfigFlowHandlers(
this.hass.callApi,
this.hass,
"integration"
);
if (integrations.includes(domain)) {
@@ -1,393 +0,0 @@
import {
mdiAlertCircle,
mdiCheckCircle,
mdiInformation,
mdiAlert,
} from "@mdi/js";
import type { ContextType } from "@lit/context";
import type { HassEntities } from "home-assistant-js-websocket";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consume } from "../../../common/decorators/consume";
import { consumeEntityStates } from "../../../common/decorators/consume-context-entry";
import { transform } from "../../../common/decorators/transform";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/item/ha-list-item-base";
import "../../../components/list/ha-list-base";
import type { ConfigEntry } from "../../../data/config_entries";
import { fetchConfigFlowInProgress } from "../../../data/config_flow";
import {
apiContext,
configEntriesContext,
connectionContext,
internationalizationContext,
} from "../../../data/context";
import type {
ConfigEntryHealth,
HealthStatus,
} from "../../../data/config_entry_health";
import { computeConfigEntryHealth } from "../../../data/config_entry_health";
import {
domainToName,
fetchIntegrationSetups,
} from "../../../data/integration";
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
import type { IntegrationSetup } from "../../../data/integration";
import type { RepairsIssue } from "../../../data/repairs";
import { fetchRepairsIssues } from "../../../data/repairs";
import type { LoggedError } from "../../../data/system_log";
import { fetchSystemLog } from "../../../data/system_log";
import { DialogMixin } from "../../../dialogs/dialog-mixin";
import { haStyleDialog } from "../../../resources/styles";
import { renderConfigEntryError } from "./ha-config-integration-page";
import type { ConfigEntryHealthDialogParams } from "./show-dialog-config-entry-health";
const STATUS_ICONS: Record<HealthStatus, string> = {
ok: mdiCheckCircle,
warning: mdiAlert,
error: mdiAlertCircle,
info: mdiInformation,
};
const MAX_LISTED = 5;
interface HealthData {
issues: RepairsIssue[];
log: LoggedError[];
flows: DataEntryFlowProgress[];
setups: IntegrationSetup[];
}
@customElement("dialog-config-entry-health")
class DialogConfigEntryHealth extends DialogMixin<ConfigEntryHealthDialogParams>(
LitElement
) {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: connectionContext, subscribe: true })
private _connection!: ContextType<typeof connectionContext>;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
// Read by the @consumeEntityStates decorator below.
@state() protected _entityIds: string[] = [];
// Only this entry's entities, so availability stays live while open.
@state()
@consumeEntityStates({ entityIdPath: ["_entityIds"] })
private _states: HassEntities = {};
// params.entry is a snapshot; updates replace the entry object.
@state()
@consume({ context: configEntriesContext, subscribe: true })
@transform<ConfigEntry[], ConfigEntry | undefined>({
transformer: function (this: DialogConfigEntryHealth, entries) {
return entries?.find(
(entry) => entry.entry_id === this.params?.entry.entry_id
);
},
})
private _entry?: ConfigEntry;
@state() private _data?: HealthData;
@state() private _error?: string;
private _computeHealth = memoizeOne(
(
entry: ConfigEntry,
entities: ConfigEntryHealthDialogParams["entities"],
loggers: string[] | undefined,
states: HassEntities,
data: HealthData
) =>
computeConfigEntryHealth(
entry,
entities,
states,
data.issues,
data.log,
data.flows,
data.setups,
loggers
)
);
connectedCallback() {
super.connectedCallback();
this._entityIds = this.params!.entities.map((entity) => entity.entity_id);
this._load();
}
private async _load(): Promise<void> {
try {
const [{ issues }, log, flows, setups] = await Promise.all([
fetchRepairsIssues(this._connection.connection),
fetchSystemLog(this._api.callWS),
fetchConfigFlowInProgress(this._connection.connection),
fetchIntegrationSetups(this._api.callWS),
]);
this._data = { issues, log, flows, setups };
} catch (err: any) {
// WebSocket errors nest their message.
this._error =
err?.error?.message ||
err?.message ||
this._i18n.localize(
"ui.panel.config.integrations.config_entry.health.load_failed"
);
}
}
protected render() {
if (!this.params) {
return nothing;
}
return html`
<ha-dialog
open
header-title=${this._i18n.localize(
"ui.panel.config.integrations.config_entry.health.title",
{ title: this._currentEntry.title }
)}
>
${
this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: this._data
? this._renderHealth(
this._computeHealth(
this._currentEntry,
this.params.entities,
this.params.manifest?.loggers,
this._states,
this._data
)
)
: html`<ha-spinner></ha-spinner>`
}
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${this._i18n.localize("ui.common.close")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
`;
}
private get _currentEntry(): ConfigEntry {
return this._entry ?? this.params!.entry;
}
private _renderHealth(health: ConfigEntryHealth) {
const entry = this._currentEntry;
const key = "ui.panel.config.integrations.config_entry.health";
// Repairs and log lines are only known per integration, not per entry.
const integration = domainToName(this._i18n.localize, entry.domain);
const entityName = (entityId: string) =>
this._states[entityId]?.attributes.friendly_name ?? entityId;
const more = (count: number) =>
count > MAX_LISTED
? html`<li>
${this._i18n.localize(`${key}.and_more`, {
count: count - MAX_LISTED,
})}
</li>`
: nothing;
return html`
<ha-alert
.alertType=${
health.status === "ok"
? "success"
: health.status === "error"
? "error"
: "warning"
}
>
${this._i18n.localize(`${key}.summary.${health.status}`)}
</ha-alert>
<ha-list-base>
${this._row(
health.state,
this._i18n.localize(
entry.disabled_by && entry.state !== "failed_unload"
? `${key}.disabled`
: `ui.panel.config.integrations.config_entry.state.${entry.state}`
),
entry.state === "failed_unload"
? html`<p>
${this._i18n.localize(
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
)}
</p>`
: health.state === "warning" || health.state === "error"
? html`<p>${renderConfigEntryError(this._i18n, entry)}</p>`
: nothing
)}
${
health.flows.flows.length
? this._row(
health.flows.status,
this._i18n.localize(`${key}.flows`),
html`<a href="/config/integrations/dashboard">
${this._i18n.localize(`${key}.flows_link`)}
</a>`
)
: nothing
}
${
health.entities.total
? this._row(
health.entities.status,
this._i18n.localize(`${key}.entities`, {
available:
health.entities.total - health.entities.unavailable.length,
total: health.entities.total,
}),
health.entities.unavailable.length
? html`<ul>
${health.entities.unavailable
.slice(0, MAX_LISTED)
.map(
(item) =>
html`<li>${entityName(item.entity_id)}</li>`
)}
${more(health.entities.unavailable.length)}
</ul>
<a
href=${`/config/entities?historyBack=1&config_entry=${entry.entry_id}`}
>
${this._i18n.localize(`${key}.entities_link`)}
</a>`
: nothing
)
: nothing
}
${this._row(
health.repairs.status,
this._i18n.localize(`${key}.repairs`, {
count: health.repairs.issues.length,
integration,
}),
health.repairs.issues.length
? html`<a href="/config/repairs">
${this._i18n.localize(`${key}.repairs_link`)}
</a>`
: nothing
)}
${this._row(
health.logs.status,
this._i18n.localize(`${key}.logs`, {
errors: health.logs.errors,
warnings: health.logs.warnings,
integration,
}),
health.logs.errors || health.logs.warnings
? html`<a
href=${`/config/logs?integration=${encodeURIComponent(entry.domain)}`}
>
${this._i18n.localize(`${key}.logs_link`)}
</a>`
: nothing
)}
${
health.setupSeconds !== undefined
? this._row(
"info",
health.setupSeconds < 0.1
? this._i18n.localize(`${key}.setup_time_fast`, {
integration,
})
: this._i18n.localize(`${key}.setup_time`, {
integration,
seconds: health.setupSeconds.toFixed(1),
})
)
: nothing
}
</ha-list-base>
`;
}
private _row(
status: HealthStatus,
text: string,
details: TemplateResult | typeof nothing = nothing
) {
return html`<ha-list-item-base>
<ha-svg-icon
slot="start"
class=${status}
.path=${STATUS_ICONS[status]}
></ha-svg-icon>
<span slot="headline">${text}</span>
${
details === nothing
? nothing
: html`<div slot="supporting-text">${details}</div>`
}
</ha-list-item-base>`;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-spinner {
display: block;
margin: var(--ha-space-6) auto;
}
ha-list-base {
margin-top: var(--ha-space-2);
}
/* Row items keep text on one line; errors and counts need to wrap. */
[slot="headline"],
[slot="supporting-text"] {
white-space: normal;
}
[slot="supporting-text"] p,
[slot="supporting-text"] ul {
margin: 0;
}
[slot="supporting-text"] ul {
padding-inline-start: var(--ha-space-5);
}
a {
color: var(--primary-color);
}
ha-svg-icon {
flex-shrink: 0;
}
ha-svg-icon.ok {
color: var(--success-color);
}
ha-svg-icon.warning {
color: var(--warning-color);
}
ha-svg-icon.error {
color: var(--error-color);
}
ha-svg-icon.info {
color: var(--secondary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-config-entry-health": DialogConfigEntryHealth;
}
}
@@ -255,11 +255,7 @@ class HaConfigEntryDeviceRow extends LitElement {
showDeviceRegistryDetailDialog(this, {
device: this.device,
updateEntry: async (updates) => {
await updateDeviceRegistryEntry(
this.hass.callWS,
this.device.id,
updates
);
await updateDeviceRegistryEntry(this.hass, this.device.id, updates);
},
});
};
@@ -343,7 +339,7 @@ class HaConfigEntryDeviceRow extends LitElement {
}
}
await updateDeviceRegistryEntry(this.hass.callWS, this.device.id, {
await updateDeviceRegistryEntry(this.hass, this.device.id, {
disabled_by: disable ? "user" : null,
});
};
@@ -8,7 +8,6 @@ import {
mdiDotsVertical,
mdiDownload,
mdiHandExtendedOutline,
mdiHeartPulse,
mdiPlayCircleOutline,
mdiPlus,
mdiProgressHelper,
@@ -49,7 +48,6 @@ import {
updateConfigEntry,
} from "../../../data/config_entries";
import { groupDevicesByParent } from "../../../data/device/device_registry";
import { showConfigEntryHealthDialog } from "./show-dialog-config-entry-health";
import type { DiagnosticInfo } from "../../../data/diagnostics";
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
@@ -337,12 +335,6 @@ export class HaConfigEntryRow extends LitElement {
`
: nothing
}
<ha-dropdown-item value="health">
<ha-svg-icon slot="icon" .path=${mdiHeartPulse}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.health.menu"
)}
</ha-dropdown-item>
${
!item.disabled_by &&
RECOVERABLE_STATES.includes(item.state) &&
@@ -567,14 +559,6 @@ export class HaConfigEntryRow extends LitElement {
this._handleReload();
return;
}
if (value === "health") {
showConfigEntryHealthDialog(this, {
entry: this.data.entry,
entities: this._getEntities(),
manifest: this.manifest,
});
return;
}
if (value === "rename") {
this._handleRename();
return;
@@ -21,17 +21,13 @@ import memoizeOne from "memoize-one";
import { ensureArray } from "../../../common/array/ensure-array";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
import { ContextSubscriptionController } from "../../../common/decorators/consume";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import {
caseInsensitiveStringCompare,
stringCompare,
} from "../../../common/string/compare";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
integrationQueryParamConfig,
@@ -43,9 +39,7 @@ import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../components/input/ha-input-search";
import "../../../components/ha-icon-next";
import "../../../components/item/ha-list-item-base";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-base";
import { getSignedPath } from "../../../data/auth";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
@@ -65,9 +59,7 @@ import {
getCoreLogFileDownloadUnavailableReason,
getErrorLogDownloadUrl,
} from "../../../data/error_log";
import { manifestsContext } from "../../../data/context";
import type {
DomainManifestLookup,
IntegrationLogInfo,
IntegrationManifest,
} from "../../../data/integration";
@@ -117,7 +109,7 @@ export interface ConfigEntryData {
const AI_SETTINGS_DOMAINS = ["mcp", "mcp_server"];
export const renderConfigEntryError = (
hass: Pick<HomeAssistant, "localize" | "loadBackendTranslation">,
hass: HomeAssistant,
entry: ConfigEntry
): TemplateResult => {
if (entry.reason) {
@@ -175,11 +167,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
@state() private _manifest?: IntegrationManifest;
/** The manifests of all loaded integrations, to find the dependents. */
@state() private _loadedManifests?: DomainManifestLookup;
private _manifestsConsumer?: ContextSubscriptionController<DomainManifestLookup>;
@state() private _extraConfigEntries?: ConfigEntry[];
@state() private _diagnosticHandler?: DiagnosticInfo;
@@ -276,7 +263,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
this.hass.loadBackendTranslation("config", [this.domain]);
this.hass.loadBackendTranslation("config_subentries", [this.domain]);
this._extraConfigEntries = undefined;
this._manifest = undefined;
this._resetDebugLogReady();
this._filter = "";
this._subEntries = {};
@@ -429,25 +415,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
this.domain
);
// Without the dependents, a dependency looks like a YAML integration.
const dependents =
this._manifest && !this._manifest.config_flow
? this._getDependents(
this.domain,
this._loadedManifests,
this.hass.localize,
this.hass.locale.language
)
: [];
const showDependents = normalEntries.length === 0 && dependents.length > 0;
// Hold back the YAML texts and "Add entry" until the manifest and the
// dependents are known. Entries do not wait, so a failed request cannot
// hide them.
const dependentsLoading =
normalEntries.length === 0 &&
(!this._manifest ||
(!this._manifest.config_flow && !this._loadedManifests));
const configPanel = isComponentLoaded(this.hass.config, this.domain)
? getConfigPanelPath(this.domain, this.hass.panels)
: undefined;
@@ -458,8 +425,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
const showAddEntry =
this._manifest?.integration_type !== "hardware" &&
!showDependents &&
!dependentsLoading &&
!setUpInPanel &&
(!this._manifest?.single_config_entry ||
(normalData.length === 0 && attentionData.length === 0));
@@ -649,8 +614,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
${
normalEntries.length === 0 &&
!showDependents &&
!dependentsLoading &&
!setUpInPanel &&
this._manifest &&
!this._manifest.config_flow &&
@@ -763,9 +726,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
${
configPanel
? html`<ha-button
.appearance=${
showAddEntry || canAddDevice ? "outlined" : "accent"
}
.appearance=${setUpInPanel ? "accent" : "outlined"}
href=${`/${configPanel}`}
>
${this.hass.localize(
@@ -1014,21 +975,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
: nothing
}
${
showDependents
? html`<div class="section">
<h3 class="section-header">
${this.hass.localize(
"ui.panel.config.integrations.integration_page.used_by"
)}
</h3>
${this._renderDependents(dependents)}
</div>`
: nothing
}
${
showDependents ||
dependentsLoading ||
(setUpInPanel && normalEntries.length === 0)
setUpInPanel && normalEntries.length === 0
? nothing
: html`<div class="section">
<h3 class="section-header">
@@ -1085,76 +1032,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
`;
}
// Only an integration without a config flow needs the loaded manifests, so
// other integration pages do not make the context fetch them.
private _watchLoadedManifests(): void {
if (this._manifestsConsumer) {
return;
}
this._manifestsConsumer = new ContextSubscriptionController(
this,
manifestsContext,
(manifests) => {
this._loadedManifests = manifests;
}
);
}
private _getDependents = memoizeOne(
(
domain: string,
manifests: DomainManifestLookup | undefined,
localize: LocalizeFunc,
language: string
) =>
Object.values(manifests ?? {})
.filter(
// System integrations, like Default Configuration, are not useful to list.
(manifest) =>
manifest.integration_type !== "system" &&
manifest.dependencies?.includes(domain)
)
// The manifests come in no fixed order.
.sort((a, b) =>
stringCompare(
domainToName(localize, a.domain, a),
domainToName(localize, b.domain, b),
language
)
)
);
private _renderDependents(dependents: IntegrationManifest[]) {
return html`<ha-list-base>
${dependents.map(
(manifest) =>
html`<ha-list-item-button
class="dependent"
href=${`/config/integrations/integration/${manifest.domain}`}
>
<img
slot="start"
alt=""
src=${brandsUrl(
{
domain: manifest.domain,
type: "icon@2x",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<div slot="headline">
${domainToName(this.hass.localize, manifest.domain, manifest)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>`
)}
</ha-list-base>`;
}
private _onImageLoad(ev) {
ev.target.style.display = "inline-block";
}
@@ -1195,9 +1072,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
return;
}
this._manifest = await fetchIntegrationManifest(this.hass, this.domain);
if (!this._manifest.config_flow) {
this._watchLoadedManifests();
}
if (
this._manifest.integration_type &&
!["device", "hub", "service"].includes(this._manifest.integration_type)
@@ -1874,17 +1748,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
ha-list-item-base.discovered {
--ha-row-item-min-height: 72px;
}
ha-list-item-button.dependent {
--ha-row-item-min-height: 72px;
}
ha-list-item-button.dependent + ha-list-item-button.dependent {
border-top: var(--ha-border-width-sm) solid var(--divider-color);
}
ha-list-item-button.dependent img {
width: 40px;
height: 40px;
object-fit: contain;
}
ha-list-item-base.config_entry {
position: relative;
}
@@ -243,14 +243,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
] => {
const entryDomains = new Set(configEntries.map((entry) => entry.domain));
// An integration loaded as a dependency of another is not set up in YAML.
const dependencies = new Set<string>();
for (const component of components) {
manifests[component.split(".")[0]]?.dependencies?.forEach((domain) =>
dependencies.add(domain)
);
}
const domains = new Set<string>();
for (const component of components) {
@@ -258,7 +250,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
const manifest = manifests[componentDomain];
if (
!entryDomains.has(componentDomain) &&
!dependencies.has(componentDomain) &&
manifest &&
!manifest.config_flow &&
(!manifest.integration_type ||
@@ -33,7 +33,6 @@ import {
getConfigEntries,
} from "../../../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import { domainToName } from "../../../../../data/integration";
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-subpage";
@@ -52,10 +51,6 @@ export class BluetoothAdapterInfoPage extends LitElement {
@state() private _configEntries: ConfigEntry[] = [];
// Config entries of the integrations that provide remote scanners
// (for example ESPHome Bluetooth proxies), keyed by entry ID.
@state() private _sourceConfigEntries: Record<string, ConfigEntry> = {};
@state() private _connectionAllocationData: BluetoothAllocationsData[] = [];
@state() private _scannerStates: Record<string, BluetoothScannerState> = {};
@@ -79,45 +74,9 @@ export class BluetoothAdapterInfoPage extends LitElement {
}
private async _loadConfigEntries(): Promise<void> {
const configEntries = await getConfigEntries(this.hass);
const bluetoothEntries: ConfigEntry[] = [];
const sourceEntries: Record<string, ConfigEntry> = {};
for (const configEntry of configEntries) {
if (configEntry.domain === "bluetooth") {
bluetoothEntries.push(configEntry);
} else {
sourceEntries[configEntry.entry_id] = configEntry;
}
}
this._configEntries = bluetoothEntries;
this._sourceConfigEntries = sourceEntries;
}
/**
* Remote scanners (such as ESPHome Bluetooth proxies) are configured by the
* integration that provides them. The Bluetooth integration creates its own
* device for a remote scanner, linked through `via_device_id` to the device
* of the integration that provides it, so look there for that
* integration's config entry.
*/
private _getSourceConfigEntry(
device: DeviceRegistryEntry
): ConfigEntry | undefined {
const sourceDevice = device.via_device_id
? this.hass.devices[device.via_device_id]
: undefined;
if (!sourceDevice) {
return undefined;
}
if (
sourceDevice.primary_config_entry &&
sourceDevice.primary_config_entry in this._sourceConfigEntries
) {
return this._sourceConfigEntries[sourceDevice.primary_config_entry];
}
return sourceDevice.config_entries
.map((id) => this._sourceConfigEntries[id])
.find((e) => e !== undefined);
this._configEntries = await getConfigEntries(this.hass, {
domain: "bluetooth",
});
}
private async _subscribeBluetoothConnectionAllocations(): Promise<void> {
@@ -330,10 +289,6 @@ export class BluetoothAdapterInfoPage extends LitElement {
const scannerType: HaScannerType =
scannerDetails?.scanner_type ?? "unknown";
const isRemoteScanner = scannerType === "remote";
// Remote scanners are configured through the integration providing them.
const optionsEntry = isRemoteScanner
? this._getSourceConfigEntry(device)
: entry;
const hasMismatch = scannerState && isScannerStateMismatch(scannerState);
const allocations = scannerDetails
@@ -368,30 +323,16 @@ export class BluetoothAdapterInfoPage extends LitElement {
<div slot="headline">${deviceName}</div>
<div slot="supporting-text">${supportingParts.join(" · ")}</div>
${
optionsEntry &&
(!isRemoteScanner ||
(optionsEntry.supports_options && !optionsEntry.disabled_by))
!isRemoteScanner
? html`<ha-icon-button
slot="end"
.path=${mdiCogOutline}
.entry=${optionsEntry}
.entry=${entry}
@click=${this._openOptionFlow}
@keydown=${stopKeydownEnterSpacePropagation}
.label=${
isRemoteScanner
? this.hass.localize(
"ui.panel.config.bluetooth.source_option_flow",
{
integration: domainToName(
this.hass.localize,
optionsEntry.domain
),
}
)
: this.hass.localize(
"ui.panel.config.bluetooth.option_flow"
)
}
.label=${this.hass.localize(
"ui.panel.config.bluetooth.option_flow"
)}
></ha-icon-button>`
: nothing
}
@@ -309,7 +309,7 @@ class DialogMatterAddDevice extends LitElement {
const areaChanged = area !== origArea;
if (nameChanged || areaChanged) {
await updateDeviceRegistryEntry(this.hass.callWS, device.id, {
await updateDeviceRegistryEntry(this.hass, device.id, {
...(nameChanged && { name_by_user: name || null }),
...(areaChanged && { area_id: area || null }),
}).catch((err: Error) =>
@@ -328,15 +328,12 @@ class DialogMatterAddDevice extends LitElement {
.map((e) => e.entity_id);
if (entityIds.length) {
const mapping = await getAutomaticEntityIds(
this.hass.callWS,
entityIds
);
const mapping = await getAutomaticEntityIds(this.hass, entityIds);
await Promise.allSettled(
Object.entries(mapping)
.filter((entry): entry is [string, string] => !!entry[1])
.map(([oldId, newId]) =>
updateEntityRegistryEntry(this.hass.callWS, oldId, {
updateEntityRegistryEntry(this.hass, oldId, {
new_entity_id: newId,
}).catch((err: Error) =>
showAlertDialog(this, {
@@ -358,7 +355,7 @@ class DialogMatterAddDevice extends LitElement {
undefined;
if (deviceClass !== origClass) {
await updateEntityRegistryEntry(
this.hass.callWS,
this.hass,
this._mainEntity.entity_id,
{ device_class: deviceClass || null }
).catch((err: Error) =>
@@ -64,7 +64,10 @@ class ThreadNetworkInfoPage extends LitElement {
this._otbrLoading = true;
const otbrInfo = await getOTBRInfo(this.hass).catch(() => undefined);
this._otbrInfo = findOTBRInfoForDataset(otbrInfo, this._dataset);
const otbr = findOTBRInfoForDataset(otbrInfo, this._dataset);
if (otbr?.active_dataset_tlvs?.includes(this._dataset.extended_pan_id)) {
this._otbrInfo = otbr;
}
this._otbrLoading = false;
}
@@ -1,24 +1,6 @@
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { computeAreaName } from "../../../../../common/entity/compute_area_name";
import { getDeviceArea } from "../../../../../common/entity/context/get_device_context";
import type { Cluster, ZHADevice, ZHAGroup } from "../../../../../data/zha";
import { ZHADeviceType } from "../../../../../data/zha";
import type { HomeAssistant } from "../../../../../types";
export const getZHADeviceAreaName = (
device: ZHADevice,
areas: HomeAssistant["areas"],
devices: HomeAssistant["devices"]
): string | undefined => {
// The device registry is authoritative: an entry with no effective
// area means the device has no area, even if ZHA still reports one
// from its cached response. Resolve via the registry so a child
// device inherits its parent's area, matching the ZHA network
// visualization.
const haDevice = devices[device.device_reg_id];
const area = haDevice ? getDeviceArea(haDevice, areas, devices) : undefined;
return area ? computeAreaName(area) : undefined;
};
export const formatAsPaddedHex = (value: string | number): string => {
let hex = value;
@@ -1,4 +1,3 @@
import type { Connection } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -43,11 +42,6 @@ class ZHAAddDevicesPage extends LitElement {
private _wakeLock?: Promise<WakeLockSentinel>;
private _connection?: Connection;
// Set for the search in flight, so a pending subscription can tell it ended
private _search?: object;
public connectedCallback(): void {
super.connectedCallback();
if (this.route && this.route.path && this.route.path !== "") {
@@ -200,34 +194,28 @@ class ZHAAddDevicesPage extends LitElement {
}
}
// The backend ends the subscription when the connection drops, and it is not
// sent again, so end the search here and show "Search again"
private _connectionLost = () => {
this._unsubscribe();
};
private _unsubscribe(): Promise<void> {
this._active = false;
this._search = undefined;
this._connection?.removeEventListener("disconnected", this._connectionLost);
this._connection = undefined;
if (this._addDevicesTimeoutHandle) {
clearTimeout(this._addDevicesTimeoutHandle);
}
let unsubscribed = Promise.resolve();
if (this._subscribed) {
unsubscribed = this._subscribed.then(
(unsub) => unsub(),
// A subscription that was cancelled or lost with the connection has
// nothing to unsubscribe
() => undefined
);
unsubscribed = this._subscribed.then((unsub) => unsub());
this._subscribed = undefined;
}
this._wakeLock?.then((wakeLock) => wakeLock.release());
return unsubscribed;
}
private _deactivate(): void {
this._active = false;
if (this._addDevicesTimeoutHandle) {
clearTimeout(this._addDevicesTimeoutHandle);
}
this._wakeLock?.then((wakeLock) => wakeLock.release());
}
private _subscribe(): void {
if (!this.hass) {
return;
@@ -237,35 +225,22 @@ class ZHAAddDevicesPage extends LitElement {
// session before starting the next one
const unsubscribed = this._unsubscribe();
this._active = true;
const search = {};
this._search = search;
const data: any = { type: "zha/devices/permit", duration: 254 };
if (this._ieeeAddress) {
data.ieee = this._ieeeAddress;
}
const connection = this.hass.connection;
this._connection = connection;
connection.addEventListener("disconnected", this._connectionLost);
this._subscribed = unsubscribed
// A failed unsubscribe leaves nothing to wait for
.catch(() => undefined)
.then(() =>
// Each subscription opens the network for joining and turns on ZHA
// debug logging, so don't send it again after a reconnect
connection.subscribeMessage(
(message) => this._handleMessage(message),
data,
{
resubscribe: false,
// While offline, subscribeMessage waits for the connection. Don't
// open the network if this search ended in the meantime
preCheck: () => this._search === search,
}
data
)
);
this._addDevicesTimeoutHandle = setTimeout(
// End the subscription with the search, so debug logging ends too
() => this._unsubscribe(),
() => this._deactivate(),
254000
);
if ("wakeLock" in navigator) {
Loaded 100 of 183 files, more files were not shown because too many files have changed in this diff. Show more