Merge pull request #13286 from home-assistant/dev

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
.nvmrc
View File

@ -1 +1 @@
14
16

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20220707.1"
version = "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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,19 +14,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;

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import "@material/mwc-list/mwc-list-item";
import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { HassEntities } from "home-assistant-js-websocket";
import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@ -23,6 +23,11 @@ import "../../../components/ha-menu-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tip";
import { CloudStatus } from "../../../data/cloud";
import {
RepairsIssue,
severitySort,
subscribeRepairsIssueRegistry,
} from "../../../data/repairs";
import {
checkForEntityUpdates,
filterUpdateEntitiesWithInstall,
@ -31,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;
}
`,
];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,9 +6,9 @@ import {
mdiCog,
mdiDatabase,
mdiDevices,
mdiHeart,
mdiInformation,
mdiInformationOutline,
mdiLifebuoy,
mdiLightningBolt,
mdiMapMarkerRadius,
mdiMathLog,
@ -267,6 +267,12 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiUpdate,
iconColor: "#3B808E",
},
{
path: "/config/repairs",
translationKey: "repairs",
iconPath: mdiLifebuoy,
iconColor: "#5c995c",
},
{
component: "logs",
path: "/config/logs",
@ -315,13 +321,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#301A8E",
component: "hassio",
},
{
path: "/config/system_health",
translationKey: "system_health",
iconPath: mdiHeart,
iconColor: "#507FfE",
components: ["system_health", "hassio"],
},
],
about: [
{
@ -440,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"),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,8 +21,8 @@ import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { 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