Compare commits

..

73 Commits

Author SHA1 Message Date
Zack
ef834ef536 First pass 2022-08-01 15:39:41 -05:00
Zack Barett
c27e3325d9 Bumped version to 20220728.0 (#13300) 2022-07-28 10:10:28 -05:00
Zack Barett
08efc2fdd1 Update dialog with status line and other stuff (#13293) 2022-07-28 15:10:05 +00:00
Zack Barett
53519ae8ab Fix Dark Mode Map when set in Dashboard card (#13297) 2022-07-28 10:00:37 -05:00
Zack Barett
9baeabed19 Add integration dialog Scroll bar styles (#13299) 2022-07-28 09:54:41 -05:00
Zack Barett
f3229bb8a7 Update Show Skipped/ignored in updates/repairs, update dialog of integration startup time (#13296) 2022-07-28 10:39:16 +02:00
Franck Nijhof
86c971b76a Add My support for Repairs (#13294) 2022-07-28 02:07:07 +02:00
Paulus Schoutsen
afc69cb270 Render brand icon for repairs based on issue_domain (#13290) 2022-07-27 22:53:59 +02:00
Paulus Schoutsen
a379d29a6c Remove non fixable from repair dialog (#13292) 2022-07-27 15:50:07 -05:00
Franck Nijhof
0769b14566 Add Bluetooth as discovery source (#13291) 2022-07-27 20:30:51 +00:00
Paulus Schoutsen
1dc68b72da Pass translation placeholders to repair title translations (#13289) 2022-07-27 19:30:22 +00:00
Bram Kragten
c08be957ce Merge branch 'master' into dev 2022-07-27 12:23:47 +02:00
Bram Kragten
140e269697 Bumped version to 20220727.0 2022-07-27 12:22:43 +02:00
Bram Kragten
7c18d5aa0e Use subscribe to fetch repair issues (#13285) 2022-07-27 12:15:31 +02:00
Yosi Levy
1acdc9cd6c Various RTL fixes (#13268) 2022-07-27 11:40:50 +02:00
Yosi Levy
7501849044 Fix conversation RTL + text-alignment (#13264) 2022-07-27 11:39:44 +02:00
Yosi Levy
26ed13e548 RTL - Humidifier more info location (#13253) 2022-07-27 11:38:35 +02:00
Steve Repsher
086c33d8b3 Fix localize key types to remove user groups exception (#13259) 2022-07-27 11:37:52 +02:00
Zack Barett
c73677f15d Move System Information to Repairs (#13281)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-07-27 09:23:55 +00:00
Zack Barett
f7090583ac Update Repairs to new design (#13276)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-07-27 09:06:38 +00:00
Zack Barett
68517018cc Add IP Information Dialog (#13283)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-07-27 08:47:03 +00:00
Zack Barett
adf3fa6a0e Use new subscribe mixin for the sidebar to show counts of repairs (#13282)
* Use new subscribe mixin for the sidebar

* spelling
2022-07-26 15:43:46 -05:00
Bram Kragten
b443ec0af5 Repair issue fixes (#13272) 2022-07-25 15:37:33 +02:00
Zack Barett
b9ae0e72b1 Add Ignore Action + dialog updates (#13254) 2022-07-25 11:36:00 +02:00
Zack Barett
63ea8e6568 Allows for My to support Supported Brands (#13256) 2022-07-25 11:35:13 +02:00
Steve Repsher
5be624f45d Fix localize key types to remove config_entry exception (#13257) 2022-07-22 11:01:44 +02:00
Bram Kragten
38f19b6180 Repair: load translations for all integrations with issues (#13252) 2022-07-21 09:52:44 -05:00
Steve Repsher
c99f00ba50 Setup stronger type for localize key (#13244) 2022-07-21 13:13:11 +00:00
Zack Barett
ce5776f59d Add Repairs to Settings (#13249)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-07-21 14:48:37 +02:00
Steve Repsher
12ff70020a Fix more bad localize keys (#13250) 2022-07-21 12:17:29 +02:00
Felipe Santos
5d605447a5 Support more icons for media players (#12997)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-07-21 12:16:00 +02:00
Michael Irigoyen
6d88d46ce4 Update Material Design Icons to v7.0.96 (#13175)
* Update Material Design Icons to v7.0.96

* Fetch updated MDI packages
2022-07-21 11:43:48 +02:00
Bram Kragten
cbe2643146 Use translation_key for repairs (#13246) 2022-07-20 11:52:55 -05:00
Steve Repsher
d332b8ab14 Fix bad localize keys (#13245)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-07-20 18:34:09 +02:00
Bram Kragten
adfef05110 Bump contrast and brightness of dark mode map (#13243) 2022-07-20 14:15:34 +00:00
Yosi Levy
ca6a7bfbe2 Additional RTL energy fixes (#13182) 2022-07-20 14:37:48 +02:00
Franck Nijhof
a22f96a481 Migrate repairs to repairs API (#13242) 2022-07-20 14:34:57 +02:00
Yosi Levy
688109524d RTL fixes - media, attributes (#13241) 2022-07-20 11:14:50 +02:00
Sven Serlier
62dd7111ce Update design.home-assistant.io (#13240)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-07-20 08:24:52 +00:00
Zack Barett
1267575f62 Merge pull request #13235 from home-assistant/fix-builds
Stringify Python version to use 3.10 vs 3.1
2022-07-19 23:25:39 -05:00
Paulus Schoutsen
b7da4dc68f Stringify Python version to use 3.10 vs 3.1 2022-07-19 21:11:21 -07:00
Zack Barett
826474518f Merge pull request #13228 from voydz/lovelace-map-autofit 2022-07-19 16:44:48 -05:00
Maximilian Ertl
a4b92fef3a Correctly set "allow-downloads" flag on iframes (#13218) 2022-07-19 20:59:45 +02:00
Bram Kragten
d41159591c Change map styles to "Voyager" (#13227) 2022-07-19 20:56:50 +02:00
Felix Rudat
cb256bc386 Add auto_fit config option to lovelace map card 2022-07-19 17:45:48 +02:00
Zack Barett
bd50d6a6a3 Start the Repairs dashboard (#13192)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-07-19 14:25:47 +00:00
Bram Kragten
05418fc83b Fix dev tools event (#13225) 2022-07-19 11:17:13 +00:00
Franck Nijhof
8b675cdbba Remove unused mypy config from pyproject (#13224) 2022-07-19 13:14:20 +02:00
Franck Nijhof
36b4909950 Bump Python to 3.10 (#13223) 2022-07-19 12:28:57 +02:00
Raman Gupta
157b3ba5f2 Clean up zwave_js device actions logic (#13185) 2022-07-19 12:11:05 +02:00
Yosi Levy
72443b4f24 RTL card fixes (#13207) 2022-07-19 12:09:50 +02:00
J. Nick Koston
1c7d3fe610 Sync frontend recoverable states with core (#13197) 2022-07-19 12:09:20 +02:00
Franck Nijhof
6ac4560b36 Use YAML in developer tools events (#13222) 2022-07-19 12:07:51 +02:00
Zack Barett
9309a4c7bc Update language when ZHA or Zwave arent installed (re: supported brands) (#13191) 2022-07-19 11:44:02 +02:00
Franck Nijhof
b582a4d014 Bump node to 16 (#13221) 2022-07-19 11:39:08 +02:00
Zack Barett
e4d233afa8 Filter Integration in Target and Area selectors + clean up some code (#13202) 2022-07-18 22:07:55 +02:00
dependabot[bot]
b131b255ec Bump actions/stale from 3.0.13 to 5.1.0 (#13212)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-18 17:09:37 +02:00
Kendell R
20bdb9ff35 Use theme default font for charts (#13210)
* Use theme default font for charts

* Prettier
2022-07-18 15:35:51 +02:00
Zack Barett
b611a58fce Add support for Supported Brands (#13184) 2022-07-13 17:51:17 +02:00
Zack Barett
24e54554ad Fix History Graph Name not being friendly (#13179) 2022-07-13 11:48:52 +02:00
Erik Montnemery
d23fca4dd1 Correct display of barometric pressure and rain (#13183) 2022-07-12 16:13:29 -05:00
Paulus Schoutsen
729e2f5248 Use entity name in device info page (#13165)
* Use entity name in device info page

* Adjust to new format

* Use latest API

* Fix types

* Fix CI?

Co-authored-by: Zack <zackbarett@hey.com>
2022-07-11 19:07:07 -07:00
Zack Barett
437723c6a6 Fix Number Selector Label (#13178) 2022-07-12 01:05:47 +00:00
Bram Kragten
a30c8205b1 Update dialog styles (#13132) 2022-07-11 15:07:10 -05:00
puddly
c50cf78bb4 Allow stale ZHA coordinator entries to be deleted (#13154)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-07-11 13:14:37 -05:00
Zack Barett
be52ba0ea9 Fix Suggested Value in HA-Form (#13173) 2022-07-11 09:46:32 -05:00
Yosi Levy
55e9ebc4d2 Energy panel/cards - RTL fixes (#13171) 2022-07-11 15:47:22 +02:00
Bram Kragten
29c3fb0f92 Fix energy demo (#13172) 2022-07-11 08:44:34 -05:00
Erik Montnemery
7c3cd9d88d Merge pull request #13170 from home-assistant/number_customize_units
Allow customizing number unit of measurement
2022-07-11 14:49:47 +02:00
Erik
4881d699e3 Allow customizing number unit of measurement 2022-07-11 14:05:23 +02:00
Joakim Sørensen
414db83359 Hide homeassistant from partial restore if no version (#13168) 2022-07-11 12:55:43 +02:00
Joakim Sørensen
cd4f6e19f4 Await backup restore (#13167) 2022-07-11 12:50:37 +02:00
Joakim Sørensen
da709cbbd1 Add hacs_repository my redirect (#13153) 2022-07-11 09:06:45 +02:00
134 changed files with 3220 additions and 1176 deletions

View File

@@ -11,7 +11,7 @@ on:
- master
env:
NODE_VERSION: 14
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
jobs:

View File

@@ -6,7 +6,7 @@ on:
- dev
env:
NODE_VERSION: 14
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
jobs:

View File

@@ -6,8 +6,8 @@ on:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: 3.8
NODE_VERSION: 14
PYTHON_VERSION: "3.10"
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
permissions:

View File

@@ -6,8 +6,8 @@ on:
- published
env:
PYTHON_VERSION: 3.8
NODE_VERSION: 14
PYTHON_VERSION: "3.10"
NODE_VERSION: 16
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@@ -21,7 +21,7 @@ jobs:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v3

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@v3.0.13
uses: actions/stale@v5.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90

View File

@@ -8,7 +8,7 @@ on:
- src/translations/en.json
env:
NODE_VERSION: 14
NODE_VERSION: 16
jobs:
upload:

2
.nvmrc
View File

@@ -1 +1 @@
14
16

View File

@@ -1 +1,30 @@
[]
[
{
"path": "M20,20H7A2,2 0 0,1 5,18V8.94L2.23,5.64C2.09,5.47 2,5.24 2,5A1,1 0 0,1 3,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20M8.5,7A0.5,0.5 0 0,0 8,7.5V8.5A0.5,0.5 0 0,0 8.5,9H18.5A0.5,0.5 0 0,0 19,8.5V7.5A0.5,0.5 0 0,0 18.5,7H8.5M8.5,11A0.5,0.5 0 0,0 8,11.5V12.5A0.5,0.5 0 0,0 8.5,13H18.5A0.5,0.5 0 0,0 19,12.5V11.5A0.5,0.5 0 0,0 18.5,11H8.5M8.5,15A0.5,0.5 0 0,0 8,15.5V16.5A0.5,0.5 0 0,0 8.5,17H13.5A0.5,0.5 0 0,0 14,16.5V15.5A0.5,0.5 0 0,0 13.5,15H8.5Z",
"name": "android-messages"
},
{
"path": "M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M20,12L17.5,10.5L15,12V4H20V12Z",
"name": "book-variant-multiple"
},
{
"path": "M21,14H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10L8,21V22H16V21L14,18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z",
"name": "desktop-mac"
},
{
"path": "M21,14V4H3V14H21M21,2A2,2 0 0,1 23,4V16A2,2 0 0,1 21,18H14L16,21V22H8V21L10,18H3C1.89,18 1,17.1 1,16V4C1,2.89 1.89,2 3,2H21M4,5H15V10H4V5M16,5H20V7H16V5M20,8V13H16V8H20M4,11H9V13H4V11M10,11H15V13H10V11Z",
"name": "desktop-mac-dashboard"
},
{
"path": "M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z",
"name": "discord"
},
{
"path": "M8.06,7.78C7.5,7.78 7.17,7.73 7.08,7.64L6.66,13.73C7.19,14.05 7.88,14.3 8.72,14.5C9.56,14.71 10.78,14.77 12.38,14.67C13.97,14.58 15.63,14.23 17.34,13.64L16.55,4.22C15.67,5.09 14.38,5.91 12.66,6.66C11.13,7.31 9.81,7.69 8.72,7.78H8.06M7.97,5.34C7.28,5.94 7,6.34 7.13,6.56C7.22,6.78 7.7,6.84 8.58,6.75C9.67,6.66 10.91,6.31 12.28,5.72C13.22,5.31 14.03,4.88 14.72,4.41C15.41,3.94 15.88,3.55 16.13,3.23C16.38,2.92 16.47,2.7 16.41,2.58C16.34,2.42 16.03,2.34 15.47,2.34C14.34,2.34 12.94,2.7 11.25,3.42C9.81,4.05 8.72,4.69 7.97,5.34M17.34,2.2C17.41,2.33 17.44,2.47 17.44,2.63L18.61,17C18.61,18.73 18,20.09 16.83,21.07C15.64,22.05 14.03,22.55 12,22.55C10,22.55 8.4,22.04 7.2,21C6,20 5.39,18.64 5.39,16.92L6.09,6.47C6.09,6.22 6.2,5.94 6.42,5.63C6.64,5.31 6.84,5.06 7.03,4.88L7.36,4.59C8.33,3.78 9.5,3.08 10.88,2.5C11.81,2.08 12.73,1.77 13.62,1.57C14.5,1.37 15.3,1.3 16,1.38C16.71,1.46 17.16,1.73 17.34,2.2Z",
"name": "google-home"
},
{
"path": "M19.25,19H4.75V3H19.25M14,22H10V21H14M18,0H6A3,3 0 0,0 3,3V21A3,3 0 0,0 6,24H18A3,3 0 0,0 21,21V3A3,3 0 0,0 18,0Z",
"name": "tablet-android"
}
]

View File

@@ -1,5 +1,4 @@
// Compat needs to be first import
import "../../src/resources/compatibility";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate";
import {
@@ -7,9 +6,14 @@ import {
provideHass,
} from "../../src/fake_data/provide_hass";
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import "../../src/resources/compatibility";
import { HomeAssistant } from "../../src/types";
import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries";
import { mockEnergy } from "./stubs/energy";
import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFrontend } from "./stubs/frontend";
import { mockHistory } from "./stubs/history";
@@ -20,9 +24,6 @@ import { mockShoppingList } from "./stubs/shopping_list";
import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations";
import { mockEnergy } from "./stubs/energy";
import { mockConfig } from "./stubs/config";
import { energyEntities } from "./stubs/entities";
class HaDemo extends HomeAssistantAppEl {
protected async _initializeHass() {
@@ -51,8 +52,36 @@ class HaDemo extends HomeAssistantAppEl {
mockMediaPlayer(hass);
mockFrontend(hass);
mockEnergy(hass);
mockConfig(hass);
mockPersistentNotification(hass);
mockConfigEntries(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.co2_intensity",
name: null,
icon: null,
platform: "co2signal",
hidden_by: null,
entity_category: null,
has_entity_name: false,
},
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.grid_fossil_fuel_percentage",
name: null,
icon: null,
platform: "co2signal",
hidden_by: null,
entity_category: null,
has_entity_name: false,
},
]);
hass.addEntities(energyEntities());

View File

@@ -1,41 +0,0 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockAPI("config/config_entries/entry?domain=co2signal", () => [
{
entry_id: "co2signal",
domain: "co2signal",
title: "CO2 Signal",
source: "user",
state: "loaded",
supports_options: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
},
]);
hass.mockWS("config/entity_registry/list", () => [
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.co2_intensity",
name: null,
icon: null,
platform: "co2signal",
},
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.grid_fossil_fuel_percentage",
name: null,
icon: null,
platform: "co2signal",
},
]);
};

View File

@@ -0,0 +1,20 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfigEntries = (hass: MockHomeAssistant) => {
hass.mockWS("config_entries/get", () => [
{
entry_id: "co2signal",
domain: "co2signal",
title: "CO2 Signal",
source: "user",
state: "loaded",
supports_options: false,
supports_remove_device: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
},
]);
};

View File

@@ -4,4 +4,6 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEntityRegistry = (
hass: MockHomeAssistant,
data: EntityRegistryEntry[] = []
) => hass.mockWS("config/entity_registry/list", () => data);
) => {
hass.mockWS("config/entity_registry/list", () => data);
};

View File

@@ -8,7 +8,7 @@ module.exports = [
{
category: "lovelace",
// Label for in the sidebar
header: "Lovelace",
header: "Dashboards",
// Specify order of pages. Any pages in the category folder but not listed here will
// automatically be added after the pages listed here.
pages: ["introduction"],
@@ -34,7 +34,7 @@ module.exports = [
},
{
category: "misc",
header: "Miscelaneous",
header: "Miscellaneous",
},
{
category: "brand",

View File

@@ -31,7 +31,7 @@ const ENTITIES = [
friendly_name: "Office Light",
}),
getEntity("fan", "kitchen", "on", {
friendly_name: "Second Office Fan",
friendly_name: "Kitchen Fan",
}),
getEntity("binary_sensor", "kitchen_door", "on", {
friendly_name: "Office Door",
@@ -102,7 +102,7 @@ class DemoArea extends LitElement {
picture: "/images/office.jpg",
},
{
name: "Second Office",
name: "Kitchen",
area_id: "kitchen",
picture: "/images/kitchen.png",
},

View File

@@ -1,11 +1,11 @@
---
title: Introduction
---
Lovelace has many different cards. Each card allows the user to tell
Dashboards have many different cards. Each card allows the user to tell
a different story about what is going on in their house. These cards
are very customizable, as no household is the same.
This gallery helps our developers and designers to see all the
different states that each card can be in.
Check [the Lovelace documentation](https://www.home-assistant.io/lovelace) for instructions on how to get started with Lovelace.
Check [the Dashboards documentation](https://www.home-assistant.io/dashboards/) for instructions on how to get started with Dashboards.

View File

@@ -194,6 +194,7 @@ const createEntityRegistryEntries = (
name: null,
icon: null,
platform: "updater",
has_entity_name: false,
},
];

View File

@@ -69,7 +69,7 @@ const ENTITIES = [
effect_list: ["random", "colorloop"],
}),
getEntity("light", "color_RGB_light", "on", {
friendly_name: "Color Effets Light",
friendly_name: "Color Effects Light",
brightness: 255,
rgb_color: [30, 100, 255],
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,

View File

@@ -81,10 +81,10 @@ class HassioAddonRepositoryEl extends LitElement {
? this.supervisor.localize(
"common.new_version_available"
)
: this.supervisor.localize("addon.installed")
: this.supervisor.localize("addon.state.installed")
: addon.available
? this.supervisor.localize("addon.not_installed")
: this.supervisor.localize("addon.not_available")}
? this.supervisor.localize("addon.state.not_installed")
: this.supervisor.localize("addon.state.not_available")}
.iconClass=${addon.installed
? addon.update_available
? "update"

View File

@@ -336,7 +336,7 @@ class HassioAddonConfig extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.supervisor.localize(
"addon.common.update_available",
"addon.failed_to_reset",
"error",
extractApiErrorMessage(err)
);

View File

@@ -81,7 +81,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
);
} catch (err: any) {
this._error = this.supervisor.localize(
"addon.documentation.get_logs",
"addon.documentation.get_documentation",
"error",
extractApiErrorMessage(err)
);

View File

@@ -168,23 +168,24 @@ export class SupervisorBackupContent extends LitElement {
: ""}
${this.backupType === "partial"
? html`<div class="partial-picker">
<ha-formfield
.label=${html`<supervisor-formfield-label
label="Home Assistant"
.iconPath=${mdiHomeAssistant}
.version=${this.backup
? this.backup.homeassistant
: this.hass.config.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.checked=${this.homeAssistant}
@change=${this.toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>
${this.backup?.homeassistant
? html`<ha-formfield
.label=${html`<supervisor-formfield-label
label="Home Assistant"
.iconPath=${mdiHomeAssistant}
.version=${this.backup
? this.backup.homeassistant
: this.hass.config.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.checked=${this.homeAssistant}
@change=${this.toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>`
: ""}
${foldersSection?.templates.length
? html`
<ha-formfield

View File

@@ -201,26 +201,24 @@ class HassioBackupDialog
}
if (!this._dialogParams?.onboarding) {
this.hass!.callApi(
"POST",
try {
await this.hass!.callApi(
"POST",
`hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/partial`,
backupDetails
).then(
() => {
this.closeDialog();
},
(error) => {
this._error = error.body.message;
}
);
`hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/partial`,
backupDetails
);
this.closeDialog();
} catch (error: any) {
this._error = error.body.message;
}
} else {
fireEvent(this, "restoring");
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
method: "POST",
body: JSON.stringify(backupDetails),
});

View File

@@ -25,7 +25,7 @@ import {
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { HomeAssistant, Route } from "../../src/types";
import { HomeAssistant, Route, TranslationDict } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation";
declare global {
@@ -124,9 +124,13 @@ export class SupervisorBaseElement extends urlSyncMixin(
this.supervisor = {
...this.supervisor,
localize: await computeLocalize(this.constructor.prototype, language, {
[language]: data,
}),
localize: await computeLocalize<TranslationDict["supervisor"]>(
this.constructor.prototype,
language,
{
[language]: data,
}
),
};
}

View File

@@ -26,7 +26,7 @@ import {
import {
UNHEALTHY_REASON_URL,
UNSUPPORTED_REASON_URL,
} from "../../../src/panels/config/system-health/ha-config-system-health";
} from "../../../src/panels/config/repairs/dialog-system-information";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";

View File

@@ -72,8 +72,8 @@
"@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.9.96",
"@mdi/svg": "6.9.96",
"@mdi/js": "7.0.96",
"@mdi/svg": "7.0.96",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20220707.1"
version = "20220728.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
@@ -23,8 +23,3 @@ include-package-data = true
[tool.setuptools.packages.find]
include = ["hass_frontend*"]
[tool.mypy]
python_version = 3.4
show_error_codes = true
strict = true

View File

@@ -47,7 +47,7 @@ import {
mdiRobotVacuum,
mdiScriptText,
mdiSineWave,
mdiTextToSpeech,
mdiMicrophoneMessage,
mdiThermometer,
mdiThermostat,
mdiTimerOutline,
@@ -74,8 +74,9 @@ export const FIXED_DOMAIN_ICONS = {
camera: mdiVideo,
climate: mdiThermostat,
configurator: mdiCog,
conversation: mdiTextToSpeech,
conversation: mdiMicrophoneMessage,
counter: mdiCounter,
demo: mdiHomeAssistant,
fan: mdiFan,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,

View File

@@ -5,8 +5,7 @@ export type LeafletModuleType = typeof import("leaflet");
export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async (
mapElement: HTMLElement,
darkMode?: boolean
mapElement: HTMLElement
): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element");
@@ -23,7 +22,7 @@ export const setupLeafletMap = async (
mapElement.parentNode.appendChild(style);
map.setView([52.3731339, 4.8903147], 13);
const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map);
const tileLayer = createTileLayer(Leaflet).addTo(map);
return [map, Leaflet, tileLayer];
};
@@ -31,23 +30,19 @@ export const setupLeafletMap = async (
export const replaceTileLayer = (
leaflet: LeafletModuleType,
map: Map,
tileLayer: TileLayer,
darkMode: boolean
tileLayer: TileLayer
): TileLayer => {
map.removeLayer(tileLayer);
tileLayer = createTileLayer(leaflet, darkMode);
tileLayer = createTileLayer(leaflet);
tileLayer.addTo(map);
return tileLayer;
};
const createTileLayer = (
leaflet: LeafletModuleType,
darkMode: boolean
): TileLayer =>
const createTileLayer = (leaflet: LeafletModuleType): TileLayer =>
leaflet.tileLayer(
`https://{s}.basemaps.cartocdn.com/${
darkMode ? "dark_all" : "light_all"
}/{z}/{x}/{y}${leaflet.Browser.retina ? "@2x.png" : ".png"}`,
`https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}${
leaflet.Browser.retina ? "@2x.png" : ".png"
}`,
{
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/attributions">CARTO</a>',

View File

@@ -8,6 +8,7 @@ import {
mdiCalendar,
mdiCast,
mdiCastConnected,
mdiCastOff,
mdiChartSankey,
mdiCheckCircleOutline,
mdiClock,
@@ -25,7 +26,15 @@ import {
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiSpeaker,
mdiSpeakerOff,
mdiSpeakerPause,
mdiSpeakerPlay,
mdiSwapHorizontal,
mdiTelevision,
mdiTelevisionOff,
mdiTelevisionPause,
mdiTelevisionPlay,
mdiToggleSwitchVariant,
mdiToggleSwitchVariantOff,
mdiWeatherNight,
@@ -127,7 +136,40 @@ export const domainIconWithoutDefault = (
}
case "media_player":
return compareState === "playing" ? mdiCastConnected : mdiCast;
switch (stateObj?.attributes.device_class) {
case "speaker":
switch (compareState) {
case "playing":
return mdiSpeakerPlay;
case "paused":
return mdiSpeakerPause;
case "off":
return mdiSpeakerOff;
default:
return mdiSpeaker;
}
case "tv":
switch (compareState) {
case "playing":
return mdiTelevisionPlay;
case "paused":
return mdiTelevisionPause;
case "off":
return mdiTelevisionOff;
default:
return mdiTelevision;
}
default:
switch (compareState) {
case "playing":
case "paused":
return mdiCastConnected;
case "off":
return mdiCastOff;
default:
return mdiCast;
}
}
case "switch":
switch (stateObj?.attributes.device_class) {

View File

@@ -0,0 +1,88 @@
import { html } from "lit";
import { getConfigEntries } from "../../data/config_entries";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { isComponentLoaded } from "../config/is_component_loaded";
import { fireEvent } from "../dom/fire_event";
import { navigate } from "../navigate";
export const protocolIntegrationPicked = async (
element: HTMLElement,
hass: HomeAssistant,
slug: string
) => {
if (slug === "zwave_js") {
const entries = await getConfigEntries(hass, {
domain: "zwave_js",
});
if (!entries.length) {
// If the component isn't loaded, ask them to load the integration first
showConfirmationDialog(element, {
text: hass.localize(
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
{
integration: "Z-Wave",
supported_hardware_link: html`<a
href=${documentationUrl(hass, "/docs/z-wave/controllers")}
target="_blank"
rel="noreferrer"
>${hass.localize(
"ui.panel.config.integrations.config_flow.supported_hardware"
)}</a
>`,
}
),
confirmText: hass.localize(
"ui.panel.config.integrations.config_flow.proceed"
),
confirm: () => {
fireEvent(element, "handler-picked", {
handler: "zwave_js",
});
},
});
return;
}
showZWaveJSAddNodeDialog(element, {
entry_id: entries[0].entry_id,
});
} else if (slug === "zha") {
// If the component isn't loaded, ask them to load the integration first
if (!isComponentLoaded(hass, "zha")) {
showConfirmationDialog(element, {
text: hass.localize(
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
{
integration: "Zigbee",
supported_hardware_link: html`<a
href=${documentationUrl(
hass,
"/integrations/zha/#known-working-zigbee-radio-modules"
)}
target="_blank"
rel="noreferrer"
>${hass.localize(
"ui.panel.config.integrations.config_flow.supported_hardware"
)}</a
>`,
}
),
confirmText: hass.localize(
"ui.panel.config.integrations.config_flow.proceed"
),
confirm: () => {
fireEvent(element, "handler-picked", {
handler: "zha",
});
},
});
return;
}
navigate("/config/zha/add");
}
};

View File

@@ -3,10 +3,39 @@ import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-plur
import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/lib/should-polyfill";
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
import IntlMessageFormat from "intl-messageformat";
import { Resources } from "../../types";
import { Resources, TranslationDict } from "../../types";
import { getLocalLanguage } from "../../util/common-translation";
export type LocalizeFunc = (key: string, ...args: any[]) => string;
// Exclude some patterns from key type checking for now
// These are intended to be removed as errors are fixed
// Fixing component category will require tighter definition of types from backend and/or web socket
type LocalizeKeyExceptions =
| `${string}`
| `panel.${string}`
| `state.${string}`
| `state_attributes.${string}`
| `state_badge.${string}`
| `ui.${string}`
| `${keyof TranslationDict["supervisor"]}.${string}`
| `component.${string}`;
// Tweaked from https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types
type FlattenObjectKeys<
T extends Record<string, any>,
Key extends keyof T = keyof T
> = Key extends string
? T[Key] extends Record<string, unknown>
? `${Key}.${FlattenObjectKeys<T[Key]>}`
: `${Key}`
: never;
export type LocalizeFunc<
Dict extends Record<string, unknown> = TranslationDict
> = (
key: FlattenObjectKeys<Dict> | LocalizeKeyExceptions,
...args: any[]
) => string;
interface FormatType {
[format: string]: any;
}
@@ -65,12 +94,14 @@ export const polyfillsLoaded =
* }
*/
export const computeLocalize = async (
export const computeLocalize = async <
Dict extends Record<string, unknown> = TranslationDict
>(
cache: any,
language: string,
resources: Resources,
formats?: FormatsType
): Promise<LocalizeFunc> => {
): Promise<LocalizeFunc<Dict>> => {
if (polyfillsLoaded) {
await polyfillsLoaded;
}

View File

@@ -188,6 +188,10 @@ export default class HaChartBase extends LitElement {
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
"--secondary-text-color"
);
ChartConstructor.defaults.font.family =
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
computedStyles.getPropertyValue("--mdc-typography-font-family") ||
"Roboto, Noto, sans-serif";
this.chart = new ChartConstructor(ctx, {
type: this.chartType,
@@ -376,6 +380,7 @@ export default class HaChartBase extends LitElement {
.chartTooltip .title {
text-align: center;
font-weight: 500;
direction: ltr;
}
.chartTooltip .footer {
font-weight: 500;

View File

@@ -84,20 +84,20 @@ class HaAddonPicker extends LitElement {
} else {
showAlertDialog(this, {
title: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.title"
"ui.components.addon-picker.error.no_supervisor.title"
),
text: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.description"
"ui.components.addon-picker.error.no_supervisor.description"
),
});
}
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.title"
"ui.components.addon-picker.error.fetch_addons.title"
),
text: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.description"
"ui.components.addon-picker.error.fetch_addons.description"
),
});
}

View File

@@ -76,6 +76,7 @@ class HaAttributes extends LitElement {
css`
.attribute-container {
margin-bottom: 8px;
direction: ltr;
}
.data-entry {
display: flex;

View File

@@ -51,7 +51,7 @@ class HaBluePrintPicker extends LitElement {
return html`
<ha-select
.label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.label")}
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
fixedMenuPosition
naturalMenuWidth
.value=${this.value}

View File

@@ -11,7 +11,7 @@ export const createCloseHeading = (
hass: HomeAssistant,
title: string | TemplateResult
) => html`
<span class="header_title">${title}</span>
<div class="header_title">${title}</div>
<ha-icon-button
.label=${hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
@@ -40,10 +40,13 @@ export class HaDialog extends DialogBase {
z-index: var(--dialog-z-index, 7);
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
backdrop-filter: var(--dialog-backdrop-filter, none);
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
--mdc-typography-headline6-font-weight: 400;
--mdc-typography-headline6-font-size: 1.574rem;
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 8px);
padding-bottom: max(env(safe-area-inset-bottom), 24px);
}
.mdc-dialog__actions span:nth-child(1) {
flex: var(--secondary-action-button-flex, unset);
@@ -54,17 +57,23 @@ export class HaDialog extends DialogBase {
.mdc-dialog__container {
align-items: var(--vertial-align-dialog, center);
}
.mdc-dialog__title {
padding: 24px 24px 0 24px;
}
.mdc-dialog__actions {
padding: 0 24px 24px 24px;
}
.mdc-dialog__title::before {
display: block;
height: 20px;
height: 0px;
}
.mdc-dialog .mdc-dialog__content {
position: var(--dialog-content-position, relative);
padding: var(--dialog-content-padding, 20px 24px);
padding: var(--dialog-content-padding, 24px);
}
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
padding-bottom: max(
var(--dialog-content-padding, 20px),
var(--dialog-content-padding, 24px),
env(safe-area-inset-bottom)
);
}
@@ -72,10 +81,7 @@ export class HaDialog extends DialogBase {
position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top);
min-height: var(--mdc-dialog-min-height, auto);
border-radius: var(
--ha-dialog-border-radius,
var(--ha-card-border-radius, 4px)
);
border-radius: var(--ha-dialog-border-radius, 28px);
}
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
@@ -89,8 +95,8 @@ export class HaDialog extends DialogBase {
color: inherit;
}
.header_title {
margin-right: 40px;
margin-inline-end: 40px;
margin-right: 32px;
margin-inline-end: 32px;
margin-inline-start: initial;
direction: var(--direction);
}

View File

@@ -29,7 +29,102 @@ interface DeprecatedIcon {
};
}
const mdiDeprecatedIcons: DeprecatedIcon = {};
const mdiDeprecatedIcons: DeprecatedIcon = {
"android-messages": {
newName: "message-text",
removeIn: "2022.10",
},
"book-variant-multiple": {
newName: "bookmark-box-multiple",
removeIn: "2022.10",
},
"desktop-mac": {
newName: "monitor",
removeIn: "2022.10",
},
"desktop-mac-dashboard": {
newName: "monitor-dashboard",
removeIn: "2022.10",
},
discord: {
removeIn: "2022.10",
},
"diving-scuba": {
newName: "diving-scuba-mask",
removeIn: "2022.10",
},
"email-send": {
newName: "email-arrow-right",
removeIn: "2022.10",
},
"email-send-outline": {
newName: "email-arrow-right-outline",
removeIn: "2022.10",
},
"email-receive": {
newName: "email-arrow-left",
removeIn: "2022.10",
},
"email-receive-outline": {
newName: "email-arrow-left-outline",
removeIn: "2022.10",
},
"format-textdirection-r-to-l": {
newName: "format-pilcrow-arrow-left",
removeIn: "2022.10",
},
"format-textdirection-l-to-r": {
newName: "format-pilcrow-arrow-right",
removeIn: "2022.10",
},
"google-controller": {
newName: "controller",
removeIn: "2022.10",
},
"google-controller-off": {
newName: "controller-off",
removeIn: "2022.10",
},
"google-home": {
removeIn: "2022.10",
},
lecturn: {
newName: "lectern",
removeIn: "2022.10",
},
receipt: {
newName: "receipt-text",
removeIn: "2022.10",
},
"receipt-outline": {
newName: "receipt-text-outline",
removeIn: "2022.10",
},
"tablet-android": {
newName: "tablet",
removeIn: "2022.10",
},
"text-to-speech": {
newName: "microphone-message",
removeIn: "2022.10",
},
"text-to-speech-off": {
newName: "microphone-message-off",
removeIn: "2022.10",
},
"timeline-help": {
newName: "timeline-question",
removeIn: "2022.10",
},
"timeline-help-outline": {
newName: "timeline-question-outline",
removeIn: "2022.10",
},
"vector-point": {
newName: "vector-point-select",
removeIn: "2022.10",
},
};
const chunks: Chunks = {};

View File

@@ -52,6 +52,11 @@ export class HaSelect extends SelectBase {
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
inset-inline-start: 48px;
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select .mdc-select__anchor {
padding-inline-start: 12px;
padding-inline-end: 0px;

View File

@@ -1,8 +1,9 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { DeviceRegistryEntry } from "../../data/device_registry";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
@@ -11,7 +12,11 @@ import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import { AreaSelector } from "../../data/selector";
import type { AreaSelector } from "../../data/selector";
import {
filterSelectorDevices,
filterSelectorEntities,
} from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../ha-area-picker";
@@ -29,13 +34,15 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
public hassSubscribe(): UnsubscribeFunc[] {
return [
@@ -45,7 +52,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
];
}
protected updated(changedProperties) {
protected updated(changedProperties: PropertyValues): void {
if (
changedProperties.has("selector") &&
(this.selector.area.device?.integration ||
@@ -58,7 +65,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
}
}
protected render() {
protected render(): TemplateResult {
if (
(this.selector.area.device?.integration ||
this.selector.area.entity?.integration) &&
@@ -77,12 +84,6 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-area-picker>
@@ -98,27 +99,22 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-areas-picker>
`;
}
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
const filterIntegration = this.selector.area.entity?.integration;
if (
filterIntegration &&
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
) {
return false;
private _filterEntities = (entity: HassEntity): boolean => {
if (!this.selector.area.entity) {
return true;
}
return true;
return filterSelectorEntities(
this.selector.area.entity,
entity,
this._entitySources
);
};
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
@@ -126,47 +122,17 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
return true;
}
const {
manufacturer: filterManufacturer,
model: filterModel,
integration: filterIntegration,
} = this.selector.area.device;
const deviceIntegrations =
this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined;
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
return false;
}
if (filterModel && device.model !== filterModel) {
return false;
}
if (filterIntegration && this._entitySources && this._entities) {
const deviceIntegrations = this._deviceIntegrations(
this._entitySources,
this._entities
);
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
return false;
}
}
return true;
return filterSelectorDevices(
this.selector.area.device,
device,
deviceIntegrations
);
};
private _deviceIntegrations = memoizeOne(
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
const deviceIntegrations: Record<string, string[]> = {};
for (const entity of entities) {
const source = entitySources[entity.entity_id];
if (!source?.domain) {
continue;
}
if (!deviceIntegrations[entity.device_id!]) {
deviceIntegrations[entity.device_id!] = [];
}
deviceIntegrations[entity.device_id!].push(source.domain);
}
return deviceIntegrations;
}
);
}
declare global {

View File

@@ -2,8 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ConfigEntry } from "../../data/config_entries";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
@@ -13,6 +13,7 @@ import {
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector";
import { filterSelectorDevices } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import "../device/ha-device-picker";
@@ -34,12 +35,12 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
@property() public helper?: string;
@state() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
@@ -107,48 +108,17 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
}
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
const {
manufacturer: filterManufacturer,
model: filterModel,
integration: filterIntegration,
} = this.selector.device;
const deviceIntegrations =
this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined;
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
return false;
}
if (filterModel && device.model !== filterModel) {
return false;
}
if (filterIntegration && this._entitySources && this._entities) {
const deviceIntegrations = this._deviceIntegrations(
this._entitySources,
this._entities
);
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
return false;
}
}
return true;
return filterSelectorDevices(
this.selector.device,
device,
deviceIntegrations
);
};
private _deviceIntegrations = memoizeOne(
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
const deviceIntegrations: Record<string, string[]> = {};
for (const entity of entities) {
const source = entitySources[entity.entity_id];
if (!source?.domain) {
continue;
}
if (!deviceIntegrations[entity.device_id!]) {
deviceIntegrations[entity.device_id!] = [];
}
deviceIntegrations[entity.device_id!].push(source.domain);
}
return deviceIntegrations;
}
);
}
declare global {

View File

@@ -1,12 +1,12 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import { EntitySelector } from "../../data/selector";
import type { EntitySelector } from "../../data/selector";
import { filterSelectorEntities } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker";
import "../entity/ha-entity-picker";
@@ -73,37 +73,8 @@ export class HaEntitySelector extends LitElement {
}
}
private _filterEntities = (entity: HassEntity): boolean => {
const {
domain: filterDomain,
device_class: filterDeviceClass,
integration: filterIntegration,
} = this.selector.entity;
if (filterDomain) {
const entityDomain = computeStateDomain(entity);
if (
Array.isArray(filterDomain)
? !filterDomain.includes(entityDomain)
: entityDomain !== filterDomain
) {
return false;
}
}
if (
filterDeviceClass &&
entity.attributes.device_class !== filterDeviceClass
) {
return false;
}
if (
filterIntegration &&
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
) {
return false;
}
return true;
};
private _filterEntities = (entity: HassEntity): boolean =>
filterSelectorEntities(this.selector.entity, entity, this._entitySources);
}
declare global {

View File

@@ -4,9 +4,9 @@ import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { NumberSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-input-helper-text";
import "../ha-slider";
import "../ha-textfield";
import "../ha-input-helper-text";
@customElement("ha-selector-number")
export class HaNumberSelector extends LitElement {
@@ -30,21 +30,25 @@ export class HaNumberSelector extends LitElement {
const isBox = this.selector.number.mode === "box";
return html`
${this.label ? html`${this.label}${this.required ? " *" : ""}` : ""}
<div class="input">
${!isBox
? html`<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step ?? 1}
.disabled=${this.disabled}
.required=${this.required}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
>
</ha-slider>`
? html`
${this.label
? html`${this.label}${this.required ? " *" : ""}`
: ""}
<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step ?? 1}
.disabled=${this.disabled}
.required=${this.required}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
>
</ha-slider>
`
: ""}
<ha-textfield
inputMode="numeric"

View File

@@ -3,17 +3,33 @@ import {
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceRegistryEntry } from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { TargetSelector } from "../../data/selector";
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
DeviceRegistryEntry,
getDeviceIntegrationLookup,
} from "../../data/device_registry";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import { subscribeEntityRegistry } from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import {
filterSelectorDevices,
filterSelectorEntities,
TargetSelector,
} from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import type { HomeAssistant } from "../../types";
import "../ha-target-picker";
@customElement("ha-selector-target")
@@ -28,119 +44,82 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@property() public helper?: string;
@state() private _entityPlaformLookup?: Record<string, string>;
@state() private _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
const entityLookup = {};
for (const confEnt of entities) {
if (!confEnt.platform) {
continue;
}
entityLookup[confEnt.entity_id] = confEnt.platform;
}
this._entityPlaformLookup = entityLookup;
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
}
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (
oldSelector !== this.selector &&
(this.selector.target.device?.integration ||
this.selector.target.entity?.integration)
) {
this._loadConfigEntries();
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
changedProperties.has("selector") &&
this.selector.target.device?.integration &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._entitySources = sources;
});
}
}
protected render() {
protected render(): TemplateResult {
if (
(this.selector.target.device?.integration ||
this.selector.target.entity?.integration) &&
!this._entitySources
) {
return html``;
}
return html`<ha-target-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.entityRegFilter=${this._filterRegEntities}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.target.entity?.device_class
? [this.selector.target.entity.device_class]
: undefined}
.includeDomains=${this.selector.target.entity?.domain
? [this.selector.target.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-target-picker>`;
}
private _filterEntities = (entity: HassEntity): boolean => {
if (
this.selector.target.entity?.integration ||
this.selector.target.device?.integration
) {
if (
!this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !==
(this.selector.target.entity?.integration ||
this.selector.target.device?.integration)
) {
return false;
}
if (!this.selector.target.entity) {
return true;
}
return true;
};
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => {
if (this.selector.target.entity?.integration) {
if (entity.platform !== this.selector.target.entity.integration) {
return false;
}
}
return true;
return filterSelectorEntities(
this.selector.target.entity,
entity,
this._entitySources
);
};
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if (
this.selector.target.device?.manufacturer &&
device.manufacturer !== this.selector.target.device.manufacturer
) {
return false;
if (!this.selector.target.device) {
return true;
}
if (
this.selector.target.device?.model &&
device.model !== this.selector.target.device.model
) {
return false;
}
if (
this.selector.target.device?.integration ||
this.selector.target.entity?.integration
) {
if (
!this._configEntries?.some((entry) =>
device.config_entries.includes(entry.entry_id)
)
) {
return false;
}
}
return true;
};
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) =>
entry.domain === this.selector.target.device?.integration ||
entry.domain === this.selector.target.entity?.integration
const deviceIntegrations =
this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined;
return filterSelectorDevices(
this.selector.target.device,
device,
deviceIntegrations
);
}
};
static get styles(): CSSResultGroup {
return css`

View File

@@ -21,6 +21,7 @@ import "@polymer/paper-item/paper-icon-item";
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResult,
@@ -44,7 +45,9 @@ import {
PersistentNotification,
subscribeNotifications,
} from "../data/persistent_notification";
import { subscribeRepairsIssueRegistry } from "../data/repairs";
import { updateCanInstall, UpdateEntity } from "../data/update";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -177,7 +180,7 @@ const computePanels = memoizeOne(
let Sortable;
@customElement("ha-sidebar")
class HaSidebar extends LitElement {
class HaSidebar extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
@@ -192,6 +195,8 @@ class HaSidebar extends LitElement {
@state() private _updatesCount = 0;
@state() private _issuesCount = 0;
@state() private _renderEmptySortable = false;
private _mouseLeaveTimeout?: number;
@@ -214,6 +219,16 @@ class HaSidebar extends LitElement {
private _sortable?;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._issuesCount = repairs.issues.filter(
(issue) => !issue.ignored
).length;
}),
];
}
protected render() {
if (!this.hass) {
return html``;
@@ -238,6 +253,7 @@ class HaSidebar extends LitElement {
changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") ||
changedProps.has("_notifications") ||
changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") ||
@@ -500,7 +516,7 @@ class HaSidebar extends LitElement {
}
private _renderConfiguration(title: string | null) {
return html` <a
return html`<a
class="configuration-container"
role="option"
href="/config"
@@ -511,17 +527,20 @@ class HaSidebar extends LitElement {
>
<paper-icon-item class="configuration" role="option">
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
${!this.alwaysExpand && this._updatesCount > 0
${!this.alwaysExpand &&
(this._updatesCount > 0 || this._issuesCount > 0)
? html`
<span class="configuration-badge" slot="item-icon">
${this._updatesCount}
${this._updatesCount + this._issuesCount}
</span>
`
: ""}
<span class="item-text">${title}</span>
${this.alwaysExpand && this._updatesCount > 0
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
? html`
<span class="configuration-badge">${this._updatesCount}</span>
<span class="configuration-badge"
>${this._updatesCount + this._issuesCount}</span
>
`
: ""}
</paper-icon-item>

View File

@@ -314,7 +314,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
class="mdc-chip__icon mdc-chip__icon--trailing"
tabindex="-1"
role="button"
.label=${this.hass.localize("ui.components.target-picker.expand")}
.label=${this.hass.localize("ui.components.target-picker.remove")}
.path=${mdiClose}
hideTooltip
.id=${id}

View File

@@ -83,7 +83,7 @@ export class HaTextField extends TextFieldBase {
}
input {
text-align: var(--text-field-text-align);
text-align: var(--text-field-text-align, start);
}
/* Chrome, Safari, Edge, Opera */

View File

@@ -23,7 +23,7 @@ export class HaThemePicker extends LitElement {
return html`
<ha-select
.label=${this.label ||
this.hass!.localize("ui.components.theme_picker.theme")}
this.hass!.localize("ui.components.theme-picker.theme")}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
@@ -34,7 +34,7 @@ export class HaThemePicker extends LitElement {
>
<mwc-list-item value="remove"
>${this.hass!.localize(
"ui.components.theme_picker.no_theme"
"ui.components.theme-picker.no_theme"
)}</mwc-list-item
>
${Object.keys(this.hass!.themes.themes)

View File

@@ -41,7 +41,7 @@ export class HaYamlEditor extends LitElement {
try {
this._yaml =
value && !isEmpty(value)
? dump(value, { schema: this.yamlSchema })
? dump(value, { schema: this.yamlSchema, quotingType: '"' })
: "";
} catch (err: any) {
// eslint-disable-next-line no-console

View File

@@ -6,21 +6,19 @@ import {
Map,
Marker,
Polyline,
TileLayer,
} from "leaflet";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
LeafletModuleType,
replaceTileLayer,
setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import "./ha-entity-marker";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import "./ha-entity-marker";
const getEntityId = (entity: string | HaMapEntity): string =>
typeof entity === "string" ? entity : entity.entity_id;
@@ -60,8 +58,6 @@ export class HaMap extends ReactiveElement {
private Leaflet?: LeafletModuleType;
private _tileLayer?: TileLayer;
private _resizeObserver?: ResizeObserver;
private _mapItems: Array<Marker | Circle> = [];
@@ -142,12 +138,6 @@ export class HaMap extends ReactiveElement {
return;
}
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
this._tileLayer = replaceTileLayer(
this.Leaflet!,
this.leafletMap!,
this._tileLayer!,
darkMode
);
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
}
@@ -159,10 +149,7 @@ export class HaMap extends ReactiveElement {
this.shadowRoot!.append(map);
}
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
[this.leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
map,
darkMode
);
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map);
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
this._loaded = true;
}
@@ -466,6 +453,7 @@ export class HaMap extends ReactiveElement {
}
#map.dark {
background: #090909;
--map-filter: invert(0.9) hue-rotate(170deg) grayscale(0.7);
}
.light {
color: #000000;
@@ -473,6 +461,13 @@ export class HaMap extends ReactiveElement {
.dark {
color: #ffffff;
}
.leaflet-tile-pane {
filter: var(--map-filter);
}
.dark .leaflet-bar a {
background-color: var(--card-background-color, #1c1c1c);
color: #ffffff;
}
.leaflet-marker-draggable {
cursor: move !important;
}

View File

@@ -11,7 +11,8 @@ export interface ConfigEntry {
| "migration_error"
| "setup_retry"
| "not_loaded"
| "failed_unload";
| "failed_unload"
| "setup_in_progress";
supports_options: boolean;
supports_remove_device: boolean;
supports_unload: boolean;
@@ -28,12 +29,21 @@ export type ConfigEntryMutableParams = Partial<
>
>;
// https://github.com/home-assistant/core/blob/2286dea636fda001f03433ba14d7adbda43979e5/homeassistant/config_entries.py#L81
export const ERROR_STATES: ConfigEntry["state"][] = [
"migration_error",
"setup_error",
"setup_retry",
];
// https://github.com/home-assistant/core/blob/2286dea636fda001f03433ba14d7adbda43979e5/homeassistant/config_entries.py#L81
export const RECOVERABLE_STATES: ConfigEntry["state"][] = [
"not_loaded",
"loaded",
"setup_error",
"setup_retry",
];
export const getConfigEntries = (
hass: HomeAssistant,
filters?: { type?: "helper" | "integration"; domain?: string }

View File

@@ -6,16 +6,17 @@ import { DataEntryFlowProgress, DataEntryFlowStep } from "./data_entry_flow";
import { domainToName } from "./integration";
export const DISCOVERY_SOURCES = [
"usb",
"unignore",
"bluetooth",
"dhcp",
"homekit",
"ssdp",
"zeroconf",
"discovery",
"hassio",
"homekit",
"integration_discovery",
"mqtt",
"hassio",
"ssdp",
"unignore",
"usb",
"zeroconf",
];
export const ATTENTION_SOURCES = ["reauth"];

View File

@@ -1,10 +1,11 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import type { Store } from "home-assistant-js-websocket/dist/store";
import { computeStateName } from "../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry";
import type { HomeAssistant } from "../types";
import type { EntityRegistryEntry } from "./entity_registry";
import type { EntitySources } from "./entity_sources";
export interface DeviceRegistryEntry {
id: string;
@@ -20,7 +21,7 @@ export interface DeviceRegistryEntry {
area_id: string | null;
name_by_user: string | null;
entry_type: "service" | null;
disabled_by: string | null;
disabled_by: "user" | "integration" | "config_entry" | null;
configuration_url: string | null;
}
@@ -142,3 +143,23 @@ export const getDeviceEntityLookup = (
}
return deviceEntityLookup;
};
export const getDeviceIntegrationLookup = (
entitySources: EntitySources,
entities: EntityRegistryEntry[]
): Record<string, string[]> => {
const deviceIntegrations: Record<string, string[]> = {};
for (const entity of entities) {
const source = entitySources[entity.entity_id];
if (!source?.domain || entity.device_id === null) {
continue;
}
if (!deviceIntegrations[entity.device_id!]) {
deviceIntegrations[entity.device_id!] = [];
}
deviceIntegrations[entity.device_id!].push(source.domain);
}
return deviceIntegrations;
};

View File

@@ -13,15 +13,16 @@ export interface EntityRegistryEntry {
config_entry_id: string | null;
device_id: string | null;
area_id: string | null;
disabled_by: string | null;
hidden_by: string | null;
disabled_by: "user" | "device" | "integration" | "config_entry" | null;
hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
entity_category: "config" | "diagnostic" | null;
has_entity_name: boolean;
original_name?: string;
}
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
unique_id: string;
capabilities: Record<string, unknown>;
original_name?: string;
original_icon?: string;
device_class?: string;
original_device_class?: string;
@@ -37,6 +38,10 @@ export interface SensorEntityOptions {
unit_of_measurement?: string | null;
}
export interface NumberEntityOptions {
unit_of_measurement?: string | null;
}
export interface WeatherEntityOptions {
precipitation_unit?: string | null;
pressure_unit?: string | null;
@@ -155,3 +160,16 @@ export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) =>
entries.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
);
export const getEntityPlatformLookup = (
entities: EntityRegistryEntry[]
): Record<string, string> => {
const entityLookup = {};
for (const confEnt of entities) {
if (!confEnt.platform) {
continue;
}
entityLookup[confEnt.entity_id] = confEnt.platform;
}
return entityLookup;
};

View File

@@ -1,4 +1,4 @@
import { HassEntity } from "home-assistant-js-websocket";
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
import {
@@ -268,7 +268,8 @@ const processTimelineEntity = (
localize: LocalizeFunc,
language: FrontendLocaleData,
entityId: string,
states: EntityHistoryState[]
states: EntityHistoryState[],
current_state: HassEntity | undefined
): TimelineEntity => {
const data: TimelineState[] = [];
const first: EntityHistoryState = states[0];
@@ -292,7 +293,10 @@ const processTimelineEntity = (
}
return {
name: computeStateNameFromEntityAttributes(entityId, states[0].a),
name: computeStateNameFromEntityAttributes(
entityId,
current_state?.attributes || first.a
),
entity_id: entityId,
data,
};
@@ -300,7 +304,8 @@ const processTimelineEntity = (
const processLineChartEntities = (
unit,
entities: HistoryStates
entities: HistoryStates,
hassEntities: HassEntities
): LineChartUnit => {
const data: LineChartEntity[] = [];
@@ -349,9 +354,16 @@ const processLineChartEntities = (
processedStates.push(processedState);
}
const attributes =
entityId in hassEntities
? hassEntities[entityId].attributes
: "friendly_name" in first.a
? first.a
: undefined;
data.push({
domain,
name: computeStateNameFromEntityAttributes(entityId, first.a),
name: computeStateNameFromEntityAttributes(entityId, attributes || {}),
entity_id: entityId,
states: processedStates,
});
@@ -411,7 +423,13 @@ export const computeHistory = (
if (!unit) {
timelineDevices.push(
processTimelineEntity(localize, hass.locale, entityId, stateInfo)
processTimelineEntity(
localize,
hass.locale,
entityId,
stateInfo,
currentState
)
);
} else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) {
lineChartDevices[unit][entityId].push(...stateInfo);
@@ -424,7 +442,7 @@ export const computeHistory = (
});
const unitStates = Object.keys(lineChartDevices).map((unit) =>
processLineChartEntities(unit, lineChartDevices[unit])
processLineChartEntities(unit, lineChartDevices[unit], hass.states)
);
return { line: unitStates, timeline: timelineDevices };

96
src/data/repairs.ts Normal file
View File

@@ -0,0 +1,96 @@
import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { Store } from "home-assistant-js-websocket/dist/store";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
import type { DataEntryFlowStep } from "./data_entry_flow";
export interface RepairsIssue {
domain: string;
issue_domain?: string;
issue_id: string;
active: boolean;
is_fixable: boolean;
severity: "error" | "warning" | "critical";
breaks_in_ha_version?: string;
ignored: boolean;
created: string;
dismissed_version?: string;
learn_more_url?: string;
translation_key?: string;
translation_placeholders?: Record<string, string>;
}
export const severitySort = {
critical: 1,
error: 2,
warning: 3,
};
export const fetchRepairsIssues = (conn: Connection) =>
conn.sendMessagePromise<{ issues: RepairsIssue[] }>({
type: "repairs/list_issues",
});
export const ignoreRepairsIssue = async (
hass: HomeAssistant,
issue: RepairsIssue,
ignore: boolean
) =>
hass.callWS<string>({
type: "repairs/ignore_issue",
issue_id: issue.issue_id,
domain: issue.domain,
ignore,
});
export const createRepairsFlow = (
hass: HomeAssistant,
handler: string,
issue_id: string
) =>
hass.callApi<DataEntryFlowStep>("POST", "repairs/issues/fix", {
handler,
issue_id,
});
export const fetchRepairsFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi<DataEntryFlowStep>("GET", `repairs/issues/fix/${flowId}`);
export const handleRepairsFlowStep = (
hass: HomeAssistant,
flowId: string,
data: Record<string, any>
) =>
hass.callApi<DataEntryFlowStep>("POST", `repairs/issues/fix/${flowId}`, data);
export const deleteRepairsFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `repairs/issues/fix/${flowId}`);
const subscribeRepairsIssueUpdates = (
conn: Connection,
store: Store<{ issues: RepairsIssue[] }>
) =>
conn.subscribeEvents(
debounce(
() =>
fetchRepairsIssues(conn).then((repairs) =>
store.setState(repairs, true)
),
500,
true
),
"repairs_issue_registry_updated"
);
export const subscribeRepairsIssueRegistry = (
conn: Connection,
onChange: (repairs: { issues: RepairsIssue[] }) => void
) =>
createCollection<{ issues: RepairsIssue[] }>(
"_repairsIssueRegistry",
fetchRepairsIssues,
subscribeRepairsIssueUpdates,
conn,
onChange
);

View File

@@ -1,3 +1,8 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import type { DeviceRegistryEntry } from "./device_registry";
import type { EntitySources } from "./entity_sources";
export type Selector =
| ActionSelector
| AddonSelector
@@ -35,18 +40,22 @@ export interface AddonSelector {
};
}
export interface SelectorDevice {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
}
export interface SelectorEntity {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
}
export interface AreaSelector {
area: {
entity?: {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
entity?: SelectorEntity;
device?: SelectorDevice;
multiple?: boolean;
};
}
@@ -89,10 +98,7 @@ export interface DeviceSelector {
integration?: string;
manufacturer?: string;
model?: string;
entity?: {
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
entity?: SelectorEntity;
multiple?: boolean;
};
}
@@ -201,16 +207,8 @@ export interface StringSelector {
export interface TargetSelector {
target: {
entity?: {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
entity?: SelectorEntity;
device?: SelectorDevice;
};
}
@@ -227,3 +225,69 @@ export interface TimeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
time: {};
}
export const filterSelectorDevices = (
filterDevice: SelectorDevice,
device: DeviceRegistryEntry,
deviceIntegrationLookup: Record<string, string[]> | undefined
): boolean => {
const {
manufacturer: filterManufacturer,
model: filterModel,
integration: filterIntegration,
} = filterDevice;
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
return false;
}
if (filterModel && device.model !== filterModel) {
return false;
}
if (filterIntegration && deviceIntegrationLookup) {
if (!deviceIntegrationLookup?.[device.id]?.includes(filterIntegration)) {
return false;
}
}
return true;
};
export const filterSelectorEntities = (
filterEntity: SelectorEntity,
entity: HassEntity,
entitySources?: EntitySources
): boolean => {
const {
domain: filterDomain,
device_class: filterDeviceClass,
integration: filterIntegration,
} = filterEntity;
if (filterDomain) {
const entityDomain = computeStateDomain(entity);
if (
Array.isArray(filterDomain)
? !filterDomain.includes(entityDomain)
: entityDomain !== filterDomain
) {
return false;
}
}
if (
filterDeviceClass &&
entity.attributes.device_class !== filterDeviceClass
) {
return false;
}
if (
filterIntegration &&
entitySources?.[entity.entity_id]?.domain !== filterIntegration
) {
return false;
}
return true;
};

View File

@@ -1,7 +1,7 @@
import { Connection, getCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { LocalizeFunc } from "../../common/translations/localize";
import { HomeAssistant } from "../../types";
import { HomeAssistant, TranslationDict } from "../../types";
import { HassioAddonsInfo } from "../hassio/addon";
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
import { NetworkInfo } from "../hassio/network";
@@ -67,7 +67,7 @@ export interface Supervisor {
os: HassioHassOSInfo;
addon: HassioAddonsInfo;
store: SupervisorStore;
localize: LocalizeFunc;
localize: LocalizeFunc<TranslationDict["supervisor"]>;
}
export const supervisorApiWsRequest = <T>(

View File

@@ -0,0 +1,27 @@
import { SupportedBrandObj } from "../dialogs/config-flow/step-flow-pick-handler";
import type { HomeAssistant } from "../types";
export type SupportedBrandHandler = Record<string, string>;
export const getSupportedBrands = (hass: HomeAssistant) =>
hass.callWS<Record<string, SupportedBrandHandler>>({
type: "supported_brands",
});
export const getSupportedBrandsLookup = (
supportedBrands: Record<string, SupportedBrandHandler>
): Record<string, Partial<SupportedBrandObj>> => {
const supportedBrandsIntegrations: Record<
string,
Partial<SupportedBrandObj>
> = {};
for (const [d, domainBrands] of Object.entries(supportedBrands)) {
for (const [slug, name] of Object.entries(domainBrands)) {
supportedBrandsIntegrations[slug] = {
name,
supported_flows: [d],
};
}
}
return supportedBrandsIntegrations;
};

View File

@@ -39,7 +39,8 @@ export type TranslationCategory =
| "mfa_setup"
| "system_health"
| "device_class"
| "application_credentials";
| "application_credentials"
| "issues";
export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, "language");

View File

@@ -4,7 +4,7 @@ import {
mdiHomeCircleOutline,
mdiCancel,
} from "@mdi/js";
import { HomeAssistant } from "../types";
import { HomeAssistant, TranslationDict } from "../types";
import { Credential } from "./auth";
export const SYSTEM_GROUP_ID_ADMIN = "system-admin";
@@ -21,7 +21,7 @@ export interface User {
is_active: boolean;
local_only: boolean;
system_generated: boolean;
group_ids: string[];
group_ids: (keyof TranslationDict["groups"])[];
credentials: Credential[];
}
@@ -95,7 +95,12 @@ export const computeUserBadges = (
includeSystem: boolean
) => {
const labels: [string, string][] = [];
const translate = (key) => hass.localize(`ui.panel.config.users.${key}`);
const translate = (
key: Extract<
keyof TranslationDict["ui"]["panel"]["config"]["users"],
`is_${string}`
>
) => hass.localize(`ui.panel.config.users.${key}`);
if (user.is_owner) {
labels.push([OWNER_ICON, translate("is_owner")]);

View File

@@ -26,6 +26,7 @@ export interface ZHADevice {
power_source?: string;
area_id?: string;
device_type: string;
active_coordinator: boolean;
signature: any;
neighbors: Neighbor[];
pairing_status?: string;

View File

@@ -7,6 +7,7 @@ import {
handleConfigFlowStep,
} from "../../data/config_flow";
import { domainToName } from "../../data/integration";
import { getSupportedBrands } from "../../data/supported_brands";
import {
DataEntryFlowDialogParams,
loadDataEntryFlowDialog,
@@ -22,12 +23,14 @@ export const showConfigFlowDialog = (
showFlowDialog(element, dialogParams, {
loadDevicesAndAreas: true,
getFlowHandlers: async (hass) => {
const [integrations, helpers] = await Promise.all([
const [integrations, helpers, supportedBrands] = await Promise.all([
getConfigFlowHandlers(hass, "integration"),
getConfigFlowHandlers(hass, "helper"),
getSupportedBrands(hass),
hass.loadBackendTranslation("title", undefined, true),
]);
return { integrations, helpers };
return { integrations, helpers, supportedBrands };
},
createFlow: async (hass, handler) => {
const [step] = await Promise.all([

View File

@@ -10,12 +10,14 @@ import {
DataEntryFlowStepMenu,
DataEntryFlowStepProgress,
} from "../../data/data_entry_flow";
import { IntegrationManifest } from "../../data/integration";
import { HomeAssistant } from "../../types";
import type { IntegrationManifest } from "../../data/integration";
import type { SupportedBrandHandler } from "../../data/supported_brands";
import type { HomeAssistant } from "../../types";
export interface FlowHandlers {
integrations: string[];
helpers: string[];
supportedBrands: Record<string, SupportedBrandHandler>;
}
export interface FlowConfig {
loadDevicesAndAreas: boolean;

View File

@@ -1,5 +1,6 @@
import "@polymer/paper-item";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";

View File

@@ -14,19 +14,20 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { protocolIntegrationPicked } from "../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../common/navigate";
import "../../components/search-input";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-icon-next";
import { getConfigEntries } from "../../data/config_entries";
import "../../components/search-input";
import { domainToName } from "../../data/integration";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import { haStyleScrollbar } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url";
import { configFlowContentStyles } from "./styles";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { FlowHandlers } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
interface HandlerObj {
name: string;
@@ -35,6 +36,10 @@ interface HandlerObj {
is_helper?: boolean;
}
export interface SupportedBrandObj extends HandlerObj {
supported_flows: string[];
}
declare global {
// for fire event
interface HASSDomEvents {
@@ -63,11 +68,22 @@ class StepFlowPickHandler extends LitElement {
h: FlowHandlers,
filter?: string,
_localize?: LocalizeFunc
): [HandlerObj[], HandlerObj[]] => {
const integrations: HandlerObj[] = h.integrations.map((handler) => ({
name: domainToName(this.hass.localize, handler),
slug: handler,
}));
): [(HandlerObj | SupportedBrandObj)[], HandlerObj[]] => {
const integrations: (HandlerObj | SupportedBrandObj)[] =
h.integrations.map((handler) => ({
name: domainToName(this.hass.localize, handler),
slug: handler,
}));
for (const [domain, domainBrands] of Object.entries(h.supportedBrands)) {
for (const [slug, name] of Object.entries(domainBrands)) {
integrations.push({
slug,
name,
supported_flows: [domain],
});
}
}
if (filter) {
const options: Fuse.IFuseOptions<HandlerObj> = {
@@ -128,6 +144,7 @@ class StepFlowPickHandler extends LitElement {
width: `${this._width}px`,
height: `${this._height}px`,
})}
class="ha-scrollbar"
>
${addDeviceRows.length
? html`
@@ -238,27 +255,10 @@ class StepFlowPickHandler extends LitElement {
}
private async _handlerPicked(ev) {
const handler: HandlerObj = ev.currentTarget.handler;
const handler: HandlerObj | SupportedBrandObj = ev.currentTarget.handler;
if (handler.is_add) {
if (handler.slug === "zwave_js") {
const entries = await getConfigEntries(this.hass, {
domain: "zwave_js",
});
if (!entries.length) {
return;
}
showZWaveJSAddNodeDialog(this, {
entry_id: entries[0].entry_id,
});
} else if (handler.slug === "zha") {
navigate("/config/zha/add");
}
// This closes dialog.
fireEvent(this, "flow-update");
this._handleAddPicked(handler.slug);
return;
}
@@ -269,11 +269,43 @@ class StepFlowPickHandler extends LitElement {
return;
}
if ("supported_flows" in handler) {
const slug = handler.supported_flows[0];
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.supported_brand_flow",
{
supported_brand: handler.name,
flow_domain_name: domainToName(this.hass.localize, slug),
}
),
confirm: () => {
if (["zha", "zwave_js"].includes(slug)) {
this._handleAddPicked(slug);
return;
}
fireEvent(this, "handler-picked", {
handler: slug,
});
},
});
return;
}
fireEvent(this, "handler-picked", {
handler: handler.slug,
});
}
private async _handleAddPicked(slug: string): Promise<void> {
await protocolIntegrationPicked(this, this.hass, slug);
// This closes dialog.
fireEvent(this, "flow-update");
}
private _maybeSubmit(ev: KeyboardEvent) {
if (ev.key !== "Enter") {
return;
@@ -291,6 +323,7 @@ class StepFlowPickHandler extends LitElement {
static get styles(): CSSResultGroup {
return [
configFlowContentStyles,
haStyleScrollbar,
css`
img {
width: 40px;

View File

@@ -144,8 +144,6 @@ class DialogBox extends LitElement {
}
p {
margin: 0;
padding-top: 6px;
padding-bottom: 24px;
color: var(--primary-text-color);
}
.no-bottom-padding {
@@ -157,7 +155,6 @@ class DialogBox extends LitElement {
ha-dialog {
--mdc-dialog-heading-ink-color: var(--primary-text-color);
--mdc-dialog-content-ink-color: var(--primary-text-color);
--justify-action-buttons: space-between;
/* Place above other dialogs */
--dialog-z-index: 104;
}

View File

@@ -206,6 +206,7 @@ class MoreInfoMediaPlayer extends LitElement {
flex-wrap: wrap;
align-items: center;
--mdc-theme-primary: currentColor;
direction: ltr;
}
.basic-controls {
@@ -213,6 +214,15 @@ class MoreInfoMediaPlayer extends LitElement {
flex-grow: 1;
}
.volume {
direction: ltr;
}
.source-input,
.sound-input {
direction: var(--direction);
}
.volume,
.source-input,
.sound-input {
@@ -225,6 +235,9 @@ class MoreInfoMediaPlayer extends LitElement {
.sound-input ha-select {
margin-left: 10px;
flex-grow: 1;
margin-inline-start: 10px;
margin-inline-end: initial;
direction: var(--direction);
}
.tts {

View File

@@ -1,12 +1,13 @@
import "../../../components/ha-alert";
import "../../../components/ha-faded";
import "@material/mwc-button/mwc-button";
import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { BINARY_STATE_OFF } from "../../../common/const";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert";
import "../../../components/ha-checkbox";
import "../../../components/ha-circular-progress";
import "../../../components/ha-faded";
import "../../../components/ha-formfield";
import "../../../components/ha-markdown";
import { UNAVAILABLE_STATES } from "../../../data/entity";
@@ -21,7 +22,6 @@ import {
UPDATE_SUPPORT_SPECIFIC_VERSION,
} from "../../../data/update";
import type { HomeAssistant } from "../../../types";
import { BINARY_STATE_OFF } from "../../../common/const";
@customElement("more-info-update")
class MoreInfoUpdate extends LitElement {
@@ -114,7 +114,8 @@ class MoreInfoUpdate extends LitElement {
></ha-markdown>`
: ""}
${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP)
? html`<hr />
? html`
<hr />
<ha-formfield
.label=${this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup"
@@ -124,7 +125,8 @@ class MoreInfoUpdate extends LitElement {
checked
.disabled=${updateIsInstalling(this.stateObj)}
></ha-checkbox>
</ha-formfield> `
</ha-formfield>
`
: ""}
<hr />
<div class="actions">

View File

@@ -329,6 +329,9 @@ export class HaVoiceCommandDialog extends LitElement {
ha-icon-button {
color: var(--secondary-text-color);
margin-right: -24px;
margin-inline-end: -24px;
margin-inline-start: initial;
direction: var(--direction);
}
ha-icon-button[active] {
@@ -373,19 +376,25 @@ export class HaVoiceCommandDialog extends LitElement {
.message.user {
margin-left: 24px;
float: right;
margin-inline-start: 24px;
margin-inline-end: initial;
float: var(--float-end);
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--text-light-primary-color, var(--primary-text-color));
direction: var(--direction);
}
.message.hass {
margin-right: 24px;
float: left;
margin-inline-end: 24px;
margin-inline-start: initial;
float: var(--float-start);
border-bottom-left-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
direction: var(--direction);
}
.message a {

View File

@@ -178,9 +178,6 @@ class DialogAreaDetail extends LitElement {
return [
haStyleDialog,
css`
.form {
padding-bottom: 24px;
}
ha-textfield {
display: block;
margin-bottom: 16px;

View File

@@ -3,7 +3,7 @@ import "@material/mwc-list/mwc-list-item";
import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { HassEntities } from "home-assistant-js-websocket";
import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -23,6 +23,11 @@ import "../../../components/ha-menu-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tip";
import { CloudStatus } from "../../../data/cloud";
import {
RepairsIssue,
severitySort,
subscribeRepairsIssueRegistry,
} from "../../../data/repairs";
import {
checkForEntityUpdates,
filterUpdateEntitiesWithInstall,
@@ -31,13 +36,15 @@ import {
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import "../../../layouts/ha-app-layout";
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "../repairs/ha-config-repairs";
import "../updates/ha-config-updates";
import "./ha-config-navigation";
import "./ha-config-updates";
const randomTip = (hass: HomeAssistant, narrow: boolean) => {
const weighted: string[] = [];
@@ -104,7 +111,7 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
};
@customElement("ha-config-dashboard")
class HaConfigDashboard extends LitElement {
class HaConfigDashboard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
@@ -118,6 +125,11 @@ class HaConfigDashboard extends LitElement {
@state() private _tip?: string;
@state() private _repairsIssues: { issues: RepairsIssue[]; total: number } = {
issues: [],
total: 0,
};
private _pages = memoizeOne((clouStatus, isLoaded) => {
const pages: PageNavigation[] = [];
if (clouStatus && isLoaded) {
@@ -133,10 +145,34 @@ class HaConfigDashboard extends LitElement {
return [...pages, ...configSections.dashboard];
});
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
const repairsIssues = repairs.issues.filter((issue) => !issue.ignored);
this._repairsIssues = {
issues: repairsIssues
.sort((a, b) => severitySort[a.severity] - severitySort[b.severity])
.slice(0, repairsIssues.length === 3 ? repairsIssues.length : 2),
total: repairsIssues.length,
};
const integrations: Set<string> = new Set();
for (const issue of this._repairsIssues.issues) {
integrations.add(issue.domain);
}
this.hass.loadBackendTranslation("issues", [...integrations]);
}),
];
}
protected render(): TemplateResult {
const [canInstallUpdates, totalUpdates] =
const { updates: canInstallUpdates, total: totalUpdates } =
this._filterUpdateEntitiesWithInstall(this.hass.states);
const { issues: repairsIssues, total: totalRepairIssues } =
this._repairsIssues;
return html`
<ha-app-layout>
<app-header fixed slot="header">
@@ -174,26 +210,60 @@ class HaConfigDashboard extends LitElement {
.isWide=${this.isWide}
full-width
>
${canInstallUpdates.length
${repairsIssues.length || canInstallUpdates.length
? html`<ha-card outlined>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`<a class="button" href="/config/updates">
${this.hass.localize(
"ui.panel.config.updates.more_updates",
{
count: totalUpdates - canInstallUpdates.length,
}
)}
</a>`
${repairsIssues.length
? html`
<ha-config-repairs
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalRepairIssues}
.repairsIssues=${repairsIssues}
></ha-config-repairs>
${totalRepairIssues > repairsIssues.length
? html`
<a class="button" href="/config/repairs">
${this.hass.localize(
"ui.panel.config.repairs.more_repairs",
{
count:
totalRepairIssues - repairsIssues.length,
}
)}
</a>
`
: ""}
`
: ""}
${repairsIssues.length && canInstallUpdates.length
? html`<hr />`
: ""}
${canInstallUpdates.length
? html`
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`
<a class="button" href="/config/updates">
${this.hass.localize(
"ui.panel.config.updates.more_updates",
{
count:
totalUpdates - canInstallUpdates.length,
}
)}
</a>
`
: ""}
`
: ""}
</ha-card>`
: ""}
<ha-card outlined>
<ha-config-navigation
.hass=${this.hass}
@@ -220,13 +290,13 @@ class HaConfigDashboard extends LitElement {
}
private _filterUpdateEntitiesWithInstall = memoizeOne(
(entities: HassEntities): [UpdateEntity[], number] => {
(entities: HassEntities): { updates: UpdateEntity[]; total: number } => {
const updates = filterUpdateEntitiesWithInstall(entities);
return [
updates.slice(0, updates.length === 3 ? updates.length : 2),
updates.length,
];
return {
updates: updates.slice(0, updates.length === 3 ? updates.length : 2),
total: updates.length,
};
}
);
@@ -268,9 +338,12 @@ class HaConfigDashboard extends LitElement {
color: var(--primary-text-color);
}
a.button {
display: block;
color: var(--primary-color);
padding: 16px;
display: inline-block;
color: var(--primary-text-color);
padding: 6px 16px;
margin: 8px 16px 16px 16px;
border-radius: 32px;
border: 1px solid var(--divider-color);
}
.title {
font-size: 16px;
@@ -300,6 +373,16 @@ class HaConfigDashboard extends LitElement {
.keep-together {
display: inline-block;
}
hr {
height: 1px;
background-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
border: none;
margin-top: 0;
}
`,
];
}

View File

@@ -163,17 +163,27 @@ export class HaDeviceEntitiesCard extends LitElement {
if (this.hass) {
element.hass = this.hass;
const stateObj = this.hass.states[entry.entity_id];
const name = stripPrefixFromEntityName(
computeStateName(stateObj),
this.deviceName.toLowerCase()
);
if (entry.hidden_by) {
config.name = `${
name || computeStateName(stateObj)
} (${this.hass.localize("ui.panel.config.devices.entities.hidden")})`;
} else if (name) {
config.name = name;
let name = entry.name
? entry.name
: entry.has_entity_name
? entry.original_name || this.deviceName
: stripPrefixFromEntityName(
computeStateName(stateObj),
this.deviceName.toLowerCase()
);
if (!name) {
name = computeStateName(stateObj);
}
if (entry.hidden_by) {
name += ` (${this.hass.localize(
"ui.panel.config.devices.entities.hidden"
)})`;
}
config.name = name;
}
// @ts-ignore
element.entry = entry;

View File

@@ -30,7 +30,7 @@ export const getZHADeviceActions = async (
const actions: DeviceAction[] = [];
if (zhaDevice.device_type !== "Coordinator") {
if (!zhaDevice.active_coordinator) {
actions.push({
label: hass.localize("ui.dialogs.zha_device_info.buttons.reconfigure"),
action: () => showZHAReconfigureDeviceDialog(el, { device: zhaDevice }),
@@ -58,50 +58,50 @@ export const getZHADeviceActions = async (
);
}
if (zhaDevice.device_type !== "Coordinator") {
actions.push(
...[
{
label: hass.localize(
"ui.dialogs.zha_device_info.buttons.zigbee_information"
actions.push(
...[
{
label: hass.localize(
"ui.dialogs.zha_device_info.buttons.zigbee_information"
),
action: () => showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }),
},
{
label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"),
action: () => showZHAClusterDialog(el, { device: zhaDevice }),
},
{
label: hass.localize(
"ui.dialogs.zha_device_info.buttons.view_in_visualization"
),
action: () =>
navigate(`/config/zha/visualization/${zhaDevice!.device_reg_id}`),
},
]
);
if (!zhaDevice.active_coordinator) {
actions.push({
label: hass.localize("ui.dialogs.zha_device_info.buttons.remove"),
classes: "warning",
action: async () => {
const confirmed = await showConfirmationDialog(el, {
text: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
),
action: () =>
showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }),
},
{
label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"),
action: () => showZHAClusterDialog(el, { device: zhaDevice }),
},
{
label: hass.localize(
"ui.dialogs.zha_device_info.buttons.view_in_visualization"
),
action: () =>
navigate(`/config/zha/visualization/${zhaDevice!.device_reg_id}`),
},
{
label: hass.localize("ui.dialogs.zha_device_info.buttons.remove"),
classes: "warning",
action: async () => {
const confirmed = await showConfirmationDialog(el, {
text: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
),
});
});
if (!confirmed) {
return;
}
if (!confirmed) {
return;
}
await hass.callService("zha", "remove", {
ieee: zhaDevice.ieee,
});
await hass.callService("zha", "remove", {
ieee: zhaDevice.ieee,
});
history.back();
},
},
]
);
history.back();
},
});
}
return actions;

View File

@@ -100,19 +100,14 @@ export const getZwaveDeviceActions = async (
action: async () => {
if (
isNodeFirmwareUpdateInProgress ||
(await fetchZwaveNodeIsFirmwareUpdateInProgress(hass, device.id))
) {
showZWaveJUpdateFirmwareNodeDialog(el, {
device,
});
} else if (
await showConfirmationDialog(el, {
(await fetchZwaveNodeIsFirmwareUpdateInProgress(hass, device.id)) ||
(await showConfirmationDialog(el, {
text: hass.localize(
"ui.panel.config.zwave_js.update_firmware.warning"
),
dismissText: hass.localize("ui.common.no"),
confirmText: hass.localize("ui.common.yes"),
})
}))
) {
showZWaveJUpdateFirmwareNodeDialog(el, {
device,

View File

@@ -6,10 +6,14 @@ import "../../../../components/ha-area-picker";
import "../../../../components/ha-dialog";
import type { HaSwitch } from "../../../../components/ha-switch";
import "../../../../components/ha-textfield";
import { computeDeviceName } from "../../../../data/device_registry";
import {
computeDeviceName,
DeviceRegistryEntry,
} from "../../../../data/device_registry";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail";
import "../../../../components/ha-alert";
@customElement("dialog-device-registry-detail")
class DialogDeviceRegistryDetail extends LitElement {
@@ -21,11 +25,11 @@ class DialogDeviceRegistryDetail extends LitElement {
@state() private _params?: DeviceRegistryDetailDialogParams;
@property() public _areaId?: string | null;
@state() private _areaId!: string;
@state() private _disabledBy!: string | null;
@state() private _disabledBy!: DeviceRegistryEntry["disabled_by"];
@state() private _submitting?: boolean;
@state() private _submitting = false;
public async showDialog(
params: DeviceRegistryDetailDialogParams
@@ -33,7 +37,7 @@ class DialogDeviceRegistryDetail extends LitElement {
this._params = params;
this._error = undefined;
this._nameByUser = this._params.device.name_by_user || "";
this._areaId = this._params.device.area_id;
this._areaId = this._params.device.area_id || "";
this._disabledBy = this._params.device.disabled_by;
await this.updateComplete;
}
@@ -169,9 +173,6 @@ class DialogDeviceRegistryDetail extends LitElement {
haStyle,
haStyleDialog,
css`
.form {
padding-bottom: 24px;
}
mwc-button.warning {
margin-right: auto;
}

View File

@@ -344,7 +344,7 @@ export class HaConfigDevicePage extends LitElement {
.disabled=${device.disabled_by}
.label=${device.disabled_by
? this.hass.localize(
"ui.panel.config.devices.automation.create_disabled",
"ui.panel.config.devices.automation.create_disable",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
@@ -437,7 +437,7 @@ export class HaConfigDevicePage extends LitElement {
.disabled=${device.disabled_by}
.label=${device.disabled_by
? this.hass.localize(
"ui.panel.config.devices.scene.create_disabled",
"ui.panel.config.devices.scene.create_disable",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
@@ -530,7 +530,7 @@ export class HaConfigDevicePage extends LitElement {
.disabled=${device.disabled_by}
.label=${device.disabled_by
? this.hass.localize(
"ui.panel.config.devices.script.create_disabled",
"ui.panel.config.devices.script.create_disable",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
@@ -768,26 +768,27 @@ export class HaConfigDevicePage extends LitElement {
: ""
}
</ha-device-info-card>
${!this.narrow ? [automationCard, sceneCard, scriptCard] : ""}
${!this.narrow ? [automationCard, sceneCard, scriptCard] : ""}
</div>
<div class="column">
${["control", "sensor", "config", "diagnostic"].map((category) =>
// Make sure we render controls if no other cards will be rendered
entitiesByCategory[category].length > 0 ||
(entities.length === 0 && category === "control")
? html`
<ha-device-entities-card
.hass=${this.hass}
.header=${this.hass.localize(
`ui.panel.config.devices.entities.${category}`
)}
.deviceName=${deviceName}
.entities=${entitiesByCategory[category]}
.showHidden=${device.disabled_by !== null}
>
</ha-device-entities-card>
`
: ""
${(["control", "sensor", "config", "diagnostic"] as const).map(
(category) =>
// Make sure we render controls if no other cards will be rendered
entitiesByCategory[category].length > 0 ||
(entities.length === 0 && category === "control")
? html`
<ha-device-entities-card
.hass=${this.hass}
.header=${this.hass.localize(
`ui.panel.config.devices.entities.${category}`
)}
.deviceName=${deviceName}
.entities=${entitiesByCategory[category]}
.showHidden=${device.disabled_by !== null}
>
</ha-device-entities-card>
`
: ""
)}
</div>
<div class="column">

View File

@@ -8,6 +8,9 @@ export const energyCardStyles = css`
height: 32px;
width: 32px;
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
direction: var(--direction);
}
h3 {
margin-top: 24px;
@@ -24,6 +27,9 @@ export const energyCardStyles = css`
.row ha-icon,
.row img {
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
direction: var(--direction);
}
.row img {
height: 24px;

View File

@@ -13,6 +13,7 @@ import {
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
updateEntityRegistryEntry,
@@ -25,7 +26,7 @@ import type { HomeAssistant } from "../../../types";
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entry!: ExtEntityRegistryEntry;
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
@state() private _origEntityId!: string;
@@ -33,7 +34,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@state() private _areaId?: string | null;
@state() private _disabledBy!: string | null;
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
@state() private _hiddenBy!: string | null;
@@ -41,7 +42,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@state() private _device?: DeviceRegistryEntry;
@state() private _submitting?: boolean;
@state() private _submitting = false;
public async updateEntry(): Promise<void> {
this._submitting = true;
@@ -145,8 +146,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
></ha-textfield>
<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
.placeholder=${this._device?.area_id}
.value=${this._areaId || undefined}
.placeholder=${this._device?.area_id || undefined}
@value-changed=${this._areaPicked}
></ha-area-picker>
@@ -182,8 +183,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${this._device?.disabled_by ||
(this._disabledBy &&
.disabled=${!!this._device?.disabled_by ||
(this._disabledBy !== null &&
!(
this._disabledBy === "user" ||
this._disabledBy === "integration"
@@ -200,8 +201,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="hidden"
.checked=${this._hiddenBy !== null}
.disabled=${this._device?.disabled_by ||
(this._disabledBy &&
.disabled=${!!this._device?.disabled_by ||
(this._disabledBy !== null &&
!(
this._disabledBy === "user" ||
this._disabledBy === "integration"
@@ -218,8 +219,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="disabled"
.checked=${this._disabledBy !== null}
.disabled=${this._device?.disabled_by ||
(this._disabledBy &&
.disabled=${!!this._device?.disabled_by ||
(this._disabledBy !== null &&
!(
this._disabledBy === "user" ||
this._disabledBy === "integration"
@@ -302,3 +303,9 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-registry-basic-editor": HaEntityRegistryBasicEditor;
}
}

View File

@@ -53,6 +53,7 @@ import {
updateDeviceRegistryEntry,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
fetchEntityRegistry,
@@ -105,6 +106,10 @@ const OVERRIDE_DEVICE_CLASSES = {
],
};
const OVERRIDE_NUMBER_UNITS = {
temperature: ["°C", "°F", "K"],
};
const OVERRIDE_SENSOR_UNITS = {
temperature: ["°C", "°F", "K"],
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
@@ -124,7 +129,7 @@ const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"];
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entry!: ExtEntityRegistryEntry;
@property({ type: Object }) public entry!: ExtEntityRegistryEntry;
@state() private _name!: string;
@@ -138,9 +143,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _areaId?: string | null;
@state() private _disabledBy!: string | null;
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
@state() private _hiddenBy!: string | null;
@state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];
@state() private _device?: DeviceRegistryEntry;
@@ -235,7 +240,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
}
if (domain === "sensor") {
if (domain === "number" || domain === "sensor") {
const stateObj: HassEntity | undefined =
this.hass.states[this.entry.entity_id];
this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement;
@@ -361,6 +366,31 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
</ha-select>
`
: ""}
${domain === "number" &&
this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
OVERRIDE_NUMBER_UNITS[this._deviceClass]?.includes(
stateObj?.attributes.unit_of_measurement
)
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.unit_of_measurement"
)}
.value=${stateObj.attributes.unit_of_measurement}
naturalMenuWidth
fixedMenuPosition
@selected=${this._unitChanged}
@closed=${stopPropagation}
>
${OVERRIDE_NUMBER_UNITS[this._deviceClass].map(
(unit: string) => html`
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
`
)}
</ha-select>
`
: ""}
${domain === "sensor" &&
this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
@@ -601,9 +631,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
this._device?.disabled_by ||
(this._disabledBy &&
.disabled=${(this._hiddenBy !== null &&
this._hiddenBy !== "user") ||
!!this._device?.disabled_by ||
(this._disabledBy !== null &&
this._disabledBy !== "user" &&
this._disabledBy !== "integration")}
@change=${this._viewStatusChanged}
@@ -861,10 +892,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
params.hidden_by = this._hiddenBy;
}
if (
domain === "sensor" &&
(domain === "number" || domain === "number") &&
stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement
) {
params.options_domain = "sensor";
params.options_domain = domain;
params.options = { unit_of_measurement: this._unit_of_measurement };
}
if (
@@ -1023,12 +1054,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
bottom: 0;
width: 100%;
box-sizing: border-box;
border-top: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
display: flex;
justify-content: space-between;
padding: 8px;
padding-bottom: max(env(safe-area-inset-bottom), 8px);
padding: 0 24px 24px 24px;
justify-content: flex-end;
padding-bottom: max(env(safe-area-inset-bottom), 24px);
background-color: var(--mdc-theme-surface, #fff);
}
ha-select {

View File

@@ -15,6 +15,7 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoize from "memoize-one";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
@@ -85,11 +86,11 @@ export interface EntityRow extends StateEntity {
export class HaConfigEntities extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public narrow!: boolean;
@property({ type: Boolean }) public narrow!: boolean;
@property() public route!: Route;
@property({ attribute: false }) public route!: Route;
@state() private _entities?: EntityRegistryEntry[];
@@ -174,7 +175,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
type: "icon",
template: (_, entry: EntityRow) => html`
<ha-state-icon
.title=${entry.entity?.state}
title=${ifDefined(entry.entity?.state)}
slot="item-icon"
.state=${entry.entity}
></ha-state-icon>
@@ -237,12 +238,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
hidden: narrow || !showDisabled,
filterable: true,
width: "15%",
template: (disabled_by) =>
this.hass.localize(
`ui.panel.config.devices.disabled_by.${disabled_by}`
) ||
disabled_by ||
"—",
template: (disabled_by: EntityRegistryEntry["disabled_by"]) =>
disabled_by === null
? "—"
: this.hass.localize(`config_entry.disabled_by.${disabled_by}`),
},
status: {
title: this.hass.localize(
@@ -736,6 +735,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
readonly: true,
selectable: false,
entity_category: null,
has_entity_name: false,
});
}
if (changed) {
@@ -1011,3 +1011,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-entities": HaConfigEntities;
}
}

View File

@@ -6,9 +6,9 @@ import {
mdiCog,
mdiDatabase,
mdiDevices,
mdiHeart,
mdiInformation,
mdiInformationOutline,
mdiLifebuoy,
mdiLightningBolt,
mdiMapMarkerRadius,
mdiMathLog,
@@ -267,6 +267,12 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiUpdate,
iconColor: "#3B808E",
},
{
path: "/config/repairs",
translationKey: "repairs",
iconPath: mdiLifebuoy,
iconColor: "#5c995c",
},
{
component: "logs",
path: "/config/logs",
@@ -315,13 +321,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#301A8E",
component: "hassio",
},
{
path: "/config/system_health",
translationKey: "system_health",
iconPath: mdiHeart,
iconColor: "#507FfE",
components: ["system_health", "hassio"],
},
],
about: [
{
@@ -440,13 +439,13 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-section-storage",
load: () => import("./storage/ha-config-section-storage"),
},
system_health: {
tag: "ha-config-system-health",
load: () => import("./system-health/ha-config-system-health"),
},
updates: {
tag: "ha-config-section-updates",
load: () => import("./core/ha-config-section-updates"),
load: () => import("./updates/ha-config-section-updates"),
},
repairs: {
tag: "ha-config-repairs-dashboard",
load: () => import("./repairs/ha-config-repairs-dashboard"),
},
users: {
tag: "ha-config-users",

View File

@@ -89,7 +89,7 @@ class DialogHardwareAvailable extends LitElement implements HassDialog {
)}
</h2>
<ha-icon-button
.label=${this.hass.localize("common.close")}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
dialogAction="close"
></ha-icon-button>
@@ -97,7 +97,9 @@ class DialogHardwareAvailable extends LitElement implements HassDialog {
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this.hass.localize("common.search")}
.label=${this.hass.localize(
"ui.panel.config.hardware.available_hardware.search"
)}
>
</search-input>
</div>

View File

@@ -202,7 +202,7 @@ class HaConfigHardware extends LitElement {
title: this.hass.localize("ui.panel.config.hardware.reboot_host"),
text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"),
confirmText: this.hass.localize("ui.panel.config.hardware.reboot_host"),
dismissText: this.hass.localize("common.cancel"),
dismissText: this.hass.localize("ui.common.cancel"),
});
if (!confirmed) {
@@ -236,7 +236,7 @@ class HaConfigHardware extends LitElement {
"ui.panel.config.hardware.shutdown_host_confirm"
),
confirmText: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
dismissText: this.hass.localize("common.cancel"),
dismissText: this.hass.localize("ui.common.cancel"),
});
if (!confirmed) {

View File

@@ -14,7 +14,8 @@ import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize";
@@ -49,6 +50,10 @@ import {
fetchIntegrationManifests,
IntegrationManifest,
} from "../../../data/integration";
import {
getSupportedBrands,
getSupportedBrandsLookup,
} from "../../../data/supported_brands";
import { scanUSBDevices } from "../../../data/usb";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import {
@@ -677,49 +682,84 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
if (!domain) {
return;
}
const handlers = await getConfigFlowHandlers(this.hass, "integration");
if (!handlers.includes(domain)) {
if (HELPER_DOMAINS.includes(domain)) {
navigate(`/config/helpers/add?domain=${domain}`, {
replace: true,
});
// Integration exists, so we can just create a flow
if (handlers.includes(domain)) {
const localize = await localizePromise;
if (
!(await showConfirmationDialog(this, {
title: localize("ui.panel.config.integrations.confirm_new", {
integration: domainToName(localize, domain),
}),
}))
) {
return;
}
const helpers = await getConfigFlowHandlers(this.hass, "helper");
if (helpers.includes(domain)) {
navigate(`/config/helpers/add?domain=${domain}`, {
replace: true,
});
return;
}
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
showConfigFlowDialog(this, {
dialogClosedCallback: () => {
this._handleFlowUpdated();
},
startFlowHandler: domain,
manifest: this._manifests[domain],
showAdvanced: this.hass.userData?.showAdvanced,
});
}
const supportedBrands = await getSupportedBrands(this.hass);
const supportedBrandsIntegrations =
getSupportedBrandsLookup(supportedBrands);
// Supported brand exists, so we can just create a flow
if (Object.keys(supportedBrandsIntegrations).includes(domain)) {
const brand = supportedBrandsIntegrations[domain];
const slug = brand.supported_flows![0];
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.no_config_flow"
"ui.panel.config.integrations.config_flow.supported_brand_flow",
{
supported_brand: brand.name,
flow_domain_name: domainToName(this.hass.localize, slug),
}
),
confirm: () => {
if (["zha", "zwave_js"].includes(slug)) {
protocolIntegrationPicked(this, this.hass, slug);
return;
}
fireEvent(this, "handler-picked", {
handler: slug,
});
},
});
return;
}
// If not an integration or supported brand, try helper else show alert
if (HELPER_DOMAINS.includes(domain)) {
navigate(`/config/helpers/add?domain=${domain}`, {
replace: true,
});
return;
}
const localize = await localizePromise;
if (
!(await showConfirmationDialog(this, {
title: localize("ui.panel.config.integrations.confirm_new", {
integration: domainToName(localize, domain),
}),
}))
) {
const helpers = await getConfigFlowHandlers(this.hass, "helper");
if (helpers.includes(domain)) {
navigate(`/config/helpers/add?domain=${domain}`, {
replace: true,
});
return;
}
showConfigFlowDialog(this, {
dialogClosedCallback: () => {
this._handleFlowUpdated();
},
startFlowHandler: domain,
manifest: this._manifests[domain],
showAdvanced: this.hass.userData?.showAdvanced,
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.no_config_flow"
),
});
}

View File

@@ -7,7 +7,7 @@ import {
mdiDotsVertical,
mdiOpenInNew,
} from "@mdi/js";
import "@polymer/paper-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox";
import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
@@ -31,6 +31,7 @@ import {
reloadConfigEntry,
updateConfigEntry,
ERROR_STATES,
RECOVERABLE_STATES,
} from "../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics";
@@ -63,13 +64,15 @@ export class HaIntegrationCard extends LitElement {
@property() public domain!: string;
@property() public items!: ConfigEntryExtended[];
@property({ attribute: false }) public items!: ConfigEntryExtended[];
@property() public manifest?: IntegrationManifest;
@property({ attribute: false }) public manifest?: IntegrationManifest;
@property() public entityRegistryEntries!: EntityRegistryEntry[];
@property({ attribute: false })
public entityRegistryEntries!: EntityRegistryEntry[];
@property() public deviceRegistryEntries!: DeviceRegistryEntry[];
@property({ attribute: false })
public deviceRegistryEntries!: DeviceRegistryEntry[];
@property() public selectedConfigEntryId?: string;
@@ -178,7 +181,7 @@ export class HaIntegrationCard extends LitElement {
const services = this._getServices(item, this.deviceRegistryEntries);
const entities = this._getEntities(item, this.entityRegistryEntries);
let stateText: [string, ...unknown[]] | undefined;
let stateText: Parameters<typeof this.hass.localize> | undefined;
let stateTextExtra: TemplateResult | string | undefined;
if (item.disabled_by) {
@@ -224,7 +227,7 @@ export class HaIntegrationCard extends LitElement {
for (const [items, localizeKey] of [
[devices, "devices"],
[services, "services"],
] as [DeviceRegistryEntry[], string][]) {
] as const) {
if (items.length === 0) {
continue;
}
@@ -366,7 +369,7 @@ export class HaIntegrationCard extends LitElement {
</a>`
: ""}
${!item.disabled_by &&
(item.state === "loaded" || item.state === "setup_retry") &&
RECOVERABLE_STATES.includes(item.state) &&
item.supports_unload &&
item.source !== "system"
? html`<mwc-list-item @request-selected=${this._handleReload}>

View File

@@ -12,13 +12,13 @@ import { brandsUrl } from "../../../util/brands-url";
export class HaIntegrationHeader extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public banner!: string;
@property() public banner?: string;
@property() public localizedDomainName?: string;
@property() public domain!: string;
@property() public label!: string;
@property() public label?: string;
@property({ attribute: false }) public manifest?: IntegrationManifest;

View File

@@ -401,7 +401,7 @@ class DialogZWaveJSAddNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "finished"
@@ -451,7 +451,7 @@ class DialogZWaveJSAddNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "provisioned"
@@ -469,7 +469,7 @@ class DialogZWaveJSAddNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>`
: ""}
</ha-dialog>

View File

@@ -120,7 +120,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
)}
</mwc-button>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}
@@ -140,7 +140,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}
@@ -160,7 +160,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}
@@ -180,7 +180,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}

View File

@@ -166,7 +166,7 @@ class DialogZWaveJSHealNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}
@@ -186,7 +186,7 @@ class DialogZWaveJSHealNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}

View File

@@ -85,7 +85,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}
@@ -105,7 +105,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}
@@ -125,7 +125,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}

View File

@@ -110,7 +110,7 @@ class DialogZWaveJSRemoveNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}
@@ -132,7 +132,7 @@ class DialogZWaveJSRemoveNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: ``}

View File

@@ -102,7 +102,7 @@ export class SystemLogCard extends LitElement {
<paper-item @click=${this._openLog} .logItem=${item}>
<paper-item-body two-line>
<div class="row">${item.message[0]}</div>
<div secondary>
<div class="row-secondary" secondary>
${this._timestamp(item)}
${html`(<span class=${item.level.toLowerCase()}
>${this.hass.localize(
@@ -209,6 +209,11 @@ export class SystemLogCard extends LitElement {
.empty-content {
direction: var(--direction);
}
.row-secondary {
direction: var(--direction);
text-align: left;
}
`;
}
}

View File

@@ -0,0 +1,91 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog";
import type { NetworkInterface } from "../../../data/hassio/network";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { IPDetailDialogParams } from "./show-ip-detail-dialog";
@customElement("dialog-ip-detail")
class DialogIPDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: IPDetailDialogParams;
@state() private _interface?: NetworkInterface;
public showDialog(params: IPDetailDialogParams): void {
this._params = params;
this._interface = this._params.interface;
}
public closeDialog() {
this._params = undefined;
this._interface = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._interface) {
return html``;
}
const ipv4 = this._interface.ipv4;
const ipv6 = this._interface.ipv6;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(this.hass, "IP Information")}
>
${ipv4
? html`
<div>
<h3>IPv4</h3>
${ipv4.address
? html`<div>IP Address: ${ipv4.address?.join(", ")}</div>`
: ""}
${ipv4.gateway ? html`<div>Gateway: ${ipv4.gateway}</div>` : ""}
${ipv4.method ? html`<div>Method: ${ipv4.method}</div>` : ""}
${ipv4.nameservers?.length
? html`
<div>Name Servers: ${ipv4.nameservers?.join(", ")}</div>
`
: ""}
</div>
`
: ""}
${ipv6
? html`
<div>
<h3>IPv6</h3>
${ipv6.address
? html`<div>IP Address: ${ipv6.address?.join(", ")}</div>`
: ""}
${ipv6.gateway ? html`<div>Gateway: ${ipv6.gateway}</div>` : ""}
${ipv6.method ? html`<div>Method: ${ipv6.method}</div>` : ""}
${ipv6.nameservers?.length
? html`
<div>Name Servers: ${ipv6.nameservers?.join(", ")}</div>
`
: ""}
</div>
`
: ""}
</ha-dialog>
`;
}
static styles: CSSResultGroup = haStyleDialog;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-ip-detail": DialogIPDetail;
}
}

View File

@@ -0,0 +1,19 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { NetworkInterface } from "../../../data/hassio/network";
export interface IPDetailDialogParams {
interface?: NetworkInterface;
}
export const loadIPDetailDialog = () => import("./dialog-ip-detail");
export const showIPDetailDialog = (
element: HTMLElement,
dialogParams: IPDetailDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-ip-detail",
dialogImport: loadIPDetailDialog,
dialogParams,
});
};

View File

@@ -1,13 +1,16 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list";
import { ActionDetail } from "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { mdiDotsVertical } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-formfield";
@@ -29,7 +32,7 @@ import {
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-card";
import { showIPDetailDialog } from "./show-ip-detail-dialog";
const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -236,9 +239,25 @@ export class HassioNetwork extends LitElement {
</ha-circular-progress>`
: this.hass.localize("ui.common.save")}
</mwc-button>
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button
slot="trigger"
.label=${"ui.common.menu"}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item>IP Information</mwc-list-item>
</ha-button-menu>
</div>`;
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
showIPDetailDialog(this, { interface: this._interface });
break;
}
}
private _selectAP(event) {
this._wifiConfiguration = event.currentTarget.ap;
this._dirty = true;

View File

@@ -454,9 +454,6 @@ class DialogPersonDetail extends LitElement {
return [
haStyleDialog,
css`
.form {
padding-bottom: 24px;
}
ha-picture-upload,
ha-textfield {
display: block;

View File

@@ -0,0 +1,65 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-card";
import { createCloseHeading } from "../../../components/ha-dialog";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import "./integrations-startup-time";
@customElement("dialog-integration-startup")
class DialogIntegrationStartup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
public showDialog(): void {
this._opened = true;
}
public closeDialog() {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._opened) {
return html``;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.repairs.integration_startup_time")
)}
@closed=${this.closeDialog}
>
<integrations-startup-time
.hass=${this.hass}
narrow
></integrations-startup-time>
</ha-dialog>
`;
}
static styles: CSSResultGroup = [
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-integration-startup": DialogIntegrationStartup;
}
}

View File

@@ -0,0 +1,166 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-markdown";
import { ignoreRepairsIssue, RepairsIssue } from "../../../data/repairs";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { RepairsIssueDialogParams } from "./show-repair-issue-dialog";
@customElement("dialog-repairs-issue")
class DialogRepairsIssue extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _issue?: RepairsIssue;
@state() private _params?: RepairsIssueDialogParams;
public showDialog(params: RepairsIssueDialogParams): void {
this._params = params;
this._issue = this._params.issue;
}
public closeDialog() {
if (this._params?.dialogClosedCallback) {
this._params.dialogClosedCallback();
}
this._params = undefined;
this._issue = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._issue) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
`component.${this._issue.domain}.issues.${
this._issue.translation_key || this._issue.issue_id
}.title`,
this._issue.translation_placeholders || {}
) || this.hass!.localize("ui.panel.config.repairs.dialog.title")
)}
>
<div>
${this._issue.breaks_in_ha_version
? html`
<ha-alert alert-type="error">
${this.hass.localize(
"ui.panel.config.repairs.dialog.breaks_in_version",
{ version: this._issue.breaks_in_ha_version }
)}
</ha-alert>
`
: ""}
<ha-markdown
allowsvg
breaks
.content=${this.hass.localize(
`component.${this._issue.domain}.issues.${
this._issue.translation_key || this._issue.issue_id
}.description`,
this._issue.translation_placeholders
)}
></ha-markdown>
${this._issue.dismissed_version
? html`
<br /><span class="dismissed">
${this.hass.localize(
"ui.panel.config.repairs.dialog.ignored_in_version",
{ version: this._issue.dismissed_version }
)}</span
>
`
: ""}
<div class="secondary">
<span class=${this._issue.severity}
>${this.hass.localize(
`ui.panel.config.repairs.${this._issue.severity}`
)}
</span>
-
${this._issue.created
? new Date(this._issue.created).toLocaleDateString(
this.hass.language
)
: ""}
</div>
</div>
${this._issue.learn_more_url
? html`
<a
href=${this._issue.learn_more_url}
target="_blank"
slot="primaryAction"
rel="noopener noreferrer"
>
<mwc-button
.label=${this.hass!.localize(
"ui.panel.config.repairs.dialog.learn"
)}
></mwc-button>
</a>
`
: ""}
<mwc-button
slot="secondaryAction"
.label=${this._issue!.ignored
? this.hass!.localize("ui.panel.config.repairs.dialog.unignore")
: this.hass!.localize("ui.panel.config.repairs.dialog.ignore")}
@click=${this._ignoreIssue}
></mwc-button>
</ha-dialog>
`;
}
private _ignoreIssue() {
ignoreRepairsIssue(this.hass, this._issue!, !this._issue!.ignored);
this.closeDialog();
}
static styles: CSSResultGroup = [
haStyleDialog,
css`
ha-alert {
margin-bottom: 16px;
display: block;
}
a {
text-decoration: none;
}
.dismissed {
font-style: italic;
}
.secondary {
margin-top: 8px;
text-align: right;
color: var(--secondary-text-color);
}
.error,
.critical {
color: var(--error-color);
}
.warning {
color: var(--warning-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-repairs-issue": DialogRepairsIssue;
}
}

View File

@@ -1,17 +1,15 @@
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiContentCopy } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket/dist/types";
import "@material/mwc-button/mwc-button";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { fireEvent } from "../../../common/dom/fire_event";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import { subscribePollingCollection } from "../../../common/util/subscribe-polling";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-metric";
import { fetchHassioStats, HassioStats } from "../../../data/hassio/common";
import {
@@ -25,12 +23,11 @@ import {
SystemHealthInfo,
} from "../../../data/system_health";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import "./integrations-card";
import "../../../components/ha-circular-progress";
const sortKeys = (a: string, b: string) => {
if (a === "homeassistant") {
@@ -53,28 +50,40 @@ export const UNHEALTHY_REASON_URL = {
privileged: "/more-info/unsupported/privileged",
};
@customElement("ha-config-system-health")
class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
@customElement("dialog-system-information")
class DialogSystemInformation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _info?: SystemHealthInfo;
@state() private _supervisorStats?: HassioStats;
@state() private _systemInfo?: SystemHealthInfo;
@state() private _resolutionInfo?: HassioResolution;
@state() private _supervisorStats?: HassioStats;
@state() private _coreStats?: HassioStats;
@state() private _error?: { code: string; message: string };
@state() private _opened = false;
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
private _subscriptions?: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>>;
public showDialog(): void {
this._opened = true;
this.hass!.loadBackendTranslation("system_health");
this._subscribe();
}
public closeDialog() {
this._opened = false;
this._unsubscribe();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _subscribe(): void {
const subs: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> = [];
if (isComponentLoaded(this.hass, "system_health")) {
subs.push(
subscribeSystemHealthInfo(this.hass!, (info) => {
this._info = info;
this._systemInfo = info;
})
);
}
@@ -93,149 +102,51 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
10000
)
);
fetchHassioResolution(this.hass).then((data) => {
this._resolutionInfo = data;
});
}
return subs;
this._subscriptions = subs;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
private _unsubscribe() {
while (this._subscriptions?.length) {
const unsub = this._subscriptions.pop()!;
if (unsub instanceof Promise) {
unsub.then((unsubFunc) => unsubFunc());
} else {
unsub();
}
}
this._subscriptions = undefined;
this.hass!.loadBackendTranslation("system_health");
this._systemInfo = undefined;
this._resolutionInfo = undefined;
this._coreStats = undefined;
this._supervisorStats = undefined;
}
protected render(): TemplateResult {
const sections: TemplateResult[] = [];
if (!this._info) {
sections.push(
html`
<div class="loading-container">
<ha-circular-progress active></ha-circular-progress>
</div>
`
);
} else {
const domains = Object.keys(this._info).sort(sortKeys);
for (const domain of domains) {
const domainInfo = this._info[domain];
const keys: TemplateResult[] = [];
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (
domainInfo.info[key] &&
typeof domainInfo.info[key] === "object"
) {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
value = html`
<ha-circular-progress active size="tiny"></ha-circular-progress>
`;
} else if (info.type === "failed") {
value = html`
<span class="error">${info.error}</span>${!info.more_info
? ""
: html`
<a
href=${info.more_info}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.info.system_health.more_info"
)}
</a>
`}
`;
} else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.locale);
}
} else {
value = domainInfo.info[key];
}
keys.push(html`
<tr>
<td>
${this.hass.localize(
`component.${domain}.system_health.info.${key}`
) || key}
</td>
<td>${value}</td>
</tr>
`);
}
if (domain !== "homeassistant") {
sections.push(
html`
<div class="card-header">
<h3>${domainToName(this.hass.localize, domain)}</h3>
${!domainInfo.manage_url
? ""
: html`
<a class="manage" href=${domainInfo.manage_url}>
<mwc-button>
${this.hass.localize(
"ui.panel.config.info.system_health.manage"
)}
</mwc-button>
</a>
`}
</div>
`
);
}
sections.push(html`
<table>
${keys}
</table>
`);
}
if (!this._opened) {
return html``;
}
const sections = this._getSections();
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/system"
.header=${this.hass.localize("ui.panel.config.system_health.caption")}
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.repairs.system_information")
)}
>
${this._error
? html`
<ha-alert alert-type="error"
>${this._error.message || this._error.code}</ha-alert
>
`
: ""}
${this._info
? html`
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._copyInfo}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.panel.config.info.copy_menu")}
.path=${mdiContentCopy}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize("ui.panel.config.info.copy_raw")}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize("ui.panel.config.info.copy_github")}
</mwc-list-item>
</ha-button-menu>
`
: ""}
<div class="content">
<div>
${this._resolutionInfo
? html`${this._resolutionInfo.unhealthy.length
? html`<ha-alert alert-type="error">
@@ -265,66 +176,63 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
: ""} `
: ""}
<ha-card outlined>
<div class="card-content">${sections}</div>
</ha-card>
<div>${sections}</div>
${!this._coreStats && !this._supervisorStats
? ""
: html`
<ha-card outlined>
<div class="card-content">
${this._coreStats
? html`
<h3>
${this.hass.localize(
"ui.panel.config.system_health.core_stats"
)}
</h3>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.cpu_usage"
)}
.value=${this._coreStats.cpu_percent}
></ha-metric>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.ram_usage"
)}
.value=${this._coreStats.memory_percent}
></ha-metric>
`
: ""}
${this._supervisorStats
? html`
<h3>
${this.hass.localize(
"ui.panel.config.system_health.supervisor_stats"
)}
</h3>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.cpu_usage"
)}
.value=${this._supervisorStats.cpu_percent}
></ha-metric>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.ram_usage"
)}
.value=${this._supervisorStats.memory_percent}
></ha-metric>
`
: ""}
</div>
</ha-card>
<div>
${this._coreStats
? html`
<h3>
${this.hass.localize(
"ui.panel.config.system_health.core_stats"
)}
</h3>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.cpu_usage"
)}
.value=${this._coreStats.cpu_percent}
></ha-metric>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.ram_usage"
)}
.value=${this._coreStats.memory_percent}
></ha-metric>
`
: ""}
${this._supervisorStats
? html`
<h3>
${this.hass.localize(
"ui.panel.config.system_health.supervisor_stats"
)}
</h3>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.cpu_usage"
)}
.value=${this._supervisorStats.cpu_percent}
></ha-metric>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.ram_usage"
)}
.value=${this._supervisorStats.memory_percent}
></ha-metric>
`
: ""}
</div>
`}
<integrations-card
.hass=${this.hass}
.narrow=${this.narrow}
></integrations-card>
</div>
</hass-subpage>
<mwc-button
slot="primaryAction"
.label=${this.hass.localize("ui.panel.config.repairs.copy")}
@click=${this._copyInfo}
></mwc-button>
</ha-dialog>
`;
}
@@ -386,17 +294,111 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
});
}
private async _copyInfo(ev: CustomEvent<ActionDetail>): Promise<void> {
const github = ev.detail.index === 1;
private _getSections(): TemplateResult[] {
const sections: TemplateResult[] = [];
if (!this._systemInfo) {
sections.push(
html`
<div class="loading-container">
<ha-circular-progress active></ha-circular-progress>
</div>
`
);
} else {
const domains = Object.keys(this._systemInfo).sort(sortKeys);
for (const domain of domains) {
const domainInfo = this._systemInfo[domain];
const keys: TemplateResult[] = [];
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (
domainInfo.info[key] &&
typeof domainInfo.info[key] === "object"
) {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
value = html`
<ha-circular-progress active size="tiny"></ha-circular-progress>
`;
} else if (info.type === "failed") {
value = html`
<span class="error">${info.error}</span>${!info.more_info
? ""
: html`
<a
href=${info.more_info}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.info.system_health.more_systemInfo"
)}
</a>
`}
`;
} else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.locale);
}
} else {
value = domainInfo.info[key];
}
keys.push(html`
<tr>
<td>
${this.hass.localize(
`component.${domain}.system_health.info.${key}`
) || key}
</td>
<td>${value}</td>
</tr>
`);
}
if (domain !== "homeassistant") {
sections.push(
html`
<div class="card-header">
<h3>${domainToName(this.hass.localize, domain)}</h3>
${!domainInfo.manage_url
? ""
: html`
<a class="manage" href=${domainInfo.manage_url}>
<mwc-button>
${this.hass.localize(
"ui.panel.config.info.system_health.manage"
)}
</mwc-button>
</a>
`}
</div>
`
);
}
sections.push(html`
<table>
${keys}
</table>
`);
}
}
return sections;
}
private async _copyInfo(): Promise<void> {
let haContent: string | undefined;
const domainParts: string[] = [];
for (const domain of Object.keys(this._info!).sort(sortKeys)) {
const domainInfo = this._info![domain];
for (const domain of Object.keys(this._systemInfo!).sort(sortKeys)) {
const domainInfo = this._systemInfo![domain];
let first = true;
const parts = [
`${
github && domain !== "homeassistant"
domain !== "homeassistant"
? `<details><summary>${domainToName(
this.hass.localize,
domain
@@ -408,7 +410,7 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (typeof domainInfo.info[key] === "object") {
if (domainInfo.info[key] && typeof domainInfo.info[key] === "object") {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
@@ -421,11 +423,11 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
} else {
value = domainInfo.info[key];
}
if (github && first) {
if (first) {
parts.push(`${key} | ${value}\n-- | --`);
first = false;
} else {
parts.push(`${key}${github ? " | " : ": "}${value}`);
parts.push(`${key} | ${value}`);
}
}
@@ -433,16 +435,14 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
haContent = parts.join("\n");
} else {
domainParts.push(parts.join("\n"));
if (github && domain !== "homeassistant") {
if (domain !== "homeassistant") {
domainParts.push("</details>");
}
}
}
await copyToClipboard(
`${github ? "## " : ""}System Health\n${haContent}\n\n${domainParts.join(
"\n\n"
)}`
`${"## "}System Information\n${haContent}\n\n${domainParts.join("\n\n")}`
);
showToast(this, {
@@ -450,73 +450,50 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
});
}
static styles: CSSResultGroup = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
integrations-card {
max-width: 600px;
display: block;
max-width: 600px;
margin: 0 auto;
margin-bottom: 24px;
margin-bottom: max(24px, env(safe-area-inset-bottom));
}
ha-card {
display: block;
max-width: 600px;
margin: 0 auto;
padding-bottom: 16px;
margin-bottom: 24px;
}
ha-alert {
display: block;
max-width: 500px;
margin: 0 auto;
margin-bottom: max(24px, env(safe-area-inset-bottom));
}
table {
width: 100%;
}
static styles: CSSResultGroup = [
haStyleDialog,
css`
ha-alert {
margin-bottom: 16px;
display: block;
}
table {
width: 100%;
}
td:first-child {
width: 45%;
}
td:first-child {
width: 45%;
}
td:last-child {
direction: ltr;
}
td:last-child {
direction: ltr;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
}
.card-header {
justify-content: space-between;
display: flex;
align-items: center;
}
.card-header {
justify-content: space-between;
display: flex;
align-items: center;
}
.error {
color: var(--error-color);
}
.error {
color: var(--error-color);
}
a {
color: var(--primary-color);
}
a.manage {
text-decoration: none;
}
`;
a.manage {
text-decoration: none;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-system-health": HaConfigSystemHealth;
"dialog-system-information": DialogSystemInformation;
}
}

View File

@@ -0,0 +1,194 @@
import { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item-base";
import { mdiDotsVertical } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-card";
import "../../../components/ha-check-list-item";
import {
RepairsIssue,
severitySort,
subscribeRepairsIssueRegistry,
} from "../../../data/repairs";
import "../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import "./ha-config-repairs";
import { showIntegrationStartupDialog } from "./show-integration-startup-dialog";
import { showSystemInformationDialog } from "./show-system-information-dialog";
@customElement("ha-config-repairs-dashboard")
class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _repairsIssues: RepairsIssue[] = [];
@state() private _showIgnored = false;
private _getFilteredIssues = memoizeOne(
(showIgnored: boolean, repairsIssues: RepairsIssue[]) =>
showIgnored
? repairsIssues
: repairsIssues.filter((issue) => !issue.ignored)
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._repairsIssues = repairs.issues.sort(
(a, b) => severitySort[a.severity] - severitySort[b.severity]
);
const integrations: Set<string> = new Set();
for (const issue of this._repairsIssues) {
integrations.add(issue.domain);
}
this.hass.loadBackendTranslation("issues", [...integrations]);
}),
];
}
protected render(): TemplateResult {
const issues = this._getFilteredIssues(
this._showIgnored,
this._repairsIssues
);
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.repairs.caption")}
>
<div slot="toolbar-icon">
<ha-button-menu corner="BOTTOM_START" multi>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-check-list-item
left
@request-selected=${this._toggleIgnored}
.selected=${this._showIgnored}
>
${this.hass.localize("ui.panel.config.repairs.show_ignored")}
</ha-check-list-item>
<li divider role="separator"></li>
${isComponentLoaded(this.hass, "system_health") ||
isComponentLoaded(this.hass, "hassio")
? html`
<mwc-list-item
@request-selected=${this._showSystemInformationDialog}
>
${this.hass.localize(
"ui.panel.config.repairs.system_information"
)}
</mwc-list-item>
`
: ""}
<mwc-list-item
@request-selected=${this._showIntegrationStartupDialog}
>
${this.hass.localize(
"ui.panel.config.repairs.integration_startup_time"
)}
</mwc-list-item>
</ha-button-menu>
</div>
<div class="content">
<ha-card outlined>
<div class="card-content">
${this._repairsIssues.length
? html`
<ha-config-repairs
.hass=${this.hass}
.narrow=${this.narrow}
.repairsIssues=${issues}
></ha-config-repairs>
`
: html`
<div class="no-repairs">
${this.hass.localize(
"ui.panel.config.repairs.no_repairs"
)}
</div>
`}
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
private _showSystemInformationDialog(
ev: CustomEvent<RequestSelectedDetail>
): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
showSystemInformationDialog(this);
}
private _showIntegrationStartupDialog(
ev: CustomEvent<RequestSelectedDetail>
): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
showIntegrationStartupDialog(this);
}
private _toggleIgnored(ev: CustomEvent<RequestSelectedDetail>): void {
if (ev.detail.source !== "property") {
return;
}
this._showIgnored = !this._showIgnored;
}
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-card {
max-width: 600px;
margin: 0 auto;
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
margin-bottom: max(24px, env(safe-area-inset-bottom));
}
.card-content {
display: flex;
justify-content: space-between;
flex-direction: column;
padding: 0;
}
.no-repairs {
padding: 16px;
}
li[divider] {
border-bottom-color: var(--divider-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-repairs-dashboard": HaConfigRepairsDashboard;
}
}

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