mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Merge pull request #13286 from home-assistant/dev
This commit is contained in:
commit
40616b6af2
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -11,7 +11,7 @@ on:
|
||||
- master
|
||||
|
||||
env:
|
||||
NODE_VERSION: 14
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
jobs:
|
||||
|
2
.github/workflows/demo.yaml
vendored
2
.github/workflows/demo.yaml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- dev
|
||||
|
||||
env:
|
||||
NODE_VERSION: 14
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
jobs:
|
||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@ -6,8 +6,8 @@ on:
|
||||
- cron: "0 1 * * *"
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: 3.8
|
||||
NODE_VERSION: 14
|
||||
PYTHON_VERSION: "3.10"
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
permissions:
|
||||
|
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@ -6,8 +6,8 @@ on:
|
||||
- published
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: 3.8
|
||||
NODE_VERSION: 14
|
||||
PYTHON_VERSION: "3.10"
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
# Set default workflow permissions
|
||||
@ -21,7 +21,7 @@ jobs:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@v3.0.13
|
||||
uses: actions/stale@v5.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@ -8,7 +8,7 @@ on:
|
||||
- src/translations/en.json
|
||||
|
||||
env:
|
||||
NODE_VERSION: 14
|
||||
NODE_VERSION: 16
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
@ -1,5 +1,4 @@
|
||||
// Compat needs to be first import
|
||||
import "../../src/resources/compatibility";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import {
|
||||
@ -7,9 +6,14 @@ import {
|
||||
provideHass,
|
||||
} from "../../src/fake_data/provide_hass";
|
||||
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
|
||||
import "../../src/resources/compatibility";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import { selectedDemoConfig } from "./configs/demo-configs";
|
||||
import { mockAuth } from "./stubs/auth";
|
||||
import { mockConfigEntries } from "./stubs/config_entries";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
import { energyEntities } from "./stubs/entities";
|
||||
import { mockEntityRegistry } from "./stubs/entity_registry";
|
||||
import { mockEvents } from "./stubs/events";
|
||||
import { mockFrontend } from "./stubs/frontend";
|
||||
import { mockHistory } from "./stubs/history";
|
||||
@ -20,9 +24,6 @@ import { mockShoppingList } from "./stubs/shopping_list";
|
||||
import { mockSystemLog } from "./stubs/system_log";
|
||||
import { mockTemplate } from "./stubs/template";
|
||||
import { mockTranslations } from "./stubs/translations";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
import { mockConfig } from "./stubs/config";
|
||||
import { energyEntities } from "./stubs/entities";
|
||||
|
||||
class HaDemo extends HomeAssistantAppEl {
|
||||
protected async _initializeHass() {
|
||||
@ -51,8 +52,36 @@ class HaDemo extends HomeAssistantAppEl {
|
||||
mockMediaPlayer(hass);
|
||||
mockFrontend(hass);
|
||||
mockEnergy(hass);
|
||||
mockConfig(hass);
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
entity_id: "sensor.co2_intensity",
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "co2signal",
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
entity_id: "sensor.grid_fossil_fuel_percentage",
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "co2signal",
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
},
|
||||
]);
|
||||
|
||||
hass.addEntities(energyEntities());
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
]);
|
||||
};
|
20
demo/src/stubs/config_entries.ts
Normal file
20
demo/src/stubs/config_entries.ts
Normal 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,
|
||||
},
|
||||
]);
|
||||
};
|
@ -4,4 +4,6 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
export const mockEntityRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: EntityRegistryEntry[] = []
|
||||
) => hass.mockWS("config/entity_registry/list", () => data);
|
||||
) => {
|
||||
hass.mockWS("config/entity_registry/list", () => data);
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ module.exports = [
|
||||
{
|
||||
category: "lovelace",
|
||||
// Label for in the sidebar
|
||||
header: "Lovelace",
|
||||
header: "Dashboards",
|
||||
// Specify order of pages. Any pages in the category folder but not listed here will
|
||||
// automatically be added after the pages listed here.
|
||||
pages: ["introduction"],
|
||||
@ -34,7 +34,7 @@ module.exports = [
|
||||
},
|
||||
{
|
||||
category: "misc",
|
||||
header: "Miscelaneous",
|
||||
header: "Miscellaneous",
|
||||
},
|
||||
{
|
||||
category: "brand",
|
||||
|
@ -31,7 +31,7 @@ const ENTITIES = [
|
||||
friendly_name: "Office Light",
|
||||
}),
|
||||
getEntity("fan", "kitchen", "on", {
|
||||
friendly_name: "Second Office Fan",
|
||||
friendly_name: "Kitchen Fan",
|
||||
}),
|
||||
getEntity("binary_sensor", "kitchen_door", "on", {
|
||||
friendly_name: "Office Door",
|
||||
@ -102,7 +102,7 @@ class DemoArea extends LitElement {
|
||||
picture: "/images/office.jpg",
|
||||
},
|
||||
{
|
||||
name: "Second Office",
|
||||
name: "Kitchen",
|
||||
area_id: "kitchen",
|
||||
picture: "/images/kitchen.png",
|
||||
},
|
||||
|
@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Introduction
|
||||
---
|
||||
Lovelace has many different cards. Each card allows the user to tell
|
||||
Dashboards have many different cards. Each card allows the user to tell
|
||||
a different story about what is going on in their house. These cards
|
||||
are very customizable, as no household is the same.
|
||||
|
||||
This gallery helps our developers and designers to see all the
|
||||
different states that each card can be in.
|
||||
|
||||
Check [the Lovelace documentation](https://www.home-assistant.io/lovelace) for instructions on how to get started with Lovelace.
|
||||
Check [the Dashboards documentation](https://www.home-assistant.io/dashboards/) for instructions on how to get started with Dashboards.
|
||||
|
@ -194,6 +194,7 @@ const createEntityRegistryEntries = (
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "updater",
|
||||
has_entity_name: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -69,7 +69,7 @@ const ENTITIES = [
|
||||
effect_list: ["random", "colorloop"],
|
||||
}),
|
||||
getEntity("light", "color_RGB_light", "on", {
|
||||
friendly_name: "Color Effets Light",
|
||||
friendly_name: "Color Effects Light",
|
||||
brightness: 255,
|
||||
rgb_color: [30, 100, 255],
|
||||
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
|
||||
|
@ -81,10 +81,10 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
? this.supervisor.localize(
|
||||
"common.new_version_available"
|
||||
)
|
||||
: this.supervisor.localize("addon.installed")
|
||||
: this.supervisor.localize("addon.state.installed")
|
||||
: addon.available
|
||||
? this.supervisor.localize("addon.not_installed")
|
||||
: this.supervisor.localize("addon.not_available")}
|
||||
? this.supervisor.localize("addon.state.not_installed")
|
||||
: this.supervisor.localize("addon.state.not_available")}
|
||||
.iconClass=${addon.installed
|
||||
? addon.update_available
|
||||
? "update"
|
||||
|
@ -336,7 +336,7 @@ class HassioAddonConfig extends LitElement {
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.common.update_available",
|
||||
"addon.failed_to_reset",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
|
@ -81,7 +81,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.documentation.get_logs",
|
||||
"addon.documentation.get_documentation",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
|
@ -168,23 +168,24 @@ export class SupervisorBackupContent extends LitElement {
|
||||
: ""}
|
||||
${this.backupType === "partial"
|
||||
? html`<div class="partial-picker">
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
label="Home Assistant"
|
||||
.iconPath=${mdiHomeAssistant}
|
||||
.version=${this.backup
|
||||
? this.backup.homeassistant
|
||||
: this.hass.config.version}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.homeAssistant}
|
||||
@change=${this.toggleHomeAssistant}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
|
||||
${this.backup?.homeassistant
|
||||
? html`<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
label="Home Assistant"
|
||||
.iconPath=${mdiHomeAssistant}
|
||||
.version=${this.backup
|
||||
? this.backup.homeassistant
|
||||
: this.hass.config.version}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.homeAssistant}
|
||||
@change=${this.toggleHomeAssistant}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
: ""}
|
||||
${foldersSection?.templates.length
|
||||
? html`
|
||||
<ha-formfield
|
||||
|
@ -201,26 +201,24 @@ class HassioBackupDialog
|
||||
}
|
||||
|
||||
if (!this._dialogParams?.onboarding) {
|
||||
this.hass!.callApi(
|
||||
"POST",
|
||||
try {
|
||||
await this.hass!.callApi(
|
||||
"POST",
|
||||
|
||||
`hassio/${
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/restore/partial`,
|
||||
backupDetails
|
||||
).then(
|
||||
() => {
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
`hassio/${
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/restore/partial`,
|
||||
backupDetails
|
||||
);
|
||||
this.closeDialog();
|
||||
} catch (error: any) {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
} else {
|
||||
fireEvent(this, "restoring");
|
||||
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
||||
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(backupDetails),
|
||||
});
|
||||
|
@ -25,7 +25,7 @@ import {
|
||||
} from "../../src/data/supervisor/supervisor";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import { HomeAssistant, Route, TranslationDict } from "../../src/types";
|
||||
import { getTranslation } from "../../src/util/common-translation";
|
||||
|
||||
declare global {
|
||||
@ -124,9 +124,13 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
||||
|
||||
this.supervisor = {
|
||||
...this.supervisor,
|
||||
localize: await computeLocalize(this.constructor.prototype, language, {
|
||||
[language]: data,
|
||||
}),
|
||||
localize: await computeLocalize<TranslationDict["supervisor"]>(
|
||||
this.constructor.prototype,
|
||||
language,
|
||||
{
|
||||
[language]: data,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
import {
|
||||
UNHEALTHY_REASON_URL,
|
||||
UNSUPPORTED_REASON_URL,
|
||||
} from "../../../src/panels/config/system-health/ha-config-system-health";
|
||||
} from "../../../src/panels/config/repairs/dialog-system-information";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
|
@ -72,8 +72,8 @@
|
||||
"@material/mwc-textfield": "0.25.3",
|
||||
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
||||
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
||||
"@mdi/js": "6.9.96",
|
||||
"@mdi/svg": "6.9.96",
|
||||
"@mdi/js": "7.0.96",
|
||||
"@mdi/svg": "7.0.96",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20220707.1"
|
||||
version = "20220727.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
@ -23,8 +23,3 @@ include-package-data = true
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["hass_frontend*"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = 3.4
|
||||
show_error_codes = true
|
||||
strict = true
|
||||
|
@ -47,7 +47,7 @@ import {
|
||||
mdiRobotVacuum,
|
||||
mdiScriptText,
|
||||
mdiSineWave,
|
||||
mdiTextToSpeech,
|
||||
mdiMicrophoneMessage,
|
||||
mdiThermometer,
|
||||
mdiThermostat,
|
||||
mdiTimerOutline,
|
||||
@ -74,8 +74,9 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
camera: mdiVideo,
|
||||
climate: mdiThermostat,
|
||||
configurator: mdiCog,
|
||||
conversation: mdiTextToSpeech,
|
||||
conversation: mdiMicrophoneMessage,
|
||||
counter: mdiCounter,
|
||||
demo: mdiHomeAssistant,
|
||||
fan: mdiFan,
|
||||
google_assistant: mdiGoogleAssistant,
|
||||
group: mdiGoogleCirclesCommunities,
|
||||
|
@ -5,8 +5,7 @@ export type LeafletModuleType = typeof import("leaflet");
|
||||
export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
||||
|
||||
export const setupLeafletMap = async (
|
||||
mapElement: HTMLElement,
|
||||
darkMode?: boolean
|
||||
mapElement: HTMLElement
|
||||
): Promise<[Map, LeafletModuleType, TileLayer]> => {
|
||||
if (!mapElement.parentNode) {
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
@ -23,7 +22,7 @@ export const setupLeafletMap = async (
|
||||
mapElement.parentNode.appendChild(style);
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
|
||||
const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map);
|
||||
const tileLayer = createTileLayer(Leaflet).addTo(map);
|
||||
|
||||
return [map, Leaflet, tileLayer];
|
||||
};
|
||||
@ -31,23 +30,19 @@ export const setupLeafletMap = async (
|
||||
export const replaceTileLayer = (
|
||||
leaflet: LeafletModuleType,
|
||||
map: Map,
|
||||
tileLayer: TileLayer,
|
||||
darkMode: boolean
|
||||
tileLayer: TileLayer
|
||||
): TileLayer => {
|
||||
map.removeLayer(tileLayer);
|
||||
tileLayer = createTileLayer(leaflet, darkMode);
|
||||
tileLayer = createTileLayer(leaflet);
|
||||
tileLayer.addTo(map);
|
||||
return tileLayer;
|
||||
};
|
||||
|
||||
const createTileLayer = (
|
||||
leaflet: LeafletModuleType,
|
||||
darkMode: boolean
|
||||
): TileLayer =>
|
||||
const createTileLayer = (leaflet: LeafletModuleType): TileLayer =>
|
||||
leaflet.tileLayer(
|
||||
`https://{s}.basemaps.cartocdn.com/${
|
||||
darkMode ? "dark_all" : "light_all"
|
||||
}/{z}/{x}/{y}${leaflet.Browser.retina ? "@2x.png" : ".png"}`,
|
||||
`https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}${
|
||||
leaflet.Browser.retina ? "@2x.png" : ".png"
|
||||
}`,
|
||||
{
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
mdiCalendar,
|
||||
mdiCast,
|
||||
mdiCastConnected,
|
||||
mdiCastOff,
|
||||
mdiChartSankey,
|
||||
mdiCheckCircleOutline,
|
||||
mdiClock,
|
||||
@ -25,7 +26,15 @@ import {
|
||||
mdiPowerPlug,
|
||||
mdiPowerPlugOff,
|
||||
mdiRestart,
|
||||
mdiSpeaker,
|
||||
mdiSpeakerOff,
|
||||
mdiSpeakerPause,
|
||||
mdiSpeakerPlay,
|
||||
mdiSwapHorizontal,
|
||||
mdiTelevision,
|
||||
mdiTelevisionOff,
|
||||
mdiTelevisionPause,
|
||||
mdiTelevisionPlay,
|
||||
mdiToggleSwitchVariant,
|
||||
mdiToggleSwitchVariantOff,
|
||||
mdiWeatherNight,
|
||||
@ -127,7 +136,40 @@ export const domainIconWithoutDefault = (
|
||||
}
|
||||
|
||||
case "media_player":
|
||||
return compareState === "playing" ? mdiCastConnected : mdiCast;
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
case "speaker":
|
||||
switch (compareState) {
|
||||
case "playing":
|
||||
return mdiSpeakerPlay;
|
||||
case "paused":
|
||||
return mdiSpeakerPause;
|
||||
case "off":
|
||||
return mdiSpeakerOff;
|
||||
default:
|
||||
return mdiSpeaker;
|
||||
}
|
||||
case "tv":
|
||||
switch (compareState) {
|
||||
case "playing":
|
||||
return mdiTelevisionPlay;
|
||||
case "paused":
|
||||
return mdiTelevisionPause;
|
||||
case "off":
|
||||
return mdiTelevisionOff;
|
||||
default:
|
||||
return mdiTelevision;
|
||||
}
|
||||
default:
|
||||
switch (compareState) {
|
||||
case "playing":
|
||||
case "paused":
|
||||
return mdiCastConnected;
|
||||
case "off":
|
||||
return mdiCastOff;
|
||||
default:
|
||||
return mdiCast;
|
||||
}
|
||||
}
|
||||
|
||||
case "switch":
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
|
88
src/common/integrations/protocolIntegrationPicked.ts
Normal file
88
src/common/integrations/protocolIntegrationPicked.ts
Normal 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");
|
||||
}
|
||||
};
|
@ -3,10 +3,39 @@ import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-plur
|
||||
import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/lib/should-polyfill";
|
||||
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
|
||||
import IntlMessageFormat from "intl-messageformat";
|
||||
import { Resources } from "../../types";
|
||||
import { Resources, TranslationDict } from "../../types";
|
||||
import { getLocalLanguage } from "../../util/common-translation";
|
||||
|
||||
export type LocalizeFunc = (key: string, ...args: any[]) => string;
|
||||
// Exclude some patterns from key type checking for now
|
||||
// These are intended to be removed as errors are fixed
|
||||
// Fixing component category will require tighter definition of types from backend and/or web socket
|
||||
type LocalizeKeyExceptions =
|
||||
| `${string}`
|
||||
| `panel.${string}`
|
||||
| `state.${string}`
|
||||
| `state_attributes.${string}`
|
||||
| `state_badge.${string}`
|
||||
| `ui.${string}`
|
||||
| `${keyof TranslationDict["supervisor"]}.${string}`
|
||||
| `component.${string}`;
|
||||
|
||||
// Tweaked from https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types
|
||||
type FlattenObjectKeys<
|
||||
T extends Record<string, any>,
|
||||
Key extends keyof T = keyof T
|
||||
> = Key extends string
|
||||
? T[Key] extends Record<string, unknown>
|
||||
? `${Key}.${FlattenObjectKeys<T[Key]>}`
|
||||
: `${Key}`
|
||||
: never;
|
||||
|
||||
export type LocalizeFunc<
|
||||
Dict extends Record<string, unknown> = TranslationDict
|
||||
> = (
|
||||
key: FlattenObjectKeys<Dict> | LocalizeKeyExceptions,
|
||||
...args: any[]
|
||||
) => string;
|
||||
|
||||
interface FormatType {
|
||||
[format: string]: any;
|
||||
}
|
||||
@ -65,12 +94,14 @@ export const polyfillsLoaded =
|
||||
* }
|
||||
*/
|
||||
|
||||
export const computeLocalize = async (
|
||||
export const computeLocalize = async <
|
||||
Dict extends Record<string, unknown> = TranslationDict
|
||||
>(
|
||||
cache: any,
|
||||
language: string,
|
||||
resources: Resources,
|
||||
formats?: FormatsType
|
||||
): Promise<LocalizeFunc> => {
|
||||
): Promise<LocalizeFunc<Dict>> => {
|
||||
if (polyfillsLoaded) {
|
||||
await polyfillsLoaded;
|
||||
}
|
||||
|
@ -188,6 +188,10 @@ export default class HaChartBase extends LitElement {
|
||||
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
|
||||
"--secondary-text-color"
|
||||
);
|
||||
ChartConstructor.defaults.font.family =
|
||||
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
|
||||
computedStyles.getPropertyValue("--mdc-typography-font-family") ||
|
||||
"Roboto, Noto, sans-serif";
|
||||
|
||||
this.chart = new ChartConstructor(ctx, {
|
||||
type: this.chartType,
|
||||
@ -376,6 +380,7 @@ export default class HaChartBase extends LitElement {
|
||||
.chartTooltip .title {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
direction: ltr;
|
||||
}
|
||||
.chartTooltip .footer {
|
||||
font-weight: 500;
|
||||
|
@ -84,20 +84,20 @@ class HaAddonPicker extends LitElement {
|
||||
} else {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.componencts.addon-picker.error.no_supervisor.title"
|
||||
"ui.components.addon-picker.error.no_supervisor.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.componencts.addon-picker.error.no_supervisor.description"
|
||||
"ui.components.addon-picker.error.no_supervisor.description"
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.componencts.addon-picker.error.fetch_addons.title"
|
||||
"ui.components.addon-picker.error.fetch_addons.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.componencts.addon-picker.error.fetch_addons.description"
|
||||
"ui.components.addon-picker.error.fetch_addons.description"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ class HaAttributes extends LitElement {
|
||||
css`
|
||||
.attribute-container {
|
||||
margin-bottom: 8px;
|
||||
direction: ltr;
|
||||
}
|
||||
.data-entry {
|
||||
display: flex;
|
||||
|
@ -51,7 +51,7 @@ class HaBluePrintPicker extends LitElement {
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.blueprint-picker.label")}
|
||||
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this.value}
|
||||
|
@ -11,7 +11,7 @@ export const createCloseHeading = (
|
||||
hass: HomeAssistant,
|
||||
title: string | TemplateResult
|
||||
) => html`
|
||||
<span class="header_title">${title}</span>
|
||||
<div class="header_title">${title}</div>
|
||||
<ha-icon-button
|
||||
.label=${hass.localize("ui.dialogs.generic.close")}
|
||||
.path=${mdiClose}
|
||||
@ -40,10 +40,13 @@ export class HaDialog extends DialogBase {
|
||||
z-index: var(--dialog-z-index, 7);
|
||||
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: 400;
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 24px);
|
||||
}
|
||||
.mdc-dialog__actions span:nth-child(1) {
|
||||
flex: var(--secondary-action-button-flex, unset);
|
||||
@ -54,17 +57,23 @@ export class HaDialog extends DialogBase {
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertial-align-dialog, center);
|
||||
}
|
||||
.mdc-dialog__title {
|
||||
padding: 24px 24px 0 24px;
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
padding: 0 24px 24px 24px;
|
||||
}
|
||||
.mdc-dialog__title::before {
|
||||
display: block;
|
||||
height: 20px;
|
||||
height: 0px;
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__content {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: var(--dialog-content-padding, 20px 24px);
|
||||
padding: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
||||
padding-bottom: max(
|
||||
var(--dialog-content-padding, 20px),
|
||||
var(--dialog-content-padding, 24px),
|
||||
env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
@ -72,10 +81,7 @@ export class HaDialog extends DialogBase {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
top: var(--dialog-surface-top);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-card-border-radius, 4px)
|
||||
);
|
||||
border-radius: var(--ha-dialog-border-radius, 28px);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
@ -89,8 +95,8 @@ export class HaDialog extends DialogBase {
|
||||
color: inherit;
|
||||
}
|
||||
.header_title {
|
||||
margin-right: 40px;
|
||||
margin-inline-end: 40px;
|
||||
margin-right: 32px;
|
||||
margin-inline-end: 32px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
@ -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 = {};
|
||||
|
||||
|
@ -52,6 +52,11 @@ export class HaSelect extends SelectBase {
|
||||
inset-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
|
||||
inset-inline-start: 48px;
|
||||
inset-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select .mdc-select__anchor {
|
||||
padding-inline-start: 12px;
|
||||
padding-inline-end: 0px;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
@ -11,7 +12,11 @@ import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import { AreaSelector } from "../../data/selector";
|
||||
import type { AreaSelector } from "../../data/selector";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
} from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-area-picker";
|
||||
@ -29,13 +34,15 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
@ -45,7 +52,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
(this.selector.area.device?.integration ||
|
||||
@ -58,7 +65,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
(this.selector.area.device?.integration ||
|
||||
this.selector.area.entity?.integration) &&
|
||||
@ -77,12 +84,6 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-area-picker>
|
||||
@ -98,27 +99,22 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-areas-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
|
||||
const filterIntegration = this.selector.area.entity?.integration;
|
||||
if (
|
||||
filterIntegration &&
|
||||
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
|
||||
) {
|
||||
return false;
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (!this.selector.area.entity) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
return filterSelectorEntities(
|
||||
this.selector.area.entity,
|
||||
entity,
|
||||
this._entitySources
|
||||
);
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
@ -126,47 +122,17 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const {
|
||||
manufacturer: filterManufacturer,
|
||||
model: filterModel,
|
||||
integration: filterIntegration,
|
||||
} = this.selector.area.device;
|
||||
const deviceIntegrations =
|
||||
this._entitySources && this._entities
|
||||
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
||||
: undefined;
|
||||
|
||||
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
|
||||
return false;
|
||||
}
|
||||
if (filterModel && device.model !== filterModel) {
|
||||
return false;
|
||||
}
|
||||
if (filterIntegration && this._entitySources && this._entities) {
|
||||
const deviceIntegrations = this._deviceIntegrations(
|
||||
this._entitySources,
|
||||
this._entities
|
||||
);
|
||||
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return filterSelectorDevices(
|
||||
this.selector.area.device,
|
||||
device,
|
||||
deviceIntegrations
|
||||
);
|
||||
};
|
||||
|
||||
private _deviceIntegrations = memoizeOne(
|
||||
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
for (const entity of entities) {
|
||||
const source = entitySources[entity.entity_id];
|
||||
if (!source?.domain) {
|
||||
continue;
|
||||
}
|
||||
if (!deviceIntegrations[entity.device_id!]) {
|
||||
deviceIntegrations[entity.device_id!] = [];
|
||||
}
|
||||
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||
}
|
||||
return deviceIntegrations;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -2,8 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ConfigEntry } from "../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
@ -13,6 +13,7 @@ import {
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import type { DeviceSelector } from "../../data/selector";
|
||||
import { filterSelectorDevices } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../device/ha-device-picker";
|
||||
@ -34,12 +35,12 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() public _configEntries?: ConfigEntry[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
@ -107,48 +108,17 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
const {
|
||||
manufacturer: filterManufacturer,
|
||||
model: filterModel,
|
||||
integration: filterIntegration,
|
||||
} = this.selector.device;
|
||||
const deviceIntegrations =
|
||||
this._entitySources && this._entities
|
||||
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
||||
: undefined;
|
||||
|
||||
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
|
||||
return false;
|
||||
}
|
||||
if (filterModel && device.model !== filterModel) {
|
||||
return false;
|
||||
}
|
||||
if (filterIntegration && this._entitySources && this._entities) {
|
||||
const deviceIntegrations = this._deviceIntegrations(
|
||||
this._entitySources,
|
||||
this._entities
|
||||
);
|
||||
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return filterSelectorDevices(
|
||||
this.selector.device,
|
||||
device,
|
||||
deviceIntegrations
|
||||
);
|
||||
};
|
||||
|
||||
private _deviceIntegrations = memoizeOne(
|
||||
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
for (const entity of entities) {
|
||||
const source = entitySources[entity.entity_id];
|
||||
if (!source?.domain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!deviceIntegrations[entity.device_id!]) {
|
||||
deviceIntegrations[entity.device_id!] = [];
|
||||
}
|
||||
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||
}
|
||||
return deviceIntegrations;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import { EntitySelector } from "../../data/selector";
|
||||
import type { EntitySelector } from "../../data/selector";
|
||||
import { filterSelectorEntities } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entities-picker";
|
||||
import "../entity/ha-entity-picker";
|
||||
@ -73,37 +73,8 @@ export class HaEntitySelector extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
const {
|
||||
domain: filterDomain,
|
||||
device_class: filterDeviceClass,
|
||||
integration: filterIntegration,
|
||||
} = this.selector.entity;
|
||||
|
||||
if (filterDomain) {
|
||||
const entityDomain = computeStateDomain(entity);
|
||||
if (
|
||||
Array.isArray(filterDomain)
|
||||
? !filterDomain.includes(entityDomain)
|
||||
: entityDomain !== filterDomain
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (
|
||||
filterDeviceClass &&
|
||||
entity.attributes.device_class !== filterDeviceClass
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filterIntegration &&
|
||||
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
private _filterEntities = (entity: HassEntity): boolean =>
|
||||
filterSelectorEntities(this.selector.entity, entity, this._entitySources);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -4,9 +4,9 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { NumberSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../ha-textfield";
|
||||
import "../ha-input-helper-text";
|
||||
|
||||
@customElement("ha-selector-number")
|
||||
export class HaNumberSelector extends LitElement {
|
||||
@ -30,21 +30,25 @@ export class HaNumberSelector extends LitElement {
|
||||
const isBox = this.selector.number.mode === "box";
|
||||
|
||||
return html`
|
||||
${this.label ? html`${this.label}${this.required ? " *" : ""}` : ""}
|
||||
<div class="input">
|
||||
${!isBox
|
||||
? html`<ha-slider
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this._value}
|
||||
.step=${this.selector.number.step ?? 1}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
pin
|
||||
ignore-bar-touch
|
||||
@change=${this._handleSliderChange}
|
||||
>
|
||||
</ha-slider>`
|
||||
? html`
|
||||
${this.label
|
||||
? html`${this.label}${this.required ? " *" : ""}`
|
||||
: ""}
|
||||
<ha-slider
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this._value}
|
||||
.step=${this.selector.number.step ?? 1}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
pin
|
||||
ignore-bar-touch
|
||||
@change=${this._handleSliderChange}
|
||||
>
|
||||
</ha-slider>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
inputMode="numeric"
|
||||
|
@ -3,17 +3,33 @@ import {
|
||||
HassServiceTarget,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { TargetSelector } from "../../data/selector";
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
getDeviceIntegrationLookup,
|
||||
} from "../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { subscribeEntityRegistry } from "../../data/entity_registry";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
TargetSelector,
|
||||
} from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-target-picker";
|
||||
|
||||
@customElement("ha-selector-target")
|
||||
@ -28,119 +44,82 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() private _entityPlaformLookup?: Record<string, string>;
|
||||
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
const entityLookup = {};
|
||||
for (const confEnt of entities) {
|
||||
if (!confEnt.platform) {
|
||||
continue;
|
||||
}
|
||||
entityLookup[confEnt.entity_id] = confEnt.platform;
|
||||
}
|
||||
this._entityPlaformLookup = entityLookup;
|
||||
this._entities = entities.filter((entity) => entity.device_id !== null);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (
|
||||
oldSelector !== this.selector &&
|
||||
(this.selector.target.device?.integration ||
|
||||
this.selector.target.entity?.integration)
|
||||
) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
this.selector.target.device?.integration &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
(this.selector.target.device?.integration ||
|
||||
this.selector.target.entity?.integration) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityRegFilter=${this._filterRegEntities}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.target.entity?.device_class
|
||||
? [this.selector.target.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.target.entity?.domain
|
||||
? [this.selector.target.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (
|
||||
this.selector.target.entity?.integration ||
|
||||
this.selector.target.device?.integration
|
||||
) {
|
||||
if (
|
||||
!this._entityPlaformLookup ||
|
||||
this._entityPlaformLookup[entity.entity_id] !==
|
||||
(this.selector.target.entity?.integration ||
|
||||
this.selector.target.device?.integration)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!this.selector.target.entity) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => {
|
||||
if (this.selector.target.entity?.integration) {
|
||||
if (entity.platform !== this.selector.target.entity.integration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return filterSelectorEntities(
|
||||
this.selector.target.entity,
|
||||
entity,
|
||||
this._entitySources
|
||||
);
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (
|
||||
this.selector.target.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.target.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
if (!this.selector.target.device) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.selector.target.device?.model &&
|
||||
device.model !== this.selector.target.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.target.device?.integration ||
|
||||
this.selector.target.entity?.integration
|
||||
) {
|
||||
if (
|
||||
!this._configEntries?.some((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) =>
|
||||
entry.domain === this.selector.target.device?.integration ||
|
||||
entry.domain === this.selector.target.entity?.integration
|
||||
const deviceIntegrations =
|
||||
this._entitySources && this._entities
|
||||
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
||||
: undefined;
|
||||
|
||||
return filterSelectorDevices(
|
||||
this.selector.target.device,
|
||||
device,
|
||||
deviceIntegrations
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
|
@ -21,6 +21,7 @@ import "@polymer/paper-item/paper-icon-item";
|
||||
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@ -44,7 +45,9 @@ import {
|
||||
PersistentNotification,
|
||||
subscribeNotifications,
|
||||
} from "../data/persistent_notification";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
import { updateCanInstall, UpdateEntity } from "../data/update";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
@ -177,7 +180,7 @@ const computePanels = memoizeOne(
|
||||
let Sortable;
|
||||
|
||||
@customElement("ha-sidebar")
|
||||
class HaSidebar extends LitElement {
|
||||
class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
@ -192,6 +195,8 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@state() private _updatesCount = 0;
|
||||
|
||||
@state() private _issuesCount = 0;
|
||||
|
||||
@state() private _renderEmptySortable = false;
|
||||
|
||||
private _mouseLeaveTimeout?: number;
|
||||
@ -214,6 +219,16 @@ class HaSidebar extends LitElement {
|
||||
|
||||
private _sortable?;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
|
||||
this._issuesCount = repairs.issues.filter(
|
||||
(issue) => !issue.ignored
|
||||
).length;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
@ -238,6 +253,7 @@ class HaSidebar extends LitElement {
|
||||
changedProps.has("alwaysExpand") ||
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_updatesCount") ||
|
||||
changedProps.has("_issuesCount") ||
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("editMode") ||
|
||||
changedProps.has("_renderEmptySortable") ||
|
||||
@ -500,7 +516,7 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
|
||||
private _renderConfiguration(title: string | null) {
|
||||
return html` <a
|
||||
return html`<a
|
||||
class="configuration-container"
|
||||
role="option"
|
||||
href="/config"
|
||||
@ -511,17 +527,20 @@ class HaSidebar extends LitElement {
|
||||
>
|
||||
<paper-icon-item class="configuration" role="option">
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
|
||||
${!this.alwaysExpand && this._updatesCount > 0
|
||||
${!this.alwaysExpand &&
|
||||
(this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`
|
||||
<span class="configuration-badge" slot="item-icon">
|
||||
${this._updatesCount}
|
||||
${this._updatesCount + this._issuesCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text">${title}</span>
|
||||
${this.alwaysExpand && this._updatesCount > 0
|
||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`
|
||||
<span class="configuration-badge">${this._updatesCount}</span>
|
||||
<span class="configuration-badge"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
|
@ -314,7 +314,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
class="mdc-chip__icon mdc-chip__icon--trailing"
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
.label=${this.hass.localize("ui.components.target-picker.expand")}
|
||||
.label=${this.hass.localize("ui.components.target-picker.remove")}
|
||||
.path=${mdiClose}
|
||||
hideTooltip
|
||||
.id=${id}
|
||||
|
@ -83,7 +83,7 @@ export class HaTextField extends TextFieldBase {
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: var(--text-field-text-align);
|
||||
text-align: var(--text-field-text-align, start);
|
||||
}
|
||||
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
|
@ -23,7 +23,7 @@ export class HaThemePicker extends LitElement {
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass!.localize("ui.components.theme_picker.theme")}
|
||||
this.hass!.localize("ui.components.theme-picker.theme")}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@ -34,7 +34,7 @@ export class HaThemePicker extends LitElement {
|
||||
>
|
||||
<mwc-list-item value="remove"
|
||||
>${this.hass!.localize(
|
||||
"ui.components.theme_picker.no_theme"
|
||||
"ui.components.theme-picker.no_theme"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
${Object.keys(this.hass!.themes.themes)
|
||||
|
@ -41,7 +41,7 @@ export class HaYamlEditor extends LitElement {
|
||||
try {
|
||||
this._yaml =
|
||||
value && !isEmpty(value)
|
||||
? dump(value, { schema: this.yamlSchema })
|
||||
? dump(value, { schema: this.yamlSchema, quotingType: '"' })
|
||||
: "";
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -6,21 +6,19 @@ import {
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
TileLayer,
|
||||
} from "leaflet";
|
||||
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
replaceTileLayer,
|
||||
setupLeafletMap,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import "./ha-entity-marker";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-button";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import "./ha-entity-marker";
|
||||
|
||||
const getEntityId = (entity: string | HaMapEntity): string =>
|
||||
typeof entity === "string" ? entity : entity.entity_id;
|
||||
@ -60,8 +58,6 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private Leaflet?: LeafletModuleType;
|
||||
|
||||
private _tileLayer?: TileLayer;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
private _mapItems: Array<Marker | Circle> = [];
|
||||
@ -142,12 +138,6 @@ export class HaMap extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
|
||||
this._tileLayer = replaceTileLayer(
|
||||
this.Leaflet!,
|
||||
this.leafletMap!,
|
||||
this._tileLayer!,
|
||||
darkMode
|
||||
);
|
||||
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
|
||||
}
|
||||
|
||||
@ -159,10 +149,7 @@ export class HaMap extends ReactiveElement {
|
||||
this.shadowRoot!.append(map);
|
||||
}
|
||||
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
|
||||
[this.leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
map,
|
||||
darkMode
|
||||
);
|
||||
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map);
|
||||
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
|
||||
this._loaded = true;
|
||||
}
|
||||
@ -473,6 +460,13 @@ export class HaMap extends ReactiveElement {
|
||||
.dark {
|
||||
color: #ffffff;
|
||||
}
|
||||
.leaflet-tile-pane {
|
||||
filter: var(--map-filter);
|
||||
}
|
||||
.dark .leaflet-bar a {
|
||||
background: var(--card-background-color);
|
||||
color: #ffffff;
|
||||
}
|
||||
.leaflet-marker-draggable {
|
||||
cursor: move !important;
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ export interface ConfigEntry {
|
||||
| "migration_error"
|
||||
| "setup_retry"
|
||||
| "not_loaded"
|
||||
| "failed_unload";
|
||||
| "failed_unload"
|
||||
| "setup_in_progress";
|
||||
supports_options: boolean;
|
||||
supports_remove_device: boolean;
|
||||
supports_unload: boolean;
|
||||
@ -28,12 +29,21 @@ export type ConfigEntryMutableParams = Partial<
|
||||
>
|
||||
>;
|
||||
|
||||
// https://github.com/home-assistant/core/blob/2286dea636fda001f03433ba14d7adbda43979e5/homeassistant/config_entries.py#L81
|
||||
export const ERROR_STATES: ConfigEntry["state"][] = [
|
||||
"migration_error",
|
||||
"setup_error",
|
||||
"setup_retry",
|
||||
];
|
||||
|
||||
// https://github.com/home-assistant/core/blob/2286dea636fda001f03433ba14d7adbda43979e5/homeassistant/config_entries.py#L81
|
||||
export const RECOVERABLE_STATES: ConfigEntry["state"][] = [
|
||||
"not_loaded",
|
||||
"loaded",
|
||||
"setup_error",
|
||||
"setup_retry",
|
||||
];
|
||||
|
||||
export const getConfigEntries = (
|
||||
hass: HomeAssistant,
|
||||
filters?: { type?: "helper" | "integration"; domain?: string }
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { EntityRegistryEntry } from "./entity_registry";
|
||||
import type { EntitySources } from "./entity_sources";
|
||||
|
||||
export interface DeviceRegistryEntry {
|
||||
id: string;
|
||||
@ -20,7 +21,7 @@ export interface DeviceRegistryEntry {
|
||||
area_id: string | null;
|
||||
name_by_user: string | null;
|
||||
entry_type: "service" | null;
|
||||
disabled_by: string | null;
|
||||
disabled_by: "user" | "integration" | "config_entry" | null;
|
||||
configuration_url: string | null;
|
||||
}
|
||||
|
||||
@ -142,3 +143,23 @@ export const getDeviceEntityLookup = (
|
||||
}
|
||||
return deviceEntityLookup;
|
||||
};
|
||||
|
||||
export const getDeviceIntegrationLookup = (
|
||||
entitySources: EntitySources,
|
||||
entities: EntityRegistryEntry[]
|
||||
): Record<string, string[]> => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
for (const entity of entities) {
|
||||
const source = entitySources[entity.entity_id];
|
||||
if (!source?.domain || entity.device_id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!deviceIntegrations[entity.device_id!]) {
|
||||
deviceIntegrations[entity.device_id!] = [];
|
||||
}
|
||||
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||
}
|
||||
return deviceIntegrations;
|
||||
};
|
||||
|
@ -13,15 +13,16 @@ export interface EntityRegistryEntry {
|
||||
config_entry_id: string | null;
|
||||
device_id: string | null;
|
||||
area_id: string | null;
|
||||
disabled_by: string | null;
|
||||
hidden_by: string | null;
|
||||
disabled_by: "user" | "device" | "integration" | "config_entry" | null;
|
||||
hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
|
||||
entity_category: "config" | "diagnostic" | null;
|
||||
has_entity_name: boolean;
|
||||
original_name?: string;
|
||||
}
|
||||
|
||||
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
||||
unique_id: string;
|
||||
capabilities: Record<string, unknown>;
|
||||
original_name?: string;
|
||||
original_icon?: string;
|
||||
device_class?: string;
|
||||
original_device_class?: string;
|
||||
@ -37,6 +38,10 @@ export interface SensorEntityOptions {
|
||||
unit_of_measurement?: string | null;
|
||||
}
|
||||
|
||||
export interface NumberEntityOptions {
|
||||
unit_of_measurement?: string | null;
|
||||
}
|
||||
|
||||
export interface WeatherEntityOptions {
|
||||
precipitation_unit?: string | null;
|
||||
pressure_unit?: string | null;
|
||||
@ -155,3 +160,16 @@ export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) =>
|
||||
entries.sort((entry1, entry2) =>
|
||||
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
|
||||
);
|
||||
|
||||
export const getEntityPlatformLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
): Record<string, string> => {
|
||||
const entityLookup = {};
|
||||
for (const confEnt of entities) {
|
||||
if (!confEnt.platform) {
|
||||
continue;
|
||||
}
|
||||
entityLookup[confEnt.entity_id] = confEnt.platform;
|
||||
}
|
||||
return entityLookup;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
|
||||
import {
|
||||
@ -268,7 +268,8 @@ const processTimelineEntity = (
|
||||
localize: LocalizeFunc,
|
||||
language: FrontendLocaleData,
|
||||
entityId: string,
|
||||
states: EntityHistoryState[]
|
||||
states: EntityHistoryState[],
|
||||
current_state: HassEntity | undefined
|
||||
): TimelineEntity => {
|
||||
const data: TimelineState[] = [];
|
||||
const first: EntityHistoryState = states[0];
|
||||
@ -292,7 +293,10 @@ const processTimelineEntity = (
|
||||
}
|
||||
|
||||
return {
|
||||
name: computeStateNameFromEntityAttributes(entityId, states[0].a),
|
||||
name: computeStateNameFromEntityAttributes(
|
||||
entityId,
|
||||
current_state?.attributes || first.a
|
||||
),
|
||||
entity_id: entityId,
|
||||
data,
|
||||
};
|
||||
@ -300,7 +304,8 @@ const processTimelineEntity = (
|
||||
|
||||
const processLineChartEntities = (
|
||||
unit,
|
||||
entities: HistoryStates
|
||||
entities: HistoryStates,
|
||||
hassEntities: HassEntities
|
||||
): LineChartUnit => {
|
||||
const data: LineChartEntity[] = [];
|
||||
|
||||
@ -349,9 +354,16 @@ const processLineChartEntities = (
|
||||
processedStates.push(processedState);
|
||||
}
|
||||
|
||||
const attributes =
|
||||
entityId in hassEntities
|
||||
? hassEntities[entityId].attributes
|
||||
: "friendly_name" in first.a
|
||||
? first.a
|
||||
: undefined;
|
||||
|
||||
data.push({
|
||||
domain,
|
||||
name: computeStateNameFromEntityAttributes(entityId, first.a),
|
||||
name: computeStateNameFromEntityAttributes(entityId, attributes || {}),
|
||||
entity_id: entityId,
|
||||
states: processedStates,
|
||||
});
|
||||
@ -411,7 +423,13 @@ export const computeHistory = (
|
||||
|
||||
if (!unit) {
|
||||
timelineDevices.push(
|
||||
processTimelineEntity(localize, hass.locale, entityId, stateInfo)
|
||||
processTimelineEntity(
|
||||
localize,
|
||||
hass.locale,
|
||||
entityId,
|
||||
stateInfo,
|
||||
currentState
|
||||
)
|
||||
);
|
||||
} else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) {
|
||||
lineChartDevices[unit][entityId].push(...stateInfo);
|
||||
@ -424,7 +442,7 @@ export const computeHistory = (
|
||||
});
|
||||
|
||||
const unitStates = Object.keys(lineChartDevices).map((unit) =>
|
||||
processLineChartEntities(unit, lineChartDevices[unit])
|
||||
processLineChartEntities(unit, lineChartDevices[unit], hass.states)
|
||||
);
|
||||
|
||||
return { line: unitStates, timeline: timelineDevices };
|
||||
|
95
src/data/repairs.ts
Normal file
95
src/data/repairs.ts
Normal 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
|
||||
);
|
@ -1,3 +1,8 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import type { DeviceRegistryEntry } from "./device_registry";
|
||||
import type { EntitySources } from "./entity_sources";
|
||||
|
||||
export type Selector =
|
||||
| ActionSelector
|
||||
| AddonSelector
|
||||
@ -35,18 +40,22 @@ export interface AddonSelector {
|
||||
};
|
||||
}
|
||||
|
||||
export interface SelectorDevice {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
}
|
||||
|
||||
export interface SelectorEntity {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
}
|
||||
|
||||
export interface AreaSelector {
|
||||
area: {
|
||||
entity?: {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
device?: {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
entity?: SelectorEntity;
|
||||
device?: SelectorDevice;
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
@ -89,10 +98,7 @@ export interface DeviceSelector {
|
||||
integration?: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
entity?: {
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
entity?: SelectorEntity;
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
@ -201,16 +207,8 @@ export interface StringSelector {
|
||||
|
||||
export interface TargetSelector {
|
||||
target: {
|
||||
entity?: {
|
||||
integration?: EntitySelector["entity"]["integration"];
|
||||
domain?: EntitySelector["entity"]["domain"];
|
||||
device_class?: EntitySelector["entity"]["device_class"];
|
||||
};
|
||||
device?: {
|
||||
integration?: DeviceSelector["device"]["integration"];
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
entity?: SelectorEntity;
|
||||
device?: SelectorDevice;
|
||||
};
|
||||
}
|
||||
|
||||
@ -227,3 +225,69 @@ export interface TimeSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
time: {};
|
||||
}
|
||||
|
||||
export const filterSelectorDevices = (
|
||||
filterDevice: SelectorDevice,
|
||||
device: DeviceRegistryEntry,
|
||||
deviceIntegrationLookup: Record<string, string[]> | undefined
|
||||
): boolean => {
|
||||
const {
|
||||
manufacturer: filterManufacturer,
|
||||
model: filterModel,
|
||||
integration: filterIntegration,
|
||||
} = filterDevice;
|
||||
|
||||
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterModel && device.model !== filterModel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterIntegration && deviceIntegrationLookup) {
|
||||
if (!deviceIntegrationLookup?.[device.id]?.includes(filterIntegration)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const filterSelectorEntities = (
|
||||
filterEntity: SelectorEntity,
|
||||
entity: HassEntity,
|
||||
entitySources?: EntitySources
|
||||
): boolean => {
|
||||
const {
|
||||
domain: filterDomain,
|
||||
device_class: filterDeviceClass,
|
||||
integration: filterIntegration,
|
||||
} = filterEntity;
|
||||
|
||||
if (filterDomain) {
|
||||
const entityDomain = computeStateDomain(entity);
|
||||
if (
|
||||
Array.isArray(filterDomain)
|
||||
? !filterDomain.includes(entityDomain)
|
||||
: entityDomain !== filterDomain
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
filterDeviceClass &&
|
||||
entity.attributes.device_class !== filterDeviceClass
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filterIntegration &&
|
||||
entitySources?.[entity.entity_id]?.domain !== filterIntegration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Connection, getCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { HomeAssistant, TranslationDict } from "../../types";
|
||||
import { HassioAddonsInfo } from "../hassio/addon";
|
||||
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
|
||||
import { NetworkInfo } from "../hassio/network";
|
||||
@ -67,7 +67,7 @@ export interface Supervisor {
|
||||
os: HassioHassOSInfo;
|
||||
addon: HassioAddonsInfo;
|
||||
store: SupervisorStore;
|
||||
localize: LocalizeFunc;
|
||||
localize: LocalizeFunc<TranslationDict["supervisor"]>;
|
||||
}
|
||||
|
||||
export const supervisorApiWsRequest = <T>(
|
||||
|
27
src/data/supported_brands.ts
Normal file
27
src/data/supported_brands.ts
Normal 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;
|
||||
};
|
@ -39,7 +39,8 @@ export type TranslationCategory =
|
||||
| "mfa_setup"
|
||||
| "system_health"
|
||||
| "device_class"
|
||||
| "application_credentials";
|
||||
| "application_credentials"
|
||||
| "issues";
|
||||
|
||||
export const fetchTranslationPreferences = (hass: HomeAssistant) =>
|
||||
fetchFrontendUserData(hass.connection, "language");
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
mdiHomeCircleOutline,
|
||||
mdiCancel,
|
||||
} from "@mdi/js";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { HomeAssistant, TranslationDict } from "../types";
|
||||
import { Credential } from "./auth";
|
||||
|
||||
export const SYSTEM_GROUP_ID_ADMIN = "system-admin";
|
||||
@ -21,7 +21,7 @@ export interface User {
|
||||
is_active: boolean;
|
||||
local_only: boolean;
|
||||
system_generated: boolean;
|
||||
group_ids: string[];
|
||||
group_ids: (keyof TranslationDict["groups"])[];
|
||||
credentials: Credential[];
|
||||
}
|
||||
|
||||
@ -95,7 +95,12 @@ export const computeUserBadges = (
|
||||
includeSystem: boolean
|
||||
) => {
|
||||
const labels: [string, string][] = [];
|
||||
const translate = (key) => hass.localize(`ui.panel.config.users.${key}`);
|
||||
const translate = (
|
||||
key: Extract<
|
||||
keyof TranslationDict["ui"]["panel"]["config"]["users"],
|
||||
`is_${string}`
|
||||
>
|
||||
) => hass.localize(`ui.panel.config.users.${key}`);
|
||||
|
||||
if (user.is_owner) {
|
||||
labels.push([OWNER_ICON, translate("is_owner")]);
|
||||
|
@ -26,6 +26,7 @@ export interface ZHADevice {
|
||||
power_source?: string;
|
||||
area_id?: string;
|
||||
device_type: string;
|
||||
active_coordinator: boolean;
|
||||
signature: any;
|
||||
neighbors: Neighbor[];
|
||||
pairing_status?: string;
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
handleConfigFlowStep,
|
||||
} from "../../data/config_flow";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { getSupportedBrands } from "../../data/supported_brands";
|
||||
import {
|
||||
DataEntryFlowDialogParams,
|
||||
loadDataEntryFlowDialog,
|
||||
@ -22,12 +23,14 @@ export const showConfigFlowDialog = (
|
||||
showFlowDialog(element, dialogParams, {
|
||||
loadDevicesAndAreas: true,
|
||||
getFlowHandlers: async (hass) => {
|
||||
const [integrations, helpers] = await Promise.all([
|
||||
const [integrations, helpers, supportedBrands] = await Promise.all([
|
||||
getConfigFlowHandlers(hass, "integration"),
|
||||
getConfigFlowHandlers(hass, "helper"),
|
||||
getSupportedBrands(hass),
|
||||
hass.loadBackendTranslation("title", undefined, true),
|
||||
]);
|
||||
return { integrations, helpers };
|
||||
|
||||
return { integrations, helpers, supportedBrands };
|
||||
},
|
||||
createFlow: async (hass, handler) => {
|
||||
const [step] = await Promise.all([
|
||||
|
@ -10,12 +10,14 @@ import {
|
||||
DataEntryFlowStepMenu,
|
||||
DataEntryFlowStepProgress,
|
||||
} from "../../data/data_entry_flow";
|
||||
import { IntegrationManifest } from "../../data/integration";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import type { IntegrationManifest } from "../../data/integration";
|
||||
import type { SupportedBrandHandler } from "../../data/supported_brands";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
export interface FlowHandlers {
|
||||
integrations: string[];
|
||||
helpers: string[];
|
||||
supportedBrands: Record<string, SupportedBrandHandler>;
|
||||
}
|
||||
export interface FlowConfig {
|
||||
loadDevicesAndAreas: boolean;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import "@polymer/paper-item";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
@ -14,19 +14,19 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { protocolIntegrationPicked } from "../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/search-input";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../../components/ha-icon-next";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import "../../components/search-input";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { FlowHandlers } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
|
||||
interface HandlerObj {
|
||||
name: string;
|
||||
@ -35,6 +35,10 @@ interface HandlerObj {
|
||||
is_helper?: boolean;
|
||||
}
|
||||
|
||||
export interface SupportedBrandObj extends HandlerObj {
|
||||
supported_flows: string[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
@ -63,11 +67,22 @@ class StepFlowPickHandler extends LitElement {
|
||||
h: FlowHandlers,
|
||||
filter?: string,
|
||||
_localize?: LocalizeFunc
|
||||
): [HandlerObj[], HandlerObj[]] => {
|
||||
const integrations: HandlerObj[] = h.integrations.map((handler) => ({
|
||||
name: domainToName(this.hass.localize, handler),
|
||||
slug: handler,
|
||||
}));
|
||||
): [(HandlerObj | SupportedBrandObj)[], HandlerObj[]] => {
|
||||
const integrations: (HandlerObj | SupportedBrandObj)[] =
|
||||
h.integrations.map((handler) => ({
|
||||
name: domainToName(this.hass.localize, handler),
|
||||
slug: handler,
|
||||
}));
|
||||
|
||||
for (const [domain, domainBrands] of Object.entries(h.supportedBrands)) {
|
||||
for (const [slug, name] of Object.entries(domainBrands)) {
|
||||
integrations.push({
|
||||
slug,
|
||||
name,
|
||||
supported_flows: [domain],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
const options: Fuse.IFuseOptions<HandlerObj> = {
|
||||
@ -238,27 +253,10 @@ class StepFlowPickHandler extends LitElement {
|
||||
}
|
||||
|
||||
private async _handlerPicked(ev) {
|
||||
const handler: HandlerObj = ev.currentTarget.handler;
|
||||
const handler: HandlerObj | SupportedBrandObj = ev.currentTarget.handler;
|
||||
|
||||
if (handler.is_add) {
|
||||
if (handler.slug === "zwave_js") {
|
||||
const entries = await getConfigEntries(this.hass, {
|
||||
domain: "zwave_js",
|
||||
});
|
||||
|
||||
if (!entries.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: entries[0].entry_id,
|
||||
});
|
||||
} else if (handler.slug === "zha") {
|
||||
navigate("/config/zha/add");
|
||||
}
|
||||
|
||||
// This closes dialog.
|
||||
fireEvent(this, "flow-update");
|
||||
this._handleAddPicked(handler.slug);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -269,11 +267,43 @@ class StepFlowPickHandler extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("supported_flows" in handler) {
|
||||
const slug = handler.supported_flows[0];
|
||||
|
||||
showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.supported_brand_flow",
|
||||
{
|
||||
supported_brand: handler.name,
|
||||
flow_domain_name: domainToName(this.hass.localize, slug),
|
||||
}
|
||||
),
|
||||
confirm: () => {
|
||||
if (["zha", "zwave_js"].includes(slug)) {
|
||||
this._handleAddPicked(slug);
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "handler-picked", {
|
||||
handler: slug,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "handler-picked", {
|
||||
handler: handler.slug,
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleAddPicked(slug: string): Promise<void> {
|
||||
await protocolIntegrationPicked(this, this.hass, slug);
|
||||
// This closes dialog.
|
||||
fireEvent(this, "flow-update");
|
||||
}
|
||||
|
||||
private _maybeSubmit(ev: KeyboardEvent) {
|
||||
if (ev.key !== "Enter") {
|
||||
return;
|
||||
|
@ -144,8 +144,6 @@ class DialogBox extends LitElement {
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 24px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.no-bottom-padding {
|
||||
@ -157,7 +155,6 @@ class DialogBox extends LitElement {
|
||||
ha-dialog {
|
||||
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
||||
--mdc-dialog-content-ink-color: var(--primary-text-color);
|
||||
--justify-action-buttons: space-between;
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
|
@ -206,6 +206,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
--mdc-theme-primary: currentColor;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.basic-controls {
|
||||
@ -213,6 +214,15 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.volume {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.source-input,
|
||||
.sound-input {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.volume,
|
||||
.source-input,
|
||||
.sound-input {
|
||||
@ -225,6 +235,9 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
.sound-input ha-select {
|
||||
margin-left: 10px;
|
||||
flex-grow: 1;
|
||||
margin-inline-start: 10px;
|
||||
margin-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.tts {
|
||||
|
@ -329,6 +329,9 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
ha-icon-button {
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: -24px;
|
||||
margin-inline-end: -24px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
ha-icon-button[active] {
|
||||
@ -373,19 +376,25 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
float: right;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
float: var(--float-end);
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
background-color: var(--light-primary-color);
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.hass {
|
||||
margin-right: 24px;
|
||||
float: left;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
float: var(--float-start);
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message a {
|
||||
|
@ -178,9 +178,6 @@ class DialogAreaDetail extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
|
@ -3,7 +3,7 @@ import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@ -23,6 +23,11 @@ import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tip";
|
||||
import { CloudStatus } from "../../../data/cloud";
|
||||
import {
|
||||
RepairsIssue,
|
||||
severitySort,
|
||||
subscribeRepairsIssueRegistry,
|
||||
} from "../../../data/repairs";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesWithInstall,
|
||||
@ -31,11 +36,13 @@ import {
|
||||
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import "../../../layouts/ha-app-layout";
|
||||
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "../repairs/ha-config-repairs";
|
||||
import "./ha-config-navigation";
|
||||
import "./ha-config-updates";
|
||||
|
||||
@ -104,7 +111,7 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
|
||||
};
|
||||
|
||||
@customElement("ha-config-dashboard")
|
||||
class HaConfigDashboard extends LitElement {
|
||||
class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
@ -118,6 +125,11 @@ class HaConfigDashboard extends LitElement {
|
||||
|
||||
@state() private _tip?: string;
|
||||
|
||||
@state() private _repairsIssues: { issues: RepairsIssue[]; total: number } = {
|
||||
issues: [],
|
||||
total: 0,
|
||||
};
|
||||
|
||||
private _pages = memoizeOne((clouStatus, isLoaded) => {
|
||||
const pages: PageNavigation[] = [];
|
||||
if (clouStatus && isLoaded) {
|
||||
@ -133,10 +145,34 @@ class HaConfigDashboard extends LitElement {
|
||||
return [...pages, ...configSections.dashboard];
|
||||
});
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
|
||||
const repairsIssues = repairs.issues.filter((issue) => !issue.ignored);
|
||||
|
||||
this._repairsIssues = {
|
||||
issues: repairsIssues
|
||||
.sort((a, b) => severitySort[a.severity] - severitySort[b.severity])
|
||||
.slice(0, repairsIssues.length === 3 ? repairsIssues.length : 2),
|
||||
total: repairsIssues.length,
|
||||
};
|
||||
|
||||
const integrations: Set<string> = new Set();
|
||||
for (const issue of this._repairsIssues.issues) {
|
||||
integrations.add(issue.domain);
|
||||
}
|
||||
this.hass.loadBackendTranslation("issues", [...integrations]);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const [canInstallUpdates, totalUpdates] =
|
||||
const { updates: canInstallUpdates, total: totalUpdates } =
|
||||
this._filterUpdateEntitiesWithInstall(this.hass.states);
|
||||
|
||||
const { issues: repairsIssues, total: totalRepairIssues } =
|
||||
this._repairsIssues;
|
||||
|
||||
return html`
|
||||
<ha-app-layout>
|
||||
<app-header fixed slot="header">
|
||||
@ -174,26 +210,60 @@ class HaConfigDashboard extends LitElement {
|
||||
.isWide=${this.isWide}
|
||||
full-width
|
||||
>
|
||||
${canInstallUpdates.length
|
||||
${repairsIssues.length || canInstallUpdates.length
|
||||
? html`<ha-card outlined>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.total=${totalUpdates}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
></ha-config-updates>
|
||||
${totalUpdates > canInstallUpdates.length
|
||||
? html`<a class="button" href="/config/updates">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.more_updates",
|
||||
{
|
||||
count: totalUpdates - canInstallUpdates.length,
|
||||
}
|
||||
)}
|
||||
</a>`
|
||||
${repairsIssues.length
|
||||
? html`
|
||||
<ha-config-repairs
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.total=${totalRepairIssues}
|
||||
.repairsIssues=${repairsIssues}
|
||||
></ha-config-repairs>
|
||||
${totalRepairIssues > repairsIssues.length
|
||||
? html`
|
||||
<a class="button" href="/config/repairs">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.repairs.more_repairs",
|
||||
{
|
||||
count:
|
||||
totalRepairIssues - repairsIssues.length,
|
||||
}
|
||||
)}
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
${repairsIssues.length && canInstallUpdates.length
|
||||
? html`<hr />`
|
||||
: ""}
|
||||
${canInstallUpdates.length
|
||||
? html`
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.total=${totalUpdates}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
></ha-config-updates>
|
||||
${totalUpdates > canInstallUpdates.length
|
||||
? html`
|
||||
<a class="button" href="/config/updates">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.more_updates",
|
||||
{
|
||||
count:
|
||||
totalUpdates - canInstallUpdates.length,
|
||||
}
|
||||
)}
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
</ha-card>`
|
||||
: ""}
|
||||
|
||||
<ha-card outlined>
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
@ -220,13 +290,13 @@ class HaConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities): [UpdateEntity[], number] => {
|
||||
(entities: HassEntities): { updates: UpdateEntity[]; total: number } => {
|
||||
const updates = filterUpdateEntitiesWithInstall(entities);
|
||||
|
||||
return [
|
||||
updates.slice(0, updates.length === 3 ? updates.length : 2),
|
||||
updates.length,
|
||||
];
|
||||
return {
|
||||
updates: updates.slice(0, updates.length === 3 ? updates.length : 2),
|
||||
total: updates.length,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@ -268,9 +338,12 @@ class HaConfigDashboard extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
a.button {
|
||||
display: block;
|
||||
color: var(--primary-color);
|
||||
padding: 16px;
|
||||
display: inline-block;
|
||||
color: var(--primary-text-color);
|
||||
padding: 6px 16px;
|
||||
margin: 8px 16px 16px 16px;
|
||||
border-radius: 32px;
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
@ -300,6 +373,16 @@ class HaConfigDashboard extends LitElement {
|
||||
.keep-together {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
background-color: var(
|
||||
--ha-card-border-color,
|
||||
var(--divider-color, #e0e0e0)
|
||||
);
|
||||
border: none;
|
||||
margin-top: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -163,17 +163,27 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
const stateObj = this.hass.states[entry.entity_id];
|
||||
const name = stripPrefixFromEntityName(
|
||||
computeStateName(stateObj),
|
||||
this.deviceName.toLowerCase()
|
||||
);
|
||||
if (entry.hidden_by) {
|
||||
config.name = `${
|
||||
name || computeStateName(stateObj)
|
||||
} (${this.hass.localize("ui.panel.config.devices.entities.hidden")})`;
|
||||
} else if (name) {
|
||||
config.name = name;
|
||||
|
||||
let name = entry.name
|
||||
? entry.name
|
||||
: entry.has_entity_name
|
||||
? entry.original_name || this.deviceName
|
||||
: stripPrefixFromEntityName(
|
||||
computeStateName(stateObj),
|
||||
this.deviceName.toLowerCase()
|
||||
);
|
||||
|
||||
if (!name) {
|
||||
name = computeStateName(stateObj);
|
||||
}
|
||||
|
||||
if (entry.hidden_by) {
|
||||
name += ` (${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.hidden"
|
||||
)})`;
|
||||
}
|
||||
|
||||
config.name = name;
|
||||
}
|
||||
// @ts-ignore
|
||||
element.entry = entry;
|
||||
|
@ -30,7 +30,7 @@ export const getZHADeviceActions = async (
|
||||
|
||||
const actions: DeviceAction[] = [];
|
||||
|
||||
if (zhaDevice.device_type !== "Coordinator") {
|
||||
if (!zhaDevice.active_coordinator) {
|
||||
actions.push({
|
||||
label: hass.localize("ui.dialogs.zha_device_info.buttons.reconfigure"),
|
||||
action: () => showZHAReconfigureDeviceDialog(el, { device: zhaDevice }),
|
||||
@ -58,50 +58,50 @@ export const getZHADeviceActions = async (
|
||||
);
|
||||
}
|
||||
|
||||
if (zhaDevice.device_type !== "Coordinator") {
|
||||
actions.push(
|
||||
...[
|
||||
{
|
||||
label: hass.localize(
|
||||
"ui.dialogs.zha_device_info.buttons.zigbee_information"
|
||||
actions.push(
|
||||
...[
|
||||
{
|
||||
label: hass.localize(
|
||||
"ui.dialogs.zha_device_info.buttons.zigbee_information"
|
||||
),
|
||||
action: () => showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }),
|
||||
},
|
||||
{
|
||||
label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"),
|
||||
action: () => showZHAClusterDialog(el, { device: zhaDevice }),
|
||||
},
|
||||
{
|
||||
label: hass.localize(
|
||||
"ui.dialogs.zha_device_info.buttons.view_in_visualization"
|
||||
),
|
||||
action: () =>
|
||||
navigate(`/config/zha/visualization/${zhaDevice!.device_reg_id}`),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
if (!zhaDevice.active_coordinator) {
|
||||
actions.push({
|
||||
label: hass.localize("ui.dialogs.zha_device_info.buttons.remove"),
|
||||
classes: "warning",
|
||||
action: async () => {
|
||||
const confirmed = await showConfirmationDialog(el, {
|
||||
text: hass.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove"
|
||||
),
|
||||
action: () =>
|
||||
showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }),
|
||||
},
|
||||
{
|
||||
label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"),
|
||||
action: () => showZHAClusterDialog(el, { device: zhaDevice }),
|
||||
},
|
||||
{
|
||||
label: hass.localize(
|
||||
"ui.dialogs.zha_device_info.buttons.view_in_visualization"
|
||||
),
|
||||
action: () =>
|
||||
navigate(`/config/zha/visualization/${zhaDevice!.device_reg_id}`),
|
||||
},
|
||||
{
|
||||
label: hass.localize("ui.dialogs.zha_device_info.buttons.remove"),
|
||||
classes: "warning",
|
||||
action: async () => {
|
||||
const confirmed = await showConfirmationDialog(el, {
|
||||
text: hass.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove"
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callService("zha", "remove", {
|
||||
ieee: zhaDevice.ieee,
|
||||
});
|
||||
await hass.callService("zha", "remove", {
|
||||
ieee: zhaDevice.ieee,
|
||||
});
|
||||
|
||||
history.back();
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
history.back();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
|
@ -100,19 +100,14 @@ export const getZwaveDeviceActions = async (
|
||||
action: async () => {
|
||||
if (
|
||||
isNodeFirmwareUpdateInProgress ||
|
||||
(await fetchZwaveNodeIsFirmwareUpdateInProgress(hass, device.id))
|
||||
) {
|
||||
showZWaveJUpdateFirmwareNodeDialog(el, {
|
||||
device,
|
||||
});
|
||||
} else if (
|
||||
await showConfirmationDialog(el, {
|
||||
(await fetchZwaveNodeIsFirmwareUpdateInProgress(hass, device.id)) ||
|
||||
(await showConfirmationDialog(el, {
|
||||
text: hass.localize(
|
||||
"ui.panel.config.zwave_js.update_firmware.warning"
|
||||
),
|
||||
dismissText: hass.localize("ui.common.no"),
|
||||
confirmText: hass.localize("ui.common.yes"),
|
||||
})
|
||||
}))
|
||||
) {
|
||||
showZWaveJUpdateFirmwareNodeDialog(el, {
|
||||
device,
|
||||
|
@ -6,10 +6,14 @@ import "../../../../components/ha-area-picker";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-textfield";
|
||||
import { computeDeviceName } from "../../../../data/device_registry";
|
||||
import {
|
||||
computeDeviceName,
|
||||
DeviceRegistryEntry,
|
||||
} from "../../../../data/device_registry";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail";
|
||||
import "../../../../components/ha-alert";
|
||||
|
||||
@customElement("dialog-device-registry-detail")
|
||||
class DialogDeviceRegistryDetail extends LitElement {
|
||||
@ -21,11 +25,11 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
|
||||
@state() private _params?: DeviceRegistryDetailDialogParams;
|
||||
|
||||
@property() public _areaId?: string | null;
|
||||
@state() private _areaId!: string;
|
||||
|
||||
@state() private _disabledBy!: string | null;
|
||||
@state() private _disabledBy!: DeviceRegistryEntry["disabled_by"];
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
@state() private _submitting = false;
|
||||
|
||||
public async showDialog(
|
||||
params: DeviceRegistryDetailDialogParams
|
||||
@ -33,7 +37,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._nameByUser = this._params.device.name_by_user || "";
|
||||
this._areaId = this._params.device.area_id;
|
||||
this._areaId = this._params.device.area_id || "";
|
||||
this._disabledBy = this._params.device.disabled_by;
|
||||
await this.updateComplete;
|
||||
}
|
||||
@ -169,9 +173,6 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
mwc-button.warning {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
@ -344,7 +344,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
.disabled=${device.disabled_by}
|
||||
.label=${device.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.automation.create_disabled",
|
||||
"ui.panel.config.devices.automation.create_disable",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
@ -437,7 +437,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
.disabled=${device.disabled_by}
|
||||
.label=${device.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.scene.create_disabled",
|
||||
"ui.panel.config.devices.scene.create_disable",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
@ -530,7 +530,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
.disabled=${device.disabled_by}
|
||||
.label=${device.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.script.create_disabled",
|
||||
"ui.panel.config.devices.script.create_disable",
|
||||
"type",
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
@ -768,26 +768,27 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: ""
|
||||
}
|
||||
</ha-device-info-card>
|
||||
${!this.narrow ? [automationCard, sceneCard, scriptCard] : ""}
|
||||
${!this.narrow ? [automationCard, sceneCard, scriptCard] : ""}
|
||||
</div>
|
||||
<div class="column">
|
||||
${["control", "sensor", "config", "diagnostic"].map((category) =>
|
||||
// Make sure we render controls if no other cards will be rendered
|
||||
entitiesByCategory[category].length > 0 ||
|
||||
(entities.length === 0 && category === "control")
|
||||
? html`
|
||||
<ha-device-entities-card
|
||||
.hass=${this.hass}
|
||||
.header=${this.hass.localize(
|
||||
`ui.panel.config.devices.entities.${category}`
|
||||
)}
|
||||
.deviceName=${deviceName}
|
||||
.entities=${entitiesByCategory[category]}
|
||||
.showHidden=${device.disabled_by !== null}
|
||||
>
|
||||
</ha-device-entities-card>
|
||||
`
|
||||
: ""
|
||||
${(["control", "sensor", "config", "diagnostic"] as const).map(
|
||||
(category) =>
|
||||
// Make sure we render controls if no other cards will be rendered
|
||||
entitiesByCategory[category].length > 0 ||
|
||||
(entities.length === 0 && category === "control")
|
||||
? html`
|
||||
<ha-device-entities-card
|
||||
.hass=${this.hass}
|
||||
.header=${this.hass.localize(
|
||||
`ui.panel.config.devices.entities.${category}`
|
||||
)}
|
||||
.deviceName=${deviceName}
|
||||
.entities=${entitiesByCategory[category]}
|
||||
.showHidden=${device.disabled_by !== null}
|
||||
>
|
||||
</ha-device-entities-card>
|
||||
`
|
||||
: ""
|
||||
)}
|
||||
</div>
|
||||
<div class="column">
|
||||
|
@ -8,6 +8,9 @@ export const energyCardStyles = css`
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
h3 {
|
||||
margin-top: 24px;
|
||||
@ -24,6 +27,9 @@ export const energyCardStyles = css`
|
||||
.row ha-icon,
|
||||
.row img {
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.row img {
|
||||
height: 24px;
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
EntityRegistryEntryUpdateParams,
|
||||
ExtEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
@ -25,7 +26,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entry!: ExtEntityRegistryEntry;
|
||||
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
|
||||
|
||||
@state() private _origEntityId!: string;
|
||||
|
||||
@ -33,7 +34,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _areaId?: string | null;
|
||||
|
||||
@state() private _disabledBy!: string | null;
|
||||
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
|
||||
|
||||
@state() private _hiddenBy!: string | null;
|
||||
|
||||
@ -41,7 +42,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _device?: DeviceRegistryEntry;
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
@state() private _submitting = false;
|
||||
|
||||
public async updateEntry(): Promise<void> {
|
||||
this._submitting = true;
|
||||
@ -145,8 +146,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
></ha-textfield>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._areaId}
|
||||
.placeholder=${this._device?.area_id}
|
||||
.value=${this._areaId || undefined}
|
||||
.placeholder=${this._device?.area_id || undefined}
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>
|
||||
|
||||
@ -182,8 +183,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
name="hiddendisabled"
|
||||
value="enabled"
|
||||
.checked=${!this._hiddenBy && !this._disabledBy}
|
||||
.disabled=${this._device?.disabled_by ||
|
||||
(this._disabledBy &&
|
||||
.disabled=${!!this._device?.disabled_by ||
|
||||
(this._disabledBy !== null &&
|
||||
!(
|
||||
this._disabledBy === "user" ||
|
||||
this._disabledBy === "integration"
|
||||
@ -200,8 +201,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
name="hiddendisabled"
|
||||
value="hidden"
|
||||
.checked=${this._hiddenBy !== null}
|
||||
.disabled=${this._device?.disabled_by ||
|
||||
(this._disabledBy &&
|
||||
.disabled=${!!this._device?.disabled_by ||
|
||||
(this._disabledBy !== null &&
|
||||
!(
|
||||
this._disabledBy === "user" ||
|
||||
this._disabledBy === "integration"
|
||||
@ -218,8 +219,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
name="hiddendisabled"
|
||||
value="disabled"
|
||||
.checked=${this._disabledBy !== null}
|
||||
.disabled=${this._device?.disabled_by ||
|
||||
(this._disabledBy &&
|
||||
.disabled=${!!this._device?.disabled_by ||
|
||||
(this._disabledBy !== null &&
|
||||
!(
|
||||
this._disabledBy === "user" ||
|
||||
this._disabledBy === "integration"
|
||||
@ -302,3 +303,9 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-registry-basic-editor": HaEntityRegistryBasicEditor;
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ import {
|
||||
updateDeviceRegistryEntry,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
EntityRegistryEntryUpdateParams,
|
||||
ExtEntityRegistryEntry,
|
||||
fetchEntityRegistry,
|
||||
@ -105,6 +106,10 @@ const OVERRIDE_DEVICE_CLASSES = {
|
||||
],
|
||||
};
|
||||
|
||||
const OVERRIDE_NUMBER_UNITS = {
|
||||
temperature: ["°C", "°F", "K"],
|
||||
};
|
||||
|
||||
const OVERRIDE_SENSOR_UNITS = {
|
||||
temperature: ["°C", "°F", "K"],
|
||||
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
|
||||
@ -124,7 +129,7 @@ const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"];
|
||||
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entry!: ExtEntityRegistryEntry;
|
||||
@property({ type: Object }) public entry!: ExtEntityRegistryEntry;
|
||||
|
||||
@state() private _name!: string;
|
||||
|
||||
@ -138,9 +143,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _areaId?: string | null;
|
||||
|
||||
@state() private _disabledBy!: string | null;
|
||||
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
|
||||
|
||||
@state() private _hiddenBy!: string | null;
|
||||
@state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];
|
||||
|
||||
@state() private _device?: DeviceRegistryEntry;
|
||||
|
||||
@ -235,7 +240,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "sensor") {
|
||||
if (domain === "number" || domain === "sensor") {
|
||||
const stateObj: HassEntity | undefined =
|
||||
this.hass.states[this.entry.entity_id];
|
||||
this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement;
|
||||
@ -361,6 +366,31 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
</ha-select>
|
||||
`
|
||||
: ""}
|
||||
${domain === "number" &&
|
||||
this._deviceClass &&
|
||||
stateObj?.attributes.unit_of_measurement &&
|
||||
OVERRIDE_NUMBER_UNITS[this._deviceClass]?.includes(
|
||||
stateObj?.attributes.unit_of_measurement
|
||||
)
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.unit_of_measurement"
|
||||
)}
|
||||
.value=${stateObj.attributes.unit_of_measurement}
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
@selected=${this._unitChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${OVERRIDE_NUMBER_UNITS[this._deviceClass].map(
|
||||
(unit: string) => html`
|
||||
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: ""}
|
||||
${domain === "sensor" &&
|
||||
this._deviceClass &&
|
||||
stateObj?.attributes.unit_of_measurement &&
|
||||
@ -601,9 +631,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
name="hiddendisabled"
|
||||
value="enabled"
|
||||
.checked=${!this._hiddenBy && !this._disabledBy}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
this._device?.disabled_by ||
|
||||
(this._disabledBy &&
|
||||
.disabled=${(this._hiddenBy !== null &&
|
||||
this._hiddenBy !== "user") ||
|
||||
!!this._device?.disabled_by ||
|
||||
(this._disabledBy !== null &&
|
||||
this._disabledBy !== "user" &&
|
||||
this._disabledBy !== "integration")}
|
||||
@change=${this._viewStatusChanged}
|
||||
@ -861,10 +892,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
params.hidden_by = this._hiddenBy;
|
||||
}
|
||||
if (
|
||||
domain === "sensor" &&
|
||||
(domain === "number" || domain === "number") &&
|
||||
stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement
|
||||
) {
|
||||
params.options_domain = "sensor";
|
||||
params.options_domain = domain;
|
||||
params.options = { unit_of_measurement: this._unit_of_measurement };
|
||||
}
|
||||
if (
|
||||
@ -1023,12 +1054,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||
padding: 0 24px 24px 24px;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 24px);
|
||||
background-color: var(--mdc-theme-surface, #fff);
|
||||
}
|
||||
ha-select {
|
||||
|
@ -15,6 +15,7 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoize from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
@ -85,11 +86,11 @@ export interface EntityRow extends StateEntity {
|
||||
export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property() public route!: Route;
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@ -174,7 +175,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
type: "icon",
|
||||
template: (_, entry: EntityRow) => html`
|
||||
<ha-state-icon
|
||||
.title=${entry.entity?.state}
|
||||
title=${ifDefined(entry.entity?.state)}
|
||||
slot="item-icon"
|
||||
.state=${entry.entity}
|
||||
></ha-state-icon>
|
||||
@ -237,12 +238,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
hidden: narrow || !showDisabled,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
template: (disabled_by) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.devices.disabled_by.${disabled_by}`
|
||||
) ||
|
||||
disabled_by ||
|
||||
"—",
|
||||
template: (disabled_by: EntityRegistryEntry["disabled_by"]) =>
|
||||
disabled_by === null
|
||||
? "—"
|
||||
: this.hass.localize(`config_entry.disabled_by.${disabled_by}`),
|
||||
},
|
||||
status: {
|
||||
title: this.hass.localize(
|
||||
@ -736,6 +735,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
readonly: true,
|
||||
selectable: false,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
});
|
||||
}
|
||||
if (changed) {
|
||||
@ -1011,3 +1011,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-entities": HaConfigEntities;
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,9 @@ import {
|
||||
mdiCog,
|
||||
mdiDatabase,
|
||||
mdiDevices,
|
||||
mdiHeart,
|
||||
mdiInformation,
|
||||
mdiInformationOutline,
|
||||
mdiLifebuoy,
|
||||
mdiLightningBolt,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMathLog,
|
||||
@ -267,6 +267,12 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
iconPath: mdiUpdate,
|
||||
iconColor: "#3B808E",
|
||||
},
|
||||
{
|
||||
path: "/config/repairs",
|
||||
translationKey: "repairs",
|
||||
iconPath: mdiLifebuoy,
|
||||
iconColor: "#5c995c",
|
||||
},
|
||||
{
|
||||
component: "logs",
|
||||
path: "/config/logs",
|
||||
@ -315,13 +321,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
iconColor: "#301A8E",
|
||||
component: "hassio",
|
||||
},
|
||||
{
|
||||
path: "/config/system_health",
|
||||
translationKey: "system_health",
|
||||
iconPath: mdiHeart,
|
||||
iconColor: "#507FfE",
|
||||
components: ["system_health", "hassio"],
|
||||
},
|
||||
],
|
||||
about: [
|
||||
{
|
||||
@ -440,14 +439,14 @@ class HaPanelConfig extends HassRouterPage {
|
||||
tag: "ha-config-section-storage",
|
||||
load: () => import("./storage/ha-config-section-storage"),
|
||||
},
|
||||
system_health: {
|
||||
tag: "ha-config-system-health",
|
||||
load: () => import("./system-health/ha-config-system-health"),
|
||||
},
|
||||
updates: {
|
||||
tag: "ha-config-section-updates",
|
||||
load: () => import("./core/ha-config-section-updates"),
|
||||
},
|
||||
repairs: {
|
||||
tag: "ha-config-repairs-dashboard",
|
||||
load: () => import("./repairs/ha-config-repairs-dashboard"),
|
||||
},
|
||||
users: {
|
||||
tag: "ha-config-users",
|
||||
load: () => import("./users/ha-config-users"),
|
||||
|
@ -89,7 +89,7 @@ class DialogHardwareAvailable extends LitElement implements HassDialog {
|
||||
)}
|
||||
</h2>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("common.close")}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
></ha-icon-button>
|
||||
@ -97,7 +97,9 @@ class DialogHardwareAvailable extends LitElement implements HassDialog {
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.hass.localize("common.search")}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.search"
|
||||
)}
|
||||
>
|
||||
</search-input>
|
||||
</div>
|
||||
|
@ -202,7 +202,7 @@ class HaConfigHardware extends LitElement {
|
||||
title: this.hass.localize("ui.panel.config.hardware.reboot_host"),
|
||||
text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"),
|
||||
confirmText: this.hass.localize("ui.panel.config.hardware.reboot_host"),
|
||||
dismissText: this.hass.localize("common.cancel"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
@ -236,7 +236,7 @@ class HaConfigHardware extends LitElement {
|
||||
"ui.panel.config.hardware.shutdown_host_confirm"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
|
||||
dismissText: this.hass.localize("common.cancel"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
@ -14,7 +14,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
@ -49,6 +50,10 @@ import {
|
||||
fetchIntegrationManifests,
|
||||
IntegrationManifest,
|
||||
} from "../../../data/integration";
|
||||
import {
|
||||
getSupportedBrands,
|
||||
getSupportedBrandsLookup,
|
||||
} from "../../../data/supported_brands";
|
||||
import { scanUSBDevices } from "../../../data/usb";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import {
|
||||
@ -677,49 +682,84 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlers = await getConfigFlowHandlers(this.hass, "integration");
|
||||
|
||||
if (!handlers.includes(domain)) {
|
||||
if (HELPER_DOMAINS.includes(domain)) {
|
||||
navigate(`/config/helpers/add?domain=${domain}`, {
|
||||
replace: true,
|
||||
});
|
||||
// Integration exists, so we can just create a flow
|
||||
if (handlers.includes(domain)) {
|
||||
const localize = await localizePromise;
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: localize("ui.panel.config.integrations.confirm_new", {
|
||||
integration: domainToName(localize, domain),
|
||||
}),
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const helpers = await getConfigFlowHandlers(this.hass, "helper");
|
||||
if (helpers.includes(domain)) {
|
||||
navigate(`/config/helpers/add?domain=${domain}`, {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
showConfigFlowDialog(this, {
|
||||
dialogClosedCallback: () => {
|
||||
this._handleFlowUpdated();
|
||||
},
|
||||
startFlowHandler: domain,
|
||||
manifest: this._manifests[domain],
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
});
|
||||
}
|
||||
|
||||
const supportedBrands = await getSupportedBrands(this.hass);
|
||||
const supportedBrandsIntegrations =
|
||||
getSupportedBrandsLookup(supportedBrands);
|
||||
|
||||
// Supported brand exists, so we can just create a flow
|
||||
if (Object.keys(supportedBrandsIntegrations).includes(domain)) {
|
||||
const brand = supportedBrandsIntegrations[domain];
|
||||
const slug = brand.supported_flows![0];
|
||||
|
||||
showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.no_config_flow"
|
||||
"ui.panel.config.integrations.config_flow.supported_brand_flow",
|
||||
{
|
||||
supported_brand: brand.name,
|
||||
flow_domain_name: domainToName(this.hass.localize, slug),
|
||||
}
|
||||
),
|
||||
confirm: () => {
|
||||
if (["zha", "zwave_js"].includes(slug)) {
|
||||
protocolIntegrationPicked(this, this.hass, slug);
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "handler-picked", {
|
||||
handler: slug,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If not an integration or supported brand, try helper else show alert
|
||||
if (HELPER_DOMAINS.includes(domain)) {
|
||||
navigate(`/config/helpers/add?domain=${domain}`, {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const localize = await localizePromise;
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: localize("ui.panel.config.integrations.confirm_new", {
|
||||
integration: domainToName(localize, domain),
|
||||
}),
|
||||
}))
|
||||
) {
|
||||
const helpers = await getConfigFlowHandlers(this.hass, "helper");
|
||||
if (helpers.includes(domain)) {
|
||||
navigate(`/config/helpers/add?domain=${domain}`, {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showConfigFlowDialog(this, {
|
||||
dialogClosedCallback: () => {
|
||||
this._handleFlowUpdated();
|
||||
},
|
||||
startFlowHandler: domain,
|
||||
manifest: this._manifests[domain],
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.no_config_flow"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
mdiDotsVertical,
|
||||
mdiOpenInNew,
|
||||
} from "@mdi/js";
|
||||
import "@polymer/paper-item";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
@ -31,6 +31,7 @@ import {
|
||||
reloadConfigEntry,
|
||||
updateConfigEntry,
|
||||
ERROR_STATES,
|
||||
RECOVERABLE_STATES,
|
||||
} from "../../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics";
|
||||
@ -63,13 +64,15 @@ export class HaIntegrationCard extends LitElement {
|
||||
|
||||
@property() public domain!: string;
|
||||
|
||||
@property() public items!: ConfigEntryExtended[];
|
||||
@property({ attribute: false }) public items!: ConfigEntryExtended[];
|
||||
|
||||
@property() public manifest?: IntegrationManifest;
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
@property() public entityRegistryEntries!: EntityRegistryEntry[];
|
||||
@property({ attribute: false })
|
||||
public entityRegistryEntries!: EntityRegistryEntry[];
|
||||
|
||||
@property() public deviceRegistryEntries!: DeviceRegistryEntry[];
|
||||
@property({ attribute: false })
|
||||
public deviceRegistryEntries!: DeviceRegistryEntry[];
|
||||
|
||||
@property() public selectedConfigEntryId?: string;
|
||||
|
||||
@ -178,7 +181,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
const services = this._getServices(item, this.deviceRegistryEntries);
|
||||
const entities = this._getEntities(item, this.entityRegistryEntries);
|
||||
|
||||
let stateText: [string, ...unknown[]] | undefined;
|
||||
let stateText: Parameters<typeof this.hass.localize> | undefined;
|
||||
let stateTextExtra: TemplateResult | string | undefined;
|
||||
|
||||
if (item.disabled_by) {
|
||||
@ -224,7 +227,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
for (const [items, localizeKey] of [
|
||||
[devices, "devices"],
|
||||
[services, "services"],
|
||||
] as [DeviceRegistryEntry[], string][]) {
|
||||
] as const) {
|
||||
if (items.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@ -366,7 +369,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
</a>`
|
||||
: ""}
|
||||
${!item.disabled_by &&
|
||||
(item.state === "loaded" || item.state === "setup_retry") &&
|
||||
RECOVERABLE_STATES.includes(item.state) &&
|
||||
item.supports_unload &&
|
||||
item.source !== "system"
|
||||
? html`<mwc-list-item @request-selected=${this._handleReload}>
|
||||
|
@ -12,13 +12,13 @@ import { brandsUrl } from "../../../util/brands-url";
|
||||
export class HaIntegrationHeader extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public banner!: string;
|
||||
@property() public banner?: string;
|
||||
|
||||
@property() public localizedDomainName?: string;
|
||||
|
||||
@property() public domain!: string;
|
||||
|
||||
@property() public label!: string;
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
|
@ -401,7 +401,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "finished"
|
||||
@ -451,7 +451,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "provisioned"
|
||||
@ -469,7 +469,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
|
@ -120,7 +120,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
@ -140,7 +140,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
@ -160,7 +160,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
@ -180,7 +180,7 @@ class DialogZWaveJSHealNetwork extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
|
@ -166,7 +166,7 @@ class DialogZWaveJSHealNode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
@ -186,7 +186,7 @@ class DialogZWaveJSHealNode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
|
@ -85,7 +85,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
@ -105,7 +105,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
@ -125,7 +125,7 @@ class DialogZWaveJSReinterviewNode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
|
@ -110,7 +110,7 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
@ -132,7 +132,7 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
|
@ -102,7 +102,7 @@ export class SystemLogCard extends LitElement {
|
||||
<paper-item @click=${this._openLog} .logItem=${item}>
|
||||
<paper-item-body two-line>
|
||||
<div class="row">${item.message[0]}</div>
|
||||
<div secondary>
|
||||
<div class="row-secondary" secondary>
|
||||
${this._timestamp(item)} –
|
||||
${html`(<span class=${item.level.toLowerCase()}
|
||||
>${this.hass.localize(
|
||||
@ -209,6 +209,11 @@ export class SystemLogCard extends LitElement {
|
||||
.empty-content {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.row-secondary {
|
||||
direction: var(--direction);
|
||||
text-align: left;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
91
src/panels/config/network/dialog-ip-detail.ts
Normal file
91
src/panels/config/network/dialog-ip-detail.ts
Normal 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;
|
||||
}
|
||||
}
|
19
src/panels/config/network/show-ip-detail-dialog.ts
Normal file
19
src/panels/config/network/show-ip-detail-dialog.ts
Normal 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,
|
||||
});
|
||||
};
|
@ -1,13 +1,16 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-formfield";
|
||||
@ -29,7 +32,7 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-card";
|
||||
import { showIPDetailDialog } from "./show-ip-detail-dialog";
|
||||
|
||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||
|
||||
@ -236,9 +239,25 @@ export class HassioNetwork extends LitElement {
|
||||
</ha-circular-progress>`
|
||||
: this.hass.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${"ui.common.menu"}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item>IP Information</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
showIPDetailDialog(this, { interface: this._interface });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _selectAP(event) {
|
||||
this._wifiConfiguration = event.currentTarget.ap;
|
||||
this._dirty = true;
|
||||
|
@ -454,9 +454,6 @@ class DialogPersonDetail extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
ha-picture-upload,
|
||||
ha-textfield {
|
||||
display: block;
|
||||
|
57
src/panels/config/repairs/dialog-integration-startup.ts
Normal file
57
src/panels/config/repairs/dialog-integration-startup.ts
Normal 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;
|
||||
}
|
||||
}
|
148
src/panels/config/repairs/dialog-repairs-issue.ts
Normal file
148
src/panels/config/repairs/dialog-repairs-issue.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -1,17 +1,15 @@
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket/dist/types";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import { subscribePollingCollection } from "../../../common/util/subscribe-polling";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-metric";
|
||||
import { fetchHassioStats, HassioStats } from "../../../data/hassio/common";
|
||||
import {
|
||||
@ -25,12 +23,11 @@ import {
|
||||
SystemHealthInfo,
|
||||
} from "../../../data/system_health";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "./integrations-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
|
||||
const sortKeys = (a: string, b: string) => {
|
||||
if (a === "homeassistant") {
|
||||
@ -53,28 +50,40 @@ export const UNHEALTHY_REASON_URL = {
|
||||
privileged: "/more-info/unsupported/privileged",
|
||||
};
|
||||
|
||||
@customElement("ha-config-system-health")
|
||||
class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
@customElement("dialog-system-information")
|
||||
class DialogSystemInformation extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() private _info?: SystemHealthInfo;
|
||||
|
||||
@state() private _supervisorStats?: HassioStats;
|
||||
@state() private _systemInfo?: SystemHealthInfo;
|
||||
|
||||
@state() private _resolutionInfo?: HassioResolution;
|
||||
|
||||
@state() private _supervisorStats?: HassioStats;
|
||||
|
||||
@state() private _coreStats?: HassioStats;
|
||||
|
||||
@state() private _error?: { code: string; message: string };
|
||||
@state() private _opened = false;
|
||||
|
||||
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
|
||||
private _subscriptions?: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>>;
|
||||
|
||||
public showDialog(): void {
|
||||
this._opened = true;
|
||||
this.hass!.loadBackendTranslation("system_health");
|
||||
this._subscribe();
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._opened = false;
|
||||
this._unsubscribe();
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _subscribe(): void {
|
||||
const subs: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> = [];
|
||||
if (isComponentLoaded(this.hass, "system_health")) {
|
||||
subs.push(
|
||||
subscribeSystemHealthInfo(this.hass!, (info) => {
|
||||
this._info = info;
|
||||
this._systemInfo = info;
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -93,149 +102,51 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
10000
|
||||
)
|
||||
);
|
||||
|
||||
fetchHassioResolution(this.hass).then((data) => {
|
||||
this._resolutionInfo = data;
|
||||
});
|
||||
}
|
||||
|
||||
return subs;
|
||||
this._subscriptions = subs;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
private _unsubscribe() {
|
||||
while (this._subscriptions?.length) {
|
||||
const unsub = this._subscriptions.pop()!;
|
||||
if (unsub instanceof Promise) {
|
||||
unsub.then((unsubFunc) => unsubFunc());
|
||||
} else {
|
||||
unsub();
|
||||
}
|
||||
}
|
||||
this._subscriptions = undefined;
|
||||
|
||||
this.hass!.loadBackendTranslation("system_health");
|
||||
this._systemInfo = undefined;
|
||||
this._resolutionInfo = undefined;
|
||||
this._coreStats = undefined;
|
||||
this._supervisorStats = undefined;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const sections: TemplateResult[] = [];
|
||||
|
||||
if (!this._info) {
|
||||
sections.push(
|
||||
html`
|
||||
<div class="loading-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
} else {
|
||||
const domains = Object.keys(this._info).sort(sortKeys);
|
||||
for (const domain of domains) {
|
||||
const domainInfo = this._info[domain];
|
||||
const keys: TemplateResult[] = [];
|
||||
|
||||
for (const key of Object.keys(domainInfo.info)) {
|
||||
let value: unknown;
|
||||
|
||||
if (
|
||||
domainInfo.info[key] &&
|
||||
typeof domainInfo.info[key] === "object"
|
||||
) {
|
||||
const info = domainInfo.info[key] as SystemCheckValueObject;
|
||||
|
||||
if (info.type === "pending") {
|
||||
value = html`
|
||||
<ha-circular-progress active size="tiny"></ha-circular-progress>
|
||||
`;
|
||||
} else if (info.type === "failed") {
|
||||
value = html`
|
||||
<span class="error">${info.error}</span>${!info.more_info
|
||||
? ""
|
||||
: html`
|
||||
–
|
||||
<a
|
||||
href=${info.more_info}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.system_health.more_info"
|
||||
)}
|
||||
</a>
|
||||
`}
|
||||
`;
|
||||
} else if (info.type === "date") {
|
||||
value = formatDateTime(new Date(info.value), this.hass.locale);
|
||||
}
|
||||
} else {
|
||||
value = domainInfo.info[key];
|
||||
}
|
||||
|
||||
keys.push(html`
|
||||
<tr>
|
||||
<td>
|
||||
${this.hass.localize(
|
||||
`component.${domain}.system_health.info.${key}`
|
||||
) || key}
|
||||
</td>
|
||||
<td>${value}</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
if (domain !== "homeassistant") {
|
||||
sections.push(
|
||||
html`
|
||||
<div class="card-header">
|
||||
<h3>${domainToName(this.hass.localize, domain)}</h3>
|
||||
${!domainInfo.manage_url
|
||||
? ""
|
||||
: html`
|
||||
<a class="manage" href=${domainInfo.manage_url}>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.system_health.manage"
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
sections.push(html`
|
||||
<table>
|
||||
${keys}
|
||||
</table>
|
||||
`);
|
||||
}
|
||||
if (!this._opened) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const sections = this._getSections();
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/system"
|
||||
.header=${this.hass.localize("ui.panel.config.system_health.caption")}
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.repairs.system_information")
|
||||
)}
|
||||
>
|
||||
${this._error
|
||||
? html`
|
||||
<ha-alert alert-type="error"
|
||||
>${this._error.message || this._error.code}</ha-alert
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${this._info
|
||||
? html`
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
@action=${this._copyInfo}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.panel.config.info.copy_menu")}
|
||||
.path=${mdiContentCopy}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item>
|
||||
${this.hass.localize("ui.panel.config.info.copy_raw")}
|
||||
</mwc-list-item>
|
||||
<mwc-list-item>
|
||||
${this.hass.localize("ui.panel.config.info.copy_github")}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
<div>
|
||||
${this._resolutionInfo
|
||||
? html`${this._resolutionInfo.unhealthy.length
|
||||
? html`<ha-alert alert-type="error">
|
||||
@ -265,66 +176,63 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
: ""} `
|
||||
: ""}
|
||||
|
||||
<ha-card outlined>
|
||||
<div class="card-content">${sections}</div>
|
||||
</ha-card>
|
||||
<div>${sections}</div>
|
||||
|
||||
${!this._coreStats && !this._supervisorStats
|
||||
? ""
|
||||
: html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${this._coreStats
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_health.core_stats"
|
||||
)}
|
||||
</h3>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.cpu_usage"
|
||||
)}
|
||||
.value=${this._coreStats.cpu_percent}
|
||||
></ha-metric>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.ram_usage"
|
||||
)}
|
||||
.value=${this._coreStats.memory_percent}
|
||||
></ha-metric>
|
||||
`
|
||||
: ""}
|
||||
${this._supervisorStats
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_health.supervisor_stats"
|
||||
)}
|
||||
</h3>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.cpu_usage"
|
||||
)}
|
||||
.value=${this._supervisorStats.cpu_percent}
|
||||
></ha-metric>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.ram_usage"
|
||||
)}
|
||||
.value=${this._supervisorStats.memory_percent}
|
||||
></ha-metric>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-card>
|
||||
<div>
|
||||
${this._coreStats
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_health.core_stats"
|
||||
)}
|
||||
</h3>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.cpu_usage"
|
||||
)}
|
||||
.value=${this._coreStats.cpu_percent}
|
||||
></ha-metric>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.ram_usage"
|
||||
)}
|
||||
.value=${this._coreStats.memory_percent}
|
||||
></ha-metric>
|
||||
`
|
||||
: ""}
|
||||
${this._supervisorStats
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_health.supervisor_stats"
|
||||
)}
|
||||
</h3>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.cpu_usage"
|
||||
)}
|
||||
.value=${this._supervisorStats.cpu_percent}
|
||||
></ha-metric>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.system_health.ram_usage"
|
||||
)}
|
||||
.value=${this._supervisorStats.memory_percent}
|
||||
></ha-metric>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
|
||||
<integrations-card
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></integrations-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.label=${this.hass.localize("ui.panel.config.repairs.copy")}
|
||||
@click=${this._copyInfo}
|
||||
></mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -386,17 +294,111 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private async _copyInfo(ev: CustomEvent<ActionDetail>): Promise<void> {
|
||||
const github = ev.detail.index === 1;
|
||||
private _getSections(): TemplateResult[] {
|
||||
const sections: TemplateResult[] = [];
|
||||
|
||||
if (!this._systemInfo) {
|
||||
sections.push(
|
||||
html`
|
||||
<div class="loading-container">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
} else {
|
||||
const domains = Object.keys(this._systemInfo).sort(sortKeys);
|
||||
for (const domain of domains) {
|
||||
const domainInfo = this._systemInfo[domain];
|
||||
const keys: TemplateResult[] = [];
|
||||
|
||||
for (const key of Object.keys(domainInfo.info)) {
|
||||
let value: unknown;
|
||||
|
||||
if (
|
||||
domainInfo.info[key] &&
|
||||
typeof domainInfo.info[key] === "object"
|
||||
) {
|
||||
const info = domainInfo.info[key] as SystemCheckValueObject;
|
||||
|
||||
if (info.type === "pending") {
|
||||
value = html`
|
||||
<ha-circular-progress active size="tiny"></ha-circular-progress>
|
||||
`;
|
||||
} else if (info.type === "failed") {
|
||||
value = html`
|
||||
<span class="error">${info.error}</span>${!info.more_info
|
||||
? ""
|
||||
: html`
|
||||
–
|
||||
<a
|
||||
href=${info.more_info}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.system_health.more_systemInfo"
|
||||
)}
|
||||
</a>
|
||||
`}
|
||||
`;
|
||||
} else if (info.type === "date") {
|
||||
value = formatDateTime(new Date(info.value), this.hass.locale);
|
||||
}
|
||||
} else {
|
||||
value = domainInfo.info[key];
|
||||
}
|
||||
|
||||
keys.push(html`
|
||||
<tr>
|
||||
<td>
|
||||
${this.hass.localize(
|
||||
`component.${domain}.system_health.info.${key}`
|
||||
) || key}
|
||||
</td>
|
||||
<td>${value}</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
if (domain !== "homeassistant") {
|
||||
sections.push(
|
||||
html`
|
||||
<div class="card-header">
|
||||
<h3>${domainToName(this.hass.localize, domain)}</h3>
|
||||
${!domainInfo.manage_url
|
||||
? ""
|
||||
: html`
|
||||
<a class="manage" href=${domainInfo.manage_url}>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.info.system_health.manage"
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
sections.push(html`
|
||||
<table>
|
||||
${keys}
|
||||
</table>
|
||||
`);
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
private async _copyInfo(): Promise<void> {
|
||||
let haContent: string | undefined;
|
||||
const domainParts: string[] = [];
|
||||
|
||||
for (const domain of Object.keys(this._info!).sort(sortKeys)) {
|
||||
const domainInfo = this._info![domain];
|
||||
for (const domain of Object.keys(this._systemInfo!).sort(sortKeys)) {
|
||||
const domainInfo = this._systemInfo![domain];
|
||||
let first = true;
|
||||
const parts = [
|
||||
`${
|
||||
github && domain !== "homeassistant"
|
||||
domain !== "homeassistant"
|
||||
? `<details><summary>${domainToName(
|
||||
this.hass.localize,
|
||||
domain
|
||||
@ -408,7 +410,7 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
for (const key of Object.keys(domainInfo.info)) {
|
||||
let value: unknown;
|
||||
|
||||
if (typeof domainInfo.info[key] === "object") {
|
||||
if (domainInfo.info[key] && typeof domainInfo.info[key] === "object") {
|
||||
const info = domainInfo.info[key] as SystemCheckValueObject;
|
||||
|
||||
if (info.type === "pending") {
|
||||
@ -421,11 +423,11 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
} else {
|
||||
value = domainInfo.info[key];
|
||||
}
|
||||
if (github && first) {
|
||||
if (first) {
|
||||
parts.push(`${key} | ${value}\n-- | --`);
|
||||
first = false;
|
||||
} else {
|
||||
parts.push(`${key}${github ? " | " : ": "}${value}`);
|
||||
parts.push(`${key} | ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -433,16 +435,14 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
haContent = parts.join("\n");
|
||||
} else {
|
||||
domainParts.push(parts.join("\n"));
|
||||
if (github && domain !== "homeassistant") {
|
||||
if (domain !== "homeassistant") {
|
||||
domainParts.push("</details>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await copyToClipboard(
|
||||
`${github ? "## " : ""}System Health\n${haContent}\n\n${domainParts.join(
|
||||
"\n\n"
|
||||
)}`
|
||||
`${"## "}System Information\n${haContent}\n\n${domainParts.join("\n\n")}`
|
||||
);
|
||||
|
||||
showToast(this, {
|
||||
@ -450,73 +450,50 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
integrations-card {
|
||||
max-width: 600px;
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
static styles: CSSResultGroup = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-alert {
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 45%;
|
||||
}
|
||||
td:first-child {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
direction: ltr;
|
||||
}
|
||||
td:last-child {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.card-header {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
a.manage {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
a.manage {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-system-health": HaConfigSystemHealth;
|
||||
"dialog-system-information": DialogSystemInformation;
|
||||
}
|
||||
}
|
187
src/panels/config/repairs/ha-config-repairs-dashboard.ts
Normal file
187
src/panels/config/repairs/ha-config-repairs-dashboard.ts
Normal 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;
|
||||
}
|
||||
}
|
143
src/panels/config/repairs/ha-config-repairs.ts
Normal file
143
src/panels/config/repairs/ha-config-repairs.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -21,8 +21,8 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
|
||||
@customElement("integrations-card")
|
||||
class IntegrationsCard extends LitElement {
|
||||
@customElement("integrations-startup-time")
|
||||
class IntegrationsStartupTime extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@ -45,57 +45,47 @@ class IntegrationsCard extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.system_health.integration_start_time"
|
||||
)}
|
||||
>
|
||||
<mwc-list>
|
||||
${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
|
||||
: "";
|
||||
<mwc-list>
|
||||
${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);
|
||||
return html`
|
||||
<ha-clickable-list-item
|
||||
graphic="avatar"
|
||||
twoline
|
||||
hasMeta
|
||||
openNewTab
|
||||
@click=${this._entryClicked}
|
||||
href=${docLink}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
src=${brandsUrl({
|
||||
domain: setup.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
referrerpolicy="no-referrer"
|
||||
slot="graphic"
|
||||
/>
|
||||
<span>
|
||||
${domainToName(this.hass.localize, setup.domain, manifest)}
|
||||
</span>
|
||||
<span slot="secondary">${setup.domain}</span>
|
||||
<div slot="meta">
|
||||
${setupSeconds ? html`${setupSeconds} s` : ""}
|
||||
</div>
|
||||
</ha-clickable-list-item>
|
||||
`;
|
||||
})}
|
||||
</mwc-list>
|
||||
</ha-card>
|
||||
const setupSeconds = setup.seconds?.toFixed(2);
|
||||
return html`
|
||||
<ha-clickable-list-item
|
||||
graphic="avatar"
|
||||
twoline
|
||||
hasMeta
|
||||
openNewTab
|
||||
@click=${this._entryClicked}
|
||||
href=${docLink}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
src=${brandsUrl({
|
||||
domain: setup.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
referrerpolicy="no-referrer"
|
||||
slot="graphic"
|
||||
/>
|
||||
<span>
|
||||
${domainToName(this.hass.localize, setup.domain, manifest)}
|
||||
</span>
|
||||
<span slot="secondary">${setup.domain}</span>
|
||||
<div slot="meta">
|
||||
${setupSeconds ? html`${setupSeconds} s` : ""}
|
||||
</div>
|
||||
</ha-clickable-list-item>
|
||||
`;
|
||||
})}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -149,6 +139,6 @@ class IntegrationsCard extends LitElement {
|
||||
|
||||
declare global {
|
||||
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
Loading…
x
Reference in New Issue
Block a user