Merge pull request #13286 from home-assistant/dev

This commit is contained in:
Bram Kragten 2022-07-27 12:41:33 +02:00 committed by GitHub
commit 40616b6af2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
128 changed files with 2811 additions and 1133 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ on:
- src/translations/en.json - src/translations/en.json
env: env:
NODE_VERSION: 14 NODE_VERSION: 16
jobs: jobs:
upload: 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 // Compat needs to be first import
import "../../src/resources/compatibility";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click"; import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate"; import { navigate } from "../../src/common/navigate";
import { import {
@ -7,9 +6,14 @@ import {
provideHass, provideHass,
} from "../../src/fake_data/provide_hass"; } from "../../src/fake_data/provide_hass";
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant"; import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import "../../src/resources/compatibility";
import { HomeAssistant } from "../../src/types"; import { HomeAssistant } from "../../src/types";
import { selectedDemoConfig } from "./configs/demo-configs"; import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAuth } from "./stubs/auth"; 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 { mockEvents } from "./stubs/events";
import { mockFrontend } from "./stubs/frontend"; import { mockFrontend } from "./stubs/frontend";
import { mockHistory } from "./stubs/history"; import { mockHistory } from "./stubs/history";
@ -20,9 +24,6 @@ import { mockShoppingList } from "./stubs/shopping_list";
import { mockSystemLog } from "./stubs/system_log"; import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template"; import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations"; import { mockTranslations } from "./stubs/translations";
import { mockEnergy } from "./stubs/energy";
import { mockConfig } from "./stubs/config";
import { energyEntities } from "./stubs/entities";
class HaDemo extends HomeAssistantAppEl { class HaDemo extends HomeAssistantAppEl {
protected async _initializeHass() { protected async _initializeHass() {
@ -51,8 +52,36 @@ class HaDemo extends HomeAssistantAppEl {
mockMediaPlayer(hass); mockMediaPlayer(hass);
mockFrontend(hass); mockFrontend(hass);
mockEnergy(hass); mockEnergy(hass);
mockConfig(hass);
mockPersistentNotification(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()); 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 = ( export const mockEntityRegistry = (
hass: MockHomeAssistant, hass: MockHomeAssistant,
data: EntityRegistryEntry[] = [] 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", category: "lovelace",
// Label for in the sidebar // Label for in the sidebar
header: "Lovelace", header: "Dashboards",
// Specify order of pages. Any pages in the category folder but not listed here will // Specify order of pages. Any pages in the category folder but not listed here will
// automatically be added after the pages listed here. // automatically be added after the pages listed here.
pages: ["introduction"], pages: ["introduction"],
@ -34,7 +34,7 @@ module.exports = [
}, },
{ {
category: "misc", category: "misc",
header: "Miscelaneous", header: "Miscellaneous",
}, },
{ {
category: "brand", category: "brand",

View File

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

View File

@ -1,11 +1,11 @@
--- ---
title: Introduction 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 a different story about what is going on in their house. These cards
are very customizable, as no household is the same. are very customizable, as no household is the same.
This gallery helps our developers and designers to see all the This gallery helps our developers and designers to see all the
different states that each card can be in. 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, name: null,
icon: null, icon: null,
platform: "updater", platform: "updater",
has_entity_name: false,
}, },
]; ];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ import {
import { import {
UNHEALTHY_REASON_URL, UNHEALTHY_REASON_URL,
UNSUPPORTED_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 { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string"; import { bytesToString } from "../../../src/util/bytes-to-string";

View File

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

View File

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

View File

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

View File

@ -5,8 +5,7 @@ export type LeafletModuleType = typeof import("leaflet");
export type LeafletDrawModuleType = typeof import("leaflet-draw"); export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async ( export const setupLeafletMap = async (
mapElement: HTMLElement, mapElement: HTMLElement
darkMode?: boolean
): Promise<[Map, LeafletModuleType, TileLayer]> => { ): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) { if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element"); throw new Error("Cannot setup Leaflet map on disconnected element");
@ -23,7 +22,7 @@ export const setupLeafletMap = async (
mapElement.parentNode.appendChild(style); mapElement.parentNode.appendChild(style);
map.setView([52.3731339, 4.8903147], 13); 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]; return [map, Leaflet, tileLayer];
}; };
@ -31,23 +30,19 @@ export const setupLeafletMap = async (
export const replaceTileLayer = ( export const replaceTileLayer = (
leaflet: LeafletModuleType, leaflet: LeafletModuleType,
map: Map, map: Map,
tileLayer: TileLayer, tileLayer: TileLayer
darkMode: boolean
): TileLayer => { ): TileLayer => {
map.removeLayer(tileLayer); map.removeLayer(tileLayer);
tileLayer = createTileLayer(leaflet, darkMode); tileLayer = createTileLayer(leaflet);
tileLayer.addTo(map); tileLayer.addTo(map);
return tileLayer; return tileLayer;
}; };
const createTileLayer = ( const createTileLayer = (leaflet: LeafletModuleType): TileLayer =>
leaflet: LeafletModuleType,
darkMode: boolean
): TileLayer =>
leaflet.tileLayer( leaflet.tileLayer(
`https://{s}.basemaps.cartocdn.com/${ `https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}${
darkMode ? "dark_all" : "light_all" leaflet.Browser.retina ? "@2x.png" : ".png"
}/{z}/{x}/{y}${leaflet.Browser.retina ? "@2x.png" : ".png"}`, }`,
{ {
attribution: attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/attributions">CARTO</a>', '&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, mdiCalendar,
mdiCast, mdiCast,
mdiCastConnected, mdiCastConnected,
mdiCastOff,
mdiChartSankey, mdiChartSankey,
mdiCheckCircleOutline, mdiCheckCircleOutline,
mdiClock, mdiClock,
@ -25,7 +26,15 @@ import {
mdiPowerPlug, mdiPowerPlug,
mdiPowerPlugOff, mdiPowerPlugOff,
mdiRestart, mdiRestart,
mdiSpeaker,
mdiSpeakerOff,
mdiSpeakerPause,
mdiSpeakerPlay,
mdiSwapHorizontal, mdiSwapHorizontal,
mdiTelevision,
mdiTelevisionOff,
mdiTelevisionPause,
mdiTelevisionPlay,
mdiToggleSwitchVariant, mdiToggleSwitchVariant,
mdiToggleSwitchVariantOff, mdiToggleSwitchVariantOff,
mdiWeatherNight, mdiWeatherNight,
@ -127,7 +136,40 @@ export const domainIconWithoutDefault = (
} }
case "media_player": 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": case "switch":
switch (stateObj?.attributes.device_class) { 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 shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/lib/should-polyfill";
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill"; import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
import IntlMessageFormat from "intl-messageformat"; import IntlMessageFormat from "intl-messageformat";
import { Resources } from "../../types"; import { Resources, TranslationDict } from "../../types";
import { getLocalLanguage } from "../../util/common-translation"; 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 { interface FormatType {
[format: string]: any; [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, cache: any,
language: string, language: string,
resources: Resources, resources: Resources,
formats?: FormatsType formats?: FormatsType
): Promise<LocalizeFunc> => { ): Promise<LocalizeFunc<Dict>> => {
if (polyfillsLoaded) { if (polyfillsLoaded) {
await polyfillsLoaded; await polyfillsLoaded;
} }

View File

@ -188,6 +188,10 @@ export default class HaChartBase extends LitElement {
ChartConstructor.defaults.color = computedStyles.getPropertyValue( ChartConstructor.defaults.color = computedStyles.getPropertyValue(
"--secondary-text-color" "--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, { this.chart = new ChartConstructor(ctx, {
type: this.chartType, type: this.chartType,
@ -376,6 +380,7 @@ export default class HaChartBase extends LitElement {
.chartTooltip .title { .chartTooltip .title {
text-align: center; text-align: center;
font-weight: 500; font-weight: 500;
direction: ltr;
} }
.chartTooltip .footer { .chartTooltip .footer {
font-weight: 500; font-weight: 500;

View File

@ -84,20 +84,20 @@ class HaAddonPicker extends LitElement {
} else { } else {
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.title" "ui.components.addon-picker.error.no_supervisor.title"
), ),
text: this.hass.localize( text: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.description" "ui.components.addon-picker.error.no_supervisor.description"
), ),
}); });
} }
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.title" "ui.components.addon-picker.error.fetch_addons.title"
), ),
text: this.hass.localize( 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` css`
.attribute-container { .attribute-container {
margin-bottom: 8px; margin-bottom: 8px;
direction: ltr;
} }
.data-entry { .data-entry {
display: flex; display: flex;

View File

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

View File

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

View File

@ -52,6 +52,11 @@ export class HaSelect extends SelectBase {
inset-inline-end: initial; inset-inline-end: initial;
direction: var(--direction); 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 { .mdc-select .mdc-select__anchor {
padding-inline-start: 12px; padding-inline-start: 12px;
padding-inline-end: 0px; padding-inline-end: 0px;

View File

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

View File

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

View File

@ -1,12 +1,12 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { import {
EntitySources, EntitySources,
fetchEntitySourcesWithCache, fetchEntitySourcesWithCache,
} from "../../data/entity_sources"; } 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 { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker"; import "../entity/ha-entities-picker";
import "../entity/ha-entity-picker"; import "../entity/ha-entity-picker";
@ -73,37 +73,8 @@ export class HaEntitySelector extends LitElement {
} }
} }
private _filterEntities = (entity: HassEntity): boolean => { private _filterEntities = (entity: HassEntity): boolean =>
const { filterSelectorEntities(this.selector.entity, entity, this._entitySources);
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;
};
} }
declare global { declare global {

View File

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

View File

@ -3,17 +3,33 @@ import {
HassServiceTarget, HassServiceTarget,
UnsubscribeFunc, UnsubscribeFunc,
} from "home-assistant-js-websocket"; } 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 { import {
EntityRegistryEntry, css,
subscribeEntityRegistry, CSSResultGroup,
} from "../../data/entity_registry"; html,
import { TargetSelector } from "../../data/selector"; 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 { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-target-picker"; import "../ha-target-picker";
@customElement("ha-selector-target") @customElement("ha-selector-target")
@ -28,119 +44,82 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@property() public helper?: string; @property() public helper?: string;
@state() private _entityPlaformLookup?: Record<string, string>;
@state() private _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeEntityRegistry(this.hass.connection!, (entities) => { subscribeEntityRegistry(this.hass.connection!, (entities) => {
const entityLookup = {}; this._entities = entities.filter((entity) => entity.device_id !== null);
for (const confEnt of entities) {
if (!confEnt.platform) {
continue;
}
entityLookup[confEnt.entity_id] = confEnt.platform;
}
this._entityPlaformLookup = entityLookup;
}), }),
]; ];
} }
protected updated(changedProperties) { protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selector")) { super.updated(changedProperties);
const oldSelector = changedProperties.get("selector"); if (
if ( changedProperties.has("selector") &&
oldSelector !== this.selector && this.selector.target.device?.integration &&
(this.selector.target.device?.integration || !this._entitySources
this.selector.target.entity?.integration) ) {
) { fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._loadConfigEntries(); 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 return html`<ha-target-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this.value} .value=${this.value}
.helper=${this.helper} .helper=${this.helper}
.deviceFilter=${this._filterDevices} .deviceFilter=${this._filterDevices}
.entityRegFilter=${this._filterRegEntities}
.entityFilter=${this._filterEntities} .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} .disabled=${this.disabled}
></ha-target-picker>`; ></ha-target-picker>`;
} }
private _filterEntities = (entity: HassEntity): boolean => { private _filterEntities = (entity: HassEntity): boolean => {
if ( if (!this.selector.target.entity) {
this.selector.target.entity?.integration || return true;
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;
}
} }
return true;
};
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => { return filterSelectorEntities(
if (this.selector.target.entity?.integration) { this.selector.target.entity,
if (entity.platform !== this.selector.target.entity.integration) { entity,
return false; this._entitySources
} );
}
return true;
}; };
private _filterDevices = (device: DeviceRegistryEntry): boolean => { private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if ( if (!this.selector.target.device) {
this.selector.target.device?.manufacturer && return true;
device.manufacturer !== this.selector.target.device.manufacturer
) {
return false;
} }
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() { const deviceIntegrations =
this._configEntries = (await getConfigEntries(this.hass)).filter( this._entitySources && this._entities
(entry) => ? this._deviceIntegrationLookup(this._entitySources, this._entities)
entry.domain === this.selector.target.device?.integration || : undefined;
entry.domain === this.selector.target.entity?.integration
return filterSelectorDevices(
this.selector.target.device,
device,
deviceIntegrations
); );
} };
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` 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 type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResult, CSSResult,
@ -44,7 +45,9 @@ import {
PersistentNotification, PersistentNotification,
subscribeNotifications, subscribeNotifications,
} from "../data/persistent_notification"; } from "../data/persistent_notification";
import { subscribeRepairsIssueRegistry } from "../data/repairs";
import { updateCanInstall, UpdateEntity } from "../data/update"; import { updateCanInstall, UpdateEntity } from "../data/update";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
@ -177,7 +180,7 @@ const computePanels = memoizeOne(
let Sortable; let Sortable;
@customElement("ha-sidebar") @customElement("ha-sidebar")
class HaSidebar extends LitElement { class HaSidebar extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow!: boolean; @property({ type: Boolean, reflect: true }) public narrow!: boolean;
@ -192,6 +195,8 @@ class HaSidebar extends LitElement {
@state() private _updatesCount = 0; @state() private _updatesCount = 0;
@state() private _issuesCount = 0;
@state() private _renderEmptySortable = false; @state() private _renderEmptySortable = false;
private _mouseLeaveTimeout?: number; private _mouseLeaveTimeout?: number;
@ -214,6 +219,16 @@ class HaSidebar extends LitElement {
private _sortable?; private _sortable?;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._issuesCount = repairs.issues.filter(
(issue) => !issue.ignored
).length;
}),
];
}
protected render() { protected render() {
if (!this.hass) { if (!this.hass) {
return html``; return html``;
@ -238,6 +253,7 @@ class HaSidebar extends LitElement {
changedProps.has("alwaysExpand") || changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") || changedProps.has("_externalConfig") ||
changedProps.has("_updatesCount") || changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") ||
changedProps.has("_notifications") || changedProps.has("_notifications") ||
changedProps.has("editMode") || changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") || changedProps.has("_renderEmptySortable") ||
@ -500,7 +516,7 @@ class HaSidebar extends LitElement {
} }
private _renderConfiguration(title: string | null) { private _renderConfiguration(title: string | null) {
return html` <a return html`<a
class="configuration-container" class="configuration-container"
role="option" role="option"
href="/config" href="/config"
@ -511,17 +527,20 @@ class HaSidebar extends LitElement {
> >
<paper-icon-item class="configuration" role="option"> <paper-icon-item class="configuration" role="option">
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon> <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` ? html`
<span class="configuration-badge" slot="item-icon"> <span class="configuration-badge" slot="item-icon">
${this._updatesCount} ${this._updatesCount + this._issuesCount}
</span> </span>
` `
: ""} : ""}
<span class="item-text">${title}</span> <span class="item-text">${title}</span>
${this.alwaysExpand && this._updatesCount > 0 ${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
? html` ? html`
<span class="configuration-badge">${this._updatesCount}</span> <span class="configuration-badge"
>${this._updatesCount + this._issuesCount}</span
>
` `
: ""} : ""}
</paper-icon-item> </paper-icon-item>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,8 @@ export interface ConfigEntry {
| "migration_error" | "migration_error"
| "setup_retry" | "setup_retry"
| "not_loaded" | "not_loaded"
| "failed_unload"; | "failed_unload"
| "setup_in_progress";
supports_options: boolean; supports_options: boolean;
supports_remove_device: boolean; supports_remove_device: boolean;
supports_unload: 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"][] = [ export const ERROR_STATES: ConfigEntry["state"][] = [
"migration_error", "migration_error",
"setup_error", "setup_error",
"setup_retry", "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 = ( export const getConfigEntries = (
hass: HomeAssistant, hass: HomeAssistant,
filters?: { type?: "helper" | "integration"; domain?: string } filters?: { type?: "helper" | "integration"; domain?: string }

View File

@ -1,10 +1,11 @@
import { Connection, createCollection } from "home-assistant-js-websocket"; 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 { computeStateName } from "../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry"; import type { EntityRegistryEntry } from "./entity_registry";
import type { EntitySources } from "./entity_sources";
export interface DeviceRegistryEntry { export interface DeviceRegistryEntry {
id: string; id: string;
@ -20,7 +21,7 @@ export interface DeviceRegistryEntry {
area_id: string | null; area_id: string | null;
name_by_user: string | null; name_by_user: string | null;
entry_type: "service" | null; entry_type: "service" | null;
disabled_by: string | null; disabled_by: "user" | "integration" | "config_entry" | null;
configuration_url: string | null; configuration_url: string | null;
} }
@ -142,3 +143,23 @@ export const getDeviceEntityLookup = (
} }
return deviceEntityLookup; 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; config_entry_id: string | null;
device_id: string | null; device_id: string | null;
area_id: string | null; area_id: string | null;
disabled_by: string | null; disabled_by: "user" | "device" | "integration" | "config_entry" | null;
hidden_by: string | null; hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
entity_category: "config" | "diagnostic" | null; entity_category: "config" | "diagnostic" | null;
has_entity_name: boolean;
original_name?: string;
} }
export interface ExtEntityRegistryEntry extends EntityRegistryEntry { export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
unique_id: string; unique_id: string;
capabilities: Record<string, unknown>; capabilities: Record<string, unknown>;
original_name?: string;
original_icon?: string; original_icon?: string;
device_class?: string; device_class?: string;
original_device_class?: string; original_device_class?: string;
@ -37,6 +38,10 @@ export interface SensorEntityOptions {
unit_of_measurement?: string | null; unit_of_measurement?: string | null;
} }
export interface NumberEntityOptions {
unit_of_measurement?: string | null;
}
export interface WeatherEntityOptions { export interface WeatherEntityOptions {
precipitation_unit?: string | null; precipitation_unit?: string | null;
pressure_unit?: string | null; pressure_unit?: string | null;
@ -155,3 +160,16 @@ export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) =>
entries.sort((entry1, entry2) => entries.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "") 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 { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display"; import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
import { import {
@ -268,7 +268,8 @@ const processTimelineEntity = (
localize: LocalizeFunc, localize: LocalizeFunc,
language: FrontendLocaleData, language: FrontendLocaleData,
entityId: string, entityId: string,
states: EntityHistoryState[] states: EntityHistoryState[],
current_state: HassEntity | undefined
): TimelineEntity => { ): TimelineEntity => {
const data: TimelineState[] = []; const data: TimelineState[] = [];
const first: EntityHistoryState = states[0]; const first: EntityHistoryState = states[0];
@ -292,7 +293,10 @@ const processTimelineEntity = (
} }
return { return {
name: computeStateNameFromEntityAttributes(entityId, states[0].a), name: computeStateNameFromEntityAttributes(
entityId,
current_state?.attributes || first.a
),
entity_id: entityId, entity_id: entityId,
data, data,
}; };
@ -300,7 +304,8 @@ const processTimelineEntity = (
const processLineChartEntities = ( const processLineChartEntities = (
unit, unit,
entities: HistoryStates entities: HistoryStates,
hassEntities: HassEntities
): LineChartUnit => { ): LineChartUnit => {
const data: LineChartEntity[] = []; const data: LineChartEntity[] = [];
@ -349,9 +354,16 @@ const processLineChartEntities = (
processedStates.push(processedState); processedStates.push(processedState);
} }
const attributes =
entityId in hassEntities
? hassEntities[entityId].attributes
: "friendly_name" in first.a
? first.a
: undefined;
data.push({ data.push({
domain, domain,
name: computeStateNameFromEntityAttributes(entityId, first.a), name: computeStateNameFromEntityAttributes(entityId, attributes || {}),
entity_id: entityId, entity_id: entityId,
states: processedStates, states: processedStates,
}); });
@ -411,7 +423,13 @@ export const computeHistory = (
if (!unit) { if (!unit) {
timelineDevices.push( timelineDevices.push(
processTimelineEntity(localize, hass.locale, entityId, stateInfo) processTimelineEntity(
localize,
hass.locale,
entityId,
stateInfo,
currentState
)
); );
} else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) { } else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) {
lineChartDevices[unit][entityId].push(...stateInfo); lineChartDevices[unit][entityId].push(...stateInfo);
@ -424,7 +442,7 @@ export const computeHistory = (
}); });
const unitStates = Object.keys(lineChartDevices).map((unit) => const unitStates = Object.keys(lineChartDevices).map((unit) =>
processLineChartEntities(unit, lineChartDevices[unit]) processLineChartEntities(unit, lineChartDevices[unit], hass.states)
); );
return { line: unitStates, timeline: timelineDevices }; return { line: unitStates, timeline: timelineDevices };

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

@ -0,0 +1,95 @@
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_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 = export type Selector =
| ActionSelector | ActionSelector
| AddonSelector | 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 { export interface AreaSelector {
area: { area: {
entity?: { entity?: SelectorEntity;
integration?: EntitySelector["entity"]["integration"]; device?: SelectorDevice;
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
multiple?: boolean; multiple?: boolean;
}; };
} }
@ -89,10 +98,7 @@ export interface DeviceSelector {
integration?: string; integration?: string;
manufacturer?: string; manufacturer?: string;
model?: string; model?: string;
entity?: { entity?: SelectorEntity;
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
multiple?: boolean; multiple?: boolean;
}; };
} }
@ -201,16 +207,8 @@ export interface StringSelector {
export interface TargetSelector { export interface TargetSelector {
target: { target: {
entity?: { entity?: SelectorEntity;
integration?: EntitySelector["entity"]["integration"]; device?: SelectorDevice;
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
}; };
} }
@ -227,3 +225,69 @@ export interface TimeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
time: {}; 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 { Connection, getCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store"; import { Store } from "home-assistant-js-websocket/dist/store";
import { LocalizeFunc } from "../../common/translations/localize"; import { LocalizeFunc } from "../../common/translations/localize";
import { HomeAssistant } from "../../types"; import { HomeAssistant, TranslationDict } from "../../types";
import { HassioAddonsInfo } from "../hassio/addon"; import { HassioAddonsInfo } from "../hassio/addon";
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host"; import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
import { NetworkInfo } from "../hassio/network"; import { NetworkInfo } from "../hassio/network";
@ -67,7 +67,7 @@ export interface Supervisor {
os: HassioHassOSInfo; os: HassioHassOSInfo;
addon: HassioAddonsInfo; addon: HassioAddonsInfo;
store: SupervisorStore; store: SupervisorStore;
localize: LocalizeFunc; localize: LocalizeFunc<TranslationDict["supervisor"]>;
} }
export const supervisorApiWsRequest = <T>( 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" | "mfa_setup"
| "system_health" | "system_health"
| "device_class" | "device_class"
| "application_credentials"; | "application_credentials"
| "issues";
export const fetchTranslationPreferences = (hass: HomeAssistant) => export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, "language"); fetchFrontendUserData(hass.connection, "language");

View File

@ -4,7 +4,7 @@ import {
mdiHomeCircleOutline, mdiHomeCircleOutline,
mdiCancel, mdiCancel,
} from "@mdi/js"; } from "@mdi/js";
import { HomeAssistant } from "../types"; import { HomeAssistant, TranslationDict } from "../types";
import { Credential } from "./auth"; import { Credential } from "./auth";
export const SYSTEM_GROUP_ID_ADMIN = "system-admin"; export const SYSTEM_GROUP_ID_ADMIN = "system-admin";
@ -21,7 +21,7 @@ export interface User {
is_active: boolean; is_active: boolean;
local_only: boolean; local_only: boolean;
system_generated: boolean; system_generated: boolean;
group_ids: string[]; group_ids: (keyof TranslationDict["groups"])[];
credentials: Credential[]; credentials: Credential[];
} }
@ -95,7 +95,12 @@ export const computeUserBadges = (
includeSystem: boolean includeSystem: boolean
) => { ) => {
const labels: [string, string][] = []; 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) { if (user.is_owner) {
labels.push([OWNER_ICON, translate("is_owner")]); labels.push([OWNER_ICON, translate("is_owner")]);

View File

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

View File

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

View File

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

View File

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

View File

@ -14,19 +14,19 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { protocolIntegrationPicked } from "../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import "../../components/search-input";
import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize"; import { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-icon-next"; import "../../components/ha-icon-next";
import { getConfigEntries } from "../../data/config_entries"; import "../../components/search-input";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url"; import { brandsUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-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 { FlowHandlers } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
interface HandlerObj { interface HandlerObj {
name: string; name: string;
@ -35,6 +35,10 @@ interface HandlerObj {
is_helper?: boolean; is_helper?: boolean;
} }
export interface SupportedBrandObj extends HandlerObj {
supported_flows: string[];
}
declare global { declare global {
// for fire event // for fire event
interface HASSDomEvents { interface HASSDomEvents {
@ -63,11 +67,22 @@ class StepFlowPickHandler extends LitElement {
h: FlowHandlers, h: FlowHandlers,
filter?: string, filter?: string,
_localize?: LocalizeFunc _localize?: LocalizeFunc
): [HandlerObj[], HandlerObj[]] => { ): [(HandlerObj | SupportedBrandObj)[], HandlerObj[]] => {
const integrations: HandlerObj[] = h.integrations.map((handler) => ({ const integrations: (HandlerObj | SupportedBrandObj)[] =
name: domainToName(this.hass.localize, handler), h.integrations.map((handler) => ({
slug: 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) { if (filter) {
const options: Fuse.IFuseOptions<HandlerObj> = { const options: Fuse.IFuseOptions<HandlerObj> = {
@ -238,27 +253,10 @@ class StepFlowPickHandler extends LitElement {
} }
private async _handlerPicked(ev) { private async _handlerPicked(ev) {
const handler: HandlerObj = ev.currentTarget.handler; const handler: HandlerObj | SupportedBrandObj = ev.currentTarget.handler;
if (handler.is_add) { if (handler.is_add) {
if (handler.slug === "zwave_js") { this._handleAddPicked(handler.slug);
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");
return; return;
} }
@ -269,11 +267,43 @@ class StepFlowPickHandler extends LitElement {
return; 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", { fireEvent(this, "handler-picked", {
handler: handler.slug, 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) { private _maybeSubmit(ev: KeyboardEvent) {
if (ev.key !== "Enter") { if (ev.key !== "Enter") {
return; return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ export const getZHADeviceActions = async (
const actions: DeviceAction[] = []; const actions: DeviceAction[] = [];
if (zhaDevice.device_type !== "Coordinator") { if (!zhaDevice.active_coordinator) {
actions.push({ actions.push({
label: hass.localize("ui.dialogs.zha_device_info.buttons.reconfigure"), label: hass.localize("ui.dialogs.zha_device_info.buttons.reconfigure"),
action: () => showZHAReconfigureDeviceDialog(el, { device: zhaDevice }), action: () => showZHAReconfigureDeviceDialog(el, { device: zhaDevice }),
@ -58,50 +58,50 @@ export const getZHADeviceActions = async (
); );
} }
if (zhaDevice.device_type !== "Coordinator") { actions.push(
actions.push( ...[
...[ {
{ label: hass.localize(
label: hass.localize( "ui.dialogs.zha_device_info.buttons.zigbee_information"
"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) { if (!confirmed) {
return; return;
} }
await hass.callService("zha", "remove", { await hass.callService("zha", "remove", {
ieee: zhaDevice.ieee, ieee: zhaDevice.ieee,
}); });
history.back(); history.back();
}, },
}, });
]
);
} }
return actions; return actions;

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import {
subscribeDeviceRegistry, subscribeDeviceRegistry,
} from "../../../data/device_registry"; } from "../../../data/device_registry";
import { import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams, EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
updateEntityRegistryEntry, updateEntityRegistryEntry,
@ -25,7 +26,7 @@ import type { HomeAssistant } from "../../../types";
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public entry!: ExtEntityRegistryEntry; @property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
@state() private _origEntityId!: string; @state() private _origEntityId!: string;
@ -33,7 +34,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@state() private _areaId?: string | null; @state() private _areaId?: string | null;
@state() private _disabledBy!: string | null; @state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
@state() private _hiddenBy!: string | null; @state() private _hiddenBy!: string | null;
@ -41,7 +42,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@state() private _device?: DeviceRegistryEntry; @state() private _device?: DeviceRegistryEntry;
@state() private _submitting?: boolean; @state() private _submitting = false;
public async updateEntry(): Promise<void> { public async updateEntry(): Promise<void> {
this._submitting = true; this._submitting = true;
@ -145,8 +146,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
></ha-textfield> ></ha-textfield>
<ha-area-picker <ha-area-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._areaId} .value=${this._areaId || undefined}
.placeholder=${this._device?.area_id} .placeholder=${this._device?.area_id || undefined}
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
></ha-area-picker> ></ha-area-picker>
@ -182,8 +183,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled" name="hiddendisabled"
value="enabled" value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy} .checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${this._device?.disabled_by || .disabled=${!!this._device?.disabled_by ||
(this._disabledBy && (this._disabledBy !== null &&
!( !(
this._disabledBy === "user" || this._disabledBy === "user" ||
this._disabledBy === "integration" this._disabledBy === "integration"
@ -200,8 +201,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled" name="hiddendisabled"
value="hidden" value="hidden"
.checked=${this._hiddenBy !== null} .checked=${this._hiddenBy !== null}
.disabled=${this._device?.disabled_by || .disabled=${!!this._device?.disabled_by ||
(this._disabledBy && (this._disabledBy !== null &&
!( !(
this._disabledBy === "user" || this._disabledBy === "user" ||
this._disabledBy === "integration" this._disabledBy === "integration"
@ -218,8 +219,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled" name="hiddendisabled"
value="disabled" value="disabled"
.checked=${this._disabledBy !== null} .checked=${this._disabledBy !== null}
.disabled=${this._device?.disabled_by || .disabled=${!!this._device?.disabled_by ||
(this._disabledBy && (this._disabledBy !== null &&
!( !(
this._disabledBy === "user" || this._disabledBy === "user" ||
this._disabledBy === "integration" 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, updateDeviceRegistryEntry,
} from "../../../data/device_registry"; } from "../../../data/device_registry";
import { import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams, EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
fetchEntityRegistry, fetchEntityRegistry,
@ -105,6 +106,10 @@ const OVERRIDE_DEVICE_CLASSES = {
], ],
}; };
const OVERRIDE_NUMBER_UNITS = {
temperature: ["°C", "°F", "K"],
};
const OVERRIDE_SENSOR_UNITS = { const OVERRIDE_SENSOR_UNITS = {
temperature: ["°C", "°F", "K"], temperature: ["°C", "°F", "K"],
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], 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) { export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public entry!: ExtEntityRegistryEntry; @property({ type: Object }) public entry!: ExtEntityRegistryEntry;
@state() private _name!: string; @state() private _name!: string;
@ -138,9 +143,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _areaId?: string | null; @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; @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 = const stateObj: HassEntity | undefined =
this.hass.states[this.entry.entity_id]; this.hass.states[this.entry.entity_id];
this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement; this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement;
@ -361,6 +366,31 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
</ha-select> </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" && ${domain === "sensor" &&
this._deviceClass && this._deviceClass &&
stateObj?.attributes.unit_of_measurement && stateObj?.attributes.unit_of_measurement &&
@ -601,9 +631,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
name="hiddendisabled" name="hiddendisabled"
value="enabled" value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy} .checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") || .disabled=${(this._hiddenBy !== null &&
this._device?.disabled_by || this._hiddenBy !== "user") ||
(this._disabledBy && !!this._device?.disabled_by ||
(this._disabledBy !== null &&
this._disabledBy !== "user" && this._disabledBy !== "user" &&
this._disabledBy !== "integration")} this._disabledBy !== "integration")}
@change=${this._viewStatusChanged} @change=${this._viewStatusChanged}
@ -861,10 +892,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
params.hidden_by = this._hiddenBy; params.hidden_by = this._hiddenBy;
} }
if ( if (
domain === "sensor" && (domain === "number" || domain === "number") &&
stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement 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 }; params.options = { unit_of_measurement: this._unit_of_measurement };
} }
if ( if (
@ -1023,12 +1054,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
bottom: 0; bottom: 0;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
border-top: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
display: flex; display: flex;
justify-content: space-between; padding: 0 24px 24px 24px;
padding: 8px; justify-content: flex-end;
padding-bottom: max(env(safe-area-inset-bottom), 8px); padding-bottom: max(env(safe-area-inset-bottom), 24px);
background-color: var(--mdc-theme-surface, #fff); background-color: var(--mdc-theme-surface, #fff);
} }
ha-select { 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 { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoize from "memoize-one"; import memoize from "memoize-one";
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../common/dom/fire_event";
@ -85,11 +86,11 @@ export interface EntityRow extends StateEntity {
export class HaConfigEntities extends SubscribeMixin(LitElement) { export class HaConfigEntities extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @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[]; @state() private _entities?: EntityRegistryEntry[];
@ -174,7 +175,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
type: "icon", type: "icon",
template: (_, entry: EntityRow) => html` template: (_, entry: EntityRow) => html`
<ha-state-icon <ha-state-icon
.title=${entry.entity?.state} title=${ifDefined(entry.entity?.state)}
slot="item-icon" slot="item-icon"
.state=${entry.entity} .state=${entry.entity}
></ha-state-icon> ></ha-state-icon>
@ -237,12 +238,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
hidden: narrow || !showDisabled, hidden: narrow || !showDisabled,
filterable: true, filterable: true,
width: "15%", width: "15%",
template: (disabled_by) => template: (disabled_by: EntityRegistryEntry["disabled_by"]) =>
this.hass.localize( disabled_by === null
`ui.panel.config.devices.disabled_by.${disabled_by}` ? "—"
) || : this.hass.localize(`config_entry.disabled_by.${disabled_by}`),
disabled_by ||
"—",
}, },
status: { status: {
title: this.hass.localize( title: this.hass.localize(
@ -736,6 +735,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
readonly: true, readonly: true,
selectable: false, selectable: false,
entity_category: null, entity_category: null,
has_entity_name: false,
}); });
} }
if (changed) { 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, mdiCog,
mdiDatabase, mdiDatabase,
mdiDevices, mdiDevices,
mdiHeart,
mdiInformation, mdiInformation,
mdiInformationOutline, mdiInformationOutline,
mdiLifebuoy,
mdiLightningBolt, mdiLightningBolt,
mdiMapMarkerRadius, mdiMapMarkerRadius,
mdiMathLog, mdiMathLog,
@ -267,6 +267,12 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiUpdate, iconPath: mdiUpdate,
iconColor: "#3B808E", iconColor: "#3B808E",
}, },
{
path: "/config/repairs",
translationKey: "repairs",
iconPath: mdiLifebuoy,
iconColor: "#5c995c",
},
{ {
component: "logs", component: "logs",
path: "/config/logs", path: "/config/logs",
@ -315,13 +321,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#301A8E", iconColor: "#301A8E",
component: "hassio", component: "hassio",
}, },
{
path: "/config/system_health",
translationKey: "system_health",
iconPath: mdiHeart,
iconColor: "#507FfE",
components: ["system_health", "hassio"],
},
], ],
about: [ about: [
{ {
@ -440,14 +439,14 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-section-storage", tag: "ha-config-section-storage",
load: () => import("./storage/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: { updates: {
tag: "ha-config-section-updates", tag: "ha-config-section-updates",
load: () => import("./core/ha-config-section-updates"), load: () => import("./core/ha-config-section-updates"),
}, },
repairs: {
tag: "ha-config-repairs-dashboard",
load: () => import("./repairs/ha-config-repairs-dashboard"),
},
users: { users: {
tag: "ha-config-users", tag: "ha-config-users",
load: () => import("./users/ha-config-users"), load: () => import("./users/ha-config-users"),

View File

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

View File

@ -202,7 +202,7 @@ class HaConfigHardware extends LitElement {
title: this.hass.localize("ui.panel.config.hardware.reboot_host"), title: this.hass.localize("ui.panel.config.hardware.reboot_host"),
text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"), text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"),
confirmText: this.hass.localize("ui.panel.config.hardware.reboot_host"), 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) { if (!confirmed) {
@ -236,7 +236,7 @@ class HaConfigHardware extends LitElement {
"ui.panel.config.hardware.shutdown_host_confirm" "ui.panel.config.hardware.shutdown_host_confirm"
), ),
confirmText: this.hass.localize("ui.panel.config.hardware.shutdown_host"), 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) { if (!confirmed) {

View File

@ -14,7 +14,8 @@ import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; 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 { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize"; import type { LocalizeFunc } from "../../../common/translations/localize";
@ -49,6 +50,10 @@ import {
fetchIntegrationManifests, fetchIntegrationManifests,
IntegrationManifest, IntegrationManifest,
} from "../../../data/integration"; } from "../../../data/integration";
import {
getSupportedBrands,
getSupportedBrandsLookup,
} from "../../../data/supported_brands";
import { scanUSBDevices } from "../../../data/usb"; import { scanUSBDevices } from "../../../data/usb";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { import {
@ -677,49 +682,84 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
if (!domain) { if (!domain) {
return; return;
} }
const handlers = await getConfigFlowHandlers(this.hass, "integration"); const handlers = await getConfigFlowHandlers(this.hass, "integration");
if (!handlers.includes(domain)) { // Integration exists, so we can just create a flow
if (HELPER_DOMAINS.includes(domain)) { if (handlers.includes(domain)) {
navigate(`/config/helpers/add?domain=${domain}`, { const localize = await localizePromise;
replace: true, if (
}); !(await showConfirmationDialog(this, {
title: localize("ui.panel.config.integrations.confirm_new", {
integration: domainToName(localize, domain),
}),
}))
) {
return; return;
} }
const helpers = await getConfigFlowHandlers(this.hass, "helper"); showConfigFlowDialog(this, {
if (helpers.includes(domain)) { dialogClosedCallback: () => {
navigate(`/config/helpers/add?domain=${domain}`, { this._handleFlowUpdated();
replace: true, },
}); startFlowHandler: domain,
return; manifest: this._manifests[domain],
} showAdvanced: this.hass.userData?.showAdvanced,
showAlertDialog(this, { });
title: this.hass.localize( }
"ui.panel.config.integrations.config_flow.error"
), 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( 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; return;
} }
const localize = await localizePromise; const helpers = await getConfigFlowHandlers(this.hass, "helper");
if ( if (helpers.includes(domain)) {
!(await showConfirmationDialog(this, { navigate(`/config/helpers/add?domain=${domain}`, {
title: localize("ui.panel.config.integrations.confirm_new", { replace: true,
integration: domainToName(localize, domain), });
}),
}))
) {
return; return;
} }
showConfigFlowDialog(this, { showAlertDialog(this, {
dialogClosedCallback: () => { title: this.hass.localize(
this._handleFlowUpdated(); "ui.panel.config.integrations.config_flow.error"
}, ),
startFlowHandler: domain, text: this.hass.localize(
manifest: this._manifests[domain], "ui.panel.config.integrations.config_flow.no_config_flow"
showAdvanced: this.hass.userData?.showAdvanced, ),
}); });
} }

View File

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

View File

@ -401,7 +401,7 @@ class DialogZWaveJSAddNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: this._status === "finished" : this._status === "finished"
@ -451,7 +451,7 @@ class DialogZWaveJSAddNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: this._status === "provisioned" : this._status === "provisioned"
@ -469,7 +469,7 @@ class DialogZWaveJSAddNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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>` </mwc-button>`
: ""} : ""}
</ha-dialog> </ha-dialog>

View File

@ -120,7 +120,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
)} )}
</mwc-button> </mwc-button>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}
@ -140,7 +140,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}
@ -160,7 +160,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}
@ -180,7 +180,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}

View File

@ -166,7 +166,7 @@ class DialogZWaveJSHealNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}
@ -186,7 +186,7 @@ class DialogZWaveJSHealNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}

View File

@ -85,7 +85,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}
@ -105,7 +105,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}
@ -125,7 +125,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}

View File

@ -110,7 +110,7 @@ class DialogZWaveJSRemoveNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}
@ -132,7 +132,7 @@ class DialogZWaveJSRemoveNode extends LitElement {
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <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> </mwc-button>
` `
: ``} : ``}

View File

@ -102,7 +102,7 @@ export class SystemLogCard extends LitElement {
<paper-item @click=${this._openLog} .logItem=${item}> <paper-item @click=${this._openLog} .logItem=${item}>
<paper-item-body two-line> <paper-item-body two-line>
<div class="row">${item.message[0]}</div> <div class="row">${item.message[0]}</div>
<div secondary> <div class="row-secondary" secondary>
${this._timestamp(item)} ${this._timestamp(item)}
${html`(<span class=${item.level.toLowerCase()} ${html`(<span class=${item.level.toLowerCase()}
>${this.hass.localize( >${this.hass.localize(
@ -209,6 +209,11 @@ export class SystemLogCard extends LitElement {
.empty-content { .empty-content {
direction: var(--direction); 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-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-list/mwc-list-item";
import "@material/mwc-tab"; import "@material/mwc-tab";
import "@material/mwc-tab-bar"; import "@material/mwc-tab-bar";
import { mdiDotsVertical } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache"; import { cache } from "lit/directives/cache";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-expansion-panel"; import "../../../components/ha-expansion-panel";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
@ -29,7 +32,7 @@ import {
showConfirmationDialog, showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box"; } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../../../components/ha-card"; import { showIPDetailDialog } from "./show-ip-detail-dialog";
const IP_VERSIONS = ["ipv4", "ipv6"]; const IP_VERSIONS = ["ipv4", "ipv6"];
@ -236,9 +239,25 @@ export class HassioNetwork extends LitElement {
</ha-circular-progress>` </ha-circular-progress>`
: this.hass.localize("ui.common.save")} : this.hass.localize("ui.common.save")}
</mwc-button> </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>`; </div>`;
} }
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
showIPDetailDialog(this, { interface: this._interface });
break;
}
}
private _selectAP(event) { private _selectAP(event) {
this._wifiConfiguration = event.currentTarget.ap; this._wifiConfiguration = event.currentTarget.ap;
this._dirty = true; this._dirty = true;

View File

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

View File

@ -0,0 +1,57 @@
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 "../../../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
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.repairs.integration_startup_time")
)}
>
<integrations-startup-time
.hass=${this.hass}
narrow
></integrations-startup-time>
</ha-dialog>
`;
}
static styles: CSSResultGroup = haStyleDialog;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-integration-startup": DialogIntegrationStartup;
}
}

View File

@ -0,0 +1,148 @@
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.hass!.localize("ui.panel.config.repairs.dialog.title")
)}
>
<div>
<ha-alert
.alertType=${this._issue.severity === "error" ||
this._issue.severity === "critical"
? "error"
: "warning"}
.title=${this.hass.localize(
`ui.panel.config.repairs.${this._issue.severity}`
)}
>${this.hass.localize(
"ui.panel.config.repairs.dialog.alert_not_fixable"
)}
${this._issue.breaks_in_ha_version
? 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>
${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;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-repairs-issue": DialogRepairsIssue;
}
}

View File

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

View File

@ -0,0 +1,187 @@
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 {
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">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${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>
<mwc-list-item @request-selected=${this._toggleIgnored}>
${this._showIgnored
? this.hass.localize("ui.panel.config.repairs.hide_ignored")
: this.hass.localize("ui.panel.config.repairs.show_ignored")}
</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 (!shouldHandleRequestSelectedEvent(ev)) {
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;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-repairs-dashboard": HaConfigRepairsDashboard;
}
}

View File

@ -0,0 +1,143 @@
import "@material/mwc-list/mwc-list";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { relativeTime } from "../../../common/datetime/relative_time";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon";
import { domainToName } from "../../../data/integration";
import type { RepairsIssue } from "../../../data/repairs";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { showRepairsFlowDialog } from "./show-dialog-repair-flow";
import { showRepairsIssueDialog } from "./show-repair-issue-dialog";
@customElement("ha-config-repairs")
class HaConfigRepairs extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false })
public repairsIssues?: RepairsIssue[];
@property({ type: Number })
public total?: number;
protected render(): TemplateResult {
if (!this.repairsIssues?.length) {
return html``;
}
const issues = this.repairsIssues;
return html`
<div class="title">
${this.hass.localize("ui.panel.config.repairs.title", {
count: this.total || this.repairsIssues.length,
})}
</div>
<mwc-list>
${issues.map(
(issue) => html`
<ha-list-item
twoline
graphic="avatar"
.hasMeta=${!this.narrow}
.issue=${issue}
class=${issue.ignored ? "ignored" : ""}
@click=${this._openShowMoreDialog}
>
<img
loading="lazy"
src=${brandsUrl({
domain: issue.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
.title=${domainToName(this.hass.localize, issue.domain)}
referrerpolicy="no-referrer"
slot="graphic"
/>
<span
>${this.hass.localize(
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.title`
)}</span
>
<span slot="secondary" class="secondary">
${issue.created
? relativeTime(new Date(issue.created), this.hass.locale)
: ""}
${issue.ignored
? ` - ${this.hass.localize(
"ui.panel.config.repairs.dialog.ignored_in_version_short",
{ version: issue.dismissed_version }
)}`
: ""}
</span>
${!this.narrow
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""}
</ha-list-item>
`
)}
</mwc-list>
`;
}
private _openShowMoreDialog(ev): void {
const issue = ev.currentTarget.issue as RepairsIssue;
if (issue.is_fixable) {
showRepairsFlowDialog(this, issue);
} else {
showRepairsIssueDialog(this, {
issue,
});
}
}
static styles = css`
:host {
--mdc-list-vertical-padding: 0;
}
.title {
font-size: 16px;
padding: 16px;
padding-bottom: 0;
}
.ignored {
opacity: var(--light-secondary-opacity);
}
button.show-more {
color: var(--primary-color);
text-align: left;
cursor: pointer;
background: none;
border-width: initial;
border-style: none;
border-color: initial;
border-image: initial;
padding: 16px;
font: inherit;
}
button.show-more:focus {
outline: none;
text-decoration: underline;
}
ha-list-item {
cursor: pointer;
font-size: 16px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-repairs": HaConfigRepairs;
}
}

View File

@ -21,8 +21,8 @@ import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
@customElement("integrations-card") @customElement("integrations-startup-time")
class IntegrationsCard extends LitElement { class IntegrationsStartupTime extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@ -45,57 +45,47 @@ class IntegrationsCard extends LitElement {
} }
return html` return html`
<ha-card <mwc-list>
outlined ${this._setups?.map((setup) => {
.header=${this.hass.localize( const manifest = this._manifests && this._manifests[setup.domain];
"ui.panel.config.system_health.integration_start_time" const docLink = manifest
)} ? manifest.is_built_in
> ? documentationUrl(this.hass, `/integrations/${manifest.domain}`)
<mwc-list> : manifest.documentation
${this._setups?.map((setup) => { : "";
const manifest = this._manifests && this._manifests[setup.domain];
const docLink = manifest
? manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${manifest.domain}`
)
: manifest.documentation
: "";
const setupSeconds = setup.seconds?.toFixed(2); const setupSeconds = setup.seconds?.toFixed(2);
return html` return html`
<ha-clickable-list-item <ha-clickable-list-item
graphic="avatar" graphic="avatar"
twoline twoline
hasMeta hasMeta
openNewTab openNewTab
@click=${this._entryClicked} @click=${this._entryClicked}
href=${docLink} href=${docLink}
> >
<img <img
loading="lazy" loading="lazy"
src=${brandsUrl({ src=${brandsUrl({
domain: setup.domain, domain: setup.domain,
type: "icon", type: "icon",
useFallback: true, useFallback: true,
darkOptimized: this.hass.themes?.darkMode, darkOptimized: this.hass.themes?.darkMode,
})} })}
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
slot="graphic" slot="graphic"
/> />
<span> <span>
${domainToName(this.hass.localize, setup.domain, manifest)} ${domainToName(this.hass.localize, setup.domain, manifest)}
</span> </span>
<span slot="secondary">${setup.domain}</span> <span slot="secondary">${setup.domain}</span>
<div slot="meta"> <div slot="meta">
${setupSeconds ? html`${setupSeconds} s` : ""} ${setupSeconds ? html`${setupSeconds} s` : ""}
</div> </div>
</ha-clickable-list-item> </ha-clickable-list-item>
`; `;
})} })}
</mwc-list> </mwc-list>
</ha-card>
`; `;
} }
@ -149,6 +139,6 @@ class IntegrationsCard extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"integrations-card": IntegrationsCard; "integrations-startup-time": IntegrationsStartupTime;
} }
} }

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