Compare commits

..

15 Commits

Author SHA1 Message Date
Zack
026d027386 Update Struct for Buttons Footer 2022-05-25 23:13:27 -05:00
J. Nick Koston
d0ead1fdb8 Fix history cache when there is cacheConfig (#12788) 2022-05-25 20:39:55 -05:00
J. Nick Koston
b0e6c41238 Handle history api being passed entity ids as CSV (#12787) 2022-05-25 23:05:56 +00:00
Philip Allgaier
2c1550b10f Fix typo in credentials removal dialog (#12784) 2022-05-25 18:09:15 +00:00
Philip Allgaier
ffc4ca5b56 Use dynamic weather domain icon + icon alignment fix weather more-info (#12781) 2022-05-25 19:39:34 +02:00
Zack Barett
85ad6619b7 Bumped version to 20220525.0 (#12779) 2022-05-25 11:49:40 -05:00
Raman Gupta
7358faf88e Update zwave_js/network_status WS API (#12735) 2022-05-25 11:49:25 -05:00
Philip Allgaier
19d014307a Ensure state is vertically centered in more-info (#12780) 2022-05-25 11:40:04 -05:00
Philip Allgaier
5217f5c50c Fix "unavailable" handling for climate state rendering (#12778) 2022-05-25 15:47:11 +00:00
Zack Barett
c4624faa71 Hardware MVP (#12773) 2022-05-25 17:11:15 +02:00
Steve Repsher
b35ba4d673 Add aria-haspopup to button menus (#12758)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-05-25 16:05:43 +02:00
Marc Mueller
f8303bff76 Move metadata to pyproject.toml (#12770) 2022-05-25 08:16:09 -05:00
Zack Barett
e61aa266a6 Move Logbook and make device page better (#12763)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-05-25 12:55:08 +00:00
Raman Gupta
d7971c69ad Add controller statistics to zwave_js config dashboard (#12668) 2022-05-25 11:36:27 +02:00
Raman Gupta
966a624ef6 Move zwave_js node comments from device config to info page (#12625)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-05-24 21:16:07 -05:00
41 changed files with 1355 additions and 952 deletions

View File

@@ -26,8 +26,8 @@ module.exports = {
},
version() {
const version = fs
.readFileSync(path.resolve(paths.polymer_dir, "setup.cfg"), "utf8")
.match(/version\W+=\W(\d{8}\.\d)/);
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8")
.match(/version\W+=\W"(\d{8}\.\d)"/);
if (!version) {
throw Error("Version not found");
}

View File

@@ -1,3 +1,30 @@
[build-system]
requires = ["setuptools~=60.5", "wheel~=0.37.1"]
requires = ["setuptools~=62.3", "wheel~=0.37.1"]
build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20220525.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
authors = [
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
]
requires-python = ">=3.4.0"
[project.urls]
"Homepage" = "https://github.com/home-assistant/frontend"
[tool.setuptools]
platforms = ["any"]
zip-safe = false
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

@@ -50,14 +50,14 @@ async function main(args) {
return;
}
const setup = fs.readFileSync("setup.cfg", "utf8");
const version = setup.match(/\d{8}\.\d+/)[0];
const setup = fs.readFileSync("pyproject.toml", "utf8");
const version = setup.match(/version\W+=\W"(\d{8}\.\d)"/)[1];
const newVersion = method(version);
console.log("Current version:", version);
console.log("New version:", newVersion);
fs.writeFileSync("setup.cfg", setup.replace(version, newVersion), "utf-8");
fs.writeFileSync("pyproject.toml", setup.replace(version, newVersion), "utf-8");
if (!commit) {
return;

View File

@@ -1,26 +0,0 @@
[metadata]
name = home-assistant-frontend
version = 20220524.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0
platforms = any
description = The Home Assistant frontend
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/home-assistant/frontend
[options]
packages = find:
zip_safe = False
include_package_data = True
python_requires = >= 3.4.0
[options.packages.find]
include =
hass_frontend*
[mypy]
python_version = 3.4
show_error_codes = True
strict = True

View File

@@ -1,6 +1,11 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE_STATES } from "../../data/entity";
export const computeActiveState = (stateObj: HassEntity): string => {
if (UNAVAILABLE_STATES.includes(stateObj.state)) {
return stateObj.state;
}
const domain = stateObj.entity_id.split(".")[0];
let state = stateObj.state;

View File

@@ -29,7 +29,8 @@ import {
mdiWeatherNight,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { updateIsInstalling, UpdateEntity } from "../../data/update";
import { UpdateEntity, updateIsInstalling } from "../../data/update";
import { weatherIcon } from "../../data/weather";
/**
* Return the icon to be used for a domain.
*
@@ -101,6 +102,15 @@ export const domainIconWithoutDefault = (
? mdiCheckCircleOutline
: mdiCloseCircleOutline;
case "input_datetime":
if (!stateObj?.attributes.has_date) {
return mdiClock;
}
if (!stateObj.attributes.has_time) {
return mdiCalendar;
}
break;
case "lock":
switch (compareState) {
case "unlocked":
@@ -138,15 +148,6 @@ export const domainIconWithoutDefault = (
break;
}
case "input_datetime":
if (!stateObj?.attributes.has_date) {
return mdiClock;
}
if (!stateObj.attributes.has_time) {
return mdiCalendar;
}
break;
case "sun":
return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain]
@@ -158,6 +159,9 @@ export const domainIconWithoutDefault = (
? mdiPackageDown
: mdiPackageUp
: mdiPackage;
case "weather":
return weatherIcon(stateObj?.state);
}
if (domain in FIXED_DOMAIN_ICONS) {

View File

@@ -2,12 +2,7 @@ import type { Button } from "@material/mwc-button";
import "@material/mwc-menu";
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
customElement,
property,
query,
queryAssignedElements,
} from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import type { HaIconButton } from "./ha-icon-button";
@@ -33,12 +28,6 @@ export class HaButtonMenu extends LitElement {
@query("mwc-menu", true) private _menu?: Menu;
@queryAssignedElements({
slot: "trigger",
selector: "ha-icon-button, mwc-button",
})
private _triggerButton!: Array<HaIconButton | Button>;
public get items() {
return this._menu?.items;
}
@@ -51,14 +40,14 @@ export class HaButtonMenu extends LitElement {
if (this._menu?.open) {
this._menu.focusItemAtIndex(0);
} else {
this._triggerButton[0]?.focus();
this._triggerButton?.focus();
}
}
protected render(): TemplateResult {
return html`
<div @click=${this._handleClick}>
<slot name="trigger"></slot>
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
</div>
<mwc-menu
.corner=${this.corner}
@@ -97,6 +86,18 @@ export class HaButtonMenu extends LitElement {
this._menu!.show();
}
private get _triggerButton() {
return this.querySelector(
'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"]'
) as HaIconButton | Button | null;
}
private _setTriggerAria() {
if (this._triggerButton) {
this._triggerButton.ariaHasPopup = "menu";
}
}
static get styles(): CSSResultGroup {
return css`
:host {

View File

@@ -3,6 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { formatNumber } from "../common/number/format_number";
import { CLIMATE_PRESET_NONE } from "../data/climate";
import { UNAVAILABLE_STATES } from "../data/entity";
import type { HomeAssistant } from "../types";
@customElement("ha-climate-state")
@@ -15,22 +16,22 @@ class HaClimateState extends LitElement {
const currentStatus = this._computeCurrentStatus();
return html`<div class="target">
${this.stateObj.state !== "unknown"
${!UNAVAILABLE_STATES.includes(this.stateObj.state)
? html`<span class="state-label">
${this._localizeState()}
${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`-
${this.hass.localize(
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
) || this.stateObj.attributes.preset_mode}`
: ""}
</span>`
: ""}
<div class="unit">${this._computeTarget()}</div>
${this._localizeState()}
${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`-
${this.hass.localize(
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
) || this.stateObj.attributes.preset_mode}`
: ""}
</span>
<div class="unit">${this._computeTarget()}</div>`
: this._localizeState()}
</div>
${currentStatus
${currentStatus && !UNAVAILABLE_STATES.includes(this.stateObj.state)
? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
@@ -108,6 +109,10 @@ class HaClimateState extends LitElement {
}
private _localizeState(): string {
if (UNAVAILABLE_STATES.includes(this.stateObj.state)) {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = this.hass.localize(
`component.climate.state._.${this.stateObj.state}`
);

View File

@@ -2,6 +2,7 @@ import "@material/mwc-icon-button";
import type { IconButton } from "@material/mwc-icon-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "./ha-svg-icon";
@customElement("ha-icon-button")
@@ -12,7 +13,12 @@ export class HaIconButton extends LitElement {
@property({ type: String }) path?: string;
// Label that is used for ARIA support and as tooltip
@property({ type: String }) label = "";
@property({ type: String }) label?: string;
// These should always be set as properties, not attributes,
// so that only the <button> element gets the attribute
@property({ type: String, attribute: "aria-haspopup" })
override ariaHasPopup!: IconButton["ariaHasPopup"];
@property({ type: Boolean }) hideTitle = false;
@@ -28,11 +34,11 @@ export class HaIconButton extends LitElement {
};
protected render(): TemplateResult {
// Note: `ariaLabel` required despite the `mwc-icon-button` docs saying `label` should be enough
return html`
<mwc-icon-button
.ariaLabel=${this.label}
.title=${this.hideTitle ? "" : this.label}
aria-label=${ifDefined(this.label)}
title=${ifDefined(this.hideTitle ? undefined : this.label)}
aria-haspopup=${ifDefined(this.ariaHasPopup)}
.disabled=${this.disabled}
>
${this.path

View File

@@ -34,7 +34,7 @@ const RECENT_THRESHOLD = 60000; // 1 minute
const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {};
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
// Cached type 1 unction. Without cache config.
// Cached type 1 function. Without cache config.
export const getRecent = (
hass: HomeAssistant,
entityId: string,
@@ -103,13 +103,14 @@ export const getRecentWithCache = (
language: string
) => {
const cacheKey = cacheConfig.cacheKey;
const fullCacheKey = cacheKey + `_${cacheConfig.hoursToShow}`;
const endTime = new Date();
const startTime = new Date(endTime);
startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow);
let toFetchStartTime = startTime;
let appendingToCache = false;
let cache = stateHistoryCache[cacheKey + `_${cacheConfig.hoursToShow}`];
let cache = stateHistoryCache[fullCacheKey];
if (
cache &&
toFetchStartTime >= cache.startTime &&
@@ -123,7 +124,7 @@ export const getRecentWithCache = (
return cache.prom;
}
} else {
cache = stateHistoryCache[cacheKey] = getEmptyCache(
cache = stateHistoryCache[fullCacheKey] = getEmptyCache(
language,
startTime,
endTime
@@ -152,7 +153,7 @@ export const getRecentWithCache = (
]);
fetchedHistory = results[1];
} catch (err: any) {
delete stateHistoryCache[cacheKey];
delete stateHistoryCache[fullCacheKey];
throw err;
}
const stateHistory = computeHistory(hass, fetchedHistory, localize);

View File

@@ -20,3 +20,20 @@ export const BOARD_NAMES: Record<string, string> = {
"intel-nuc": "Intel NUC",
yellow: "Home Assistant Yellow",
};
export interface HardwareInfo {
hardware: HardwareInfoEntry[];
}
export interface HardwareInfoEntry {
board: HardwareInfoBoardInfo;
name: string;
url?: string;
}
export interface HardwareInfoBoardInfo {
manufacturer: string;
model?: string;
revision?: string;
hassio_board_id?: string;
}

View File

@@ -197,7 +197,7 @@ export const fetchRecent = (
export const fetchRecentWS = (
hass: HomeAssistant,
entityId: string,
entityId: string, // This may be CSV
startTime: Date,
endTime: Date,
skipInitialState = false,
@@ -213,7 +213,7 @@ export const fetchRecentWS = (
include_start_time_state: !skipInitialState,
minimal_response: minimalResponse,
no_attributes: noAttributes || false,
entity_ids: [entityId],
entity_ids: entityId.split(","),
});
export const fetchDate = (

View File

@@ -2,9 +2,21 @@ import {
mdiAlertCircleOutline,
mdiGauge,
mdiWaterPercent,
mdiWeatherCloudy,
mdiWeatherFog,
mdiWeatherHail,
mdiWeatherLightning,
mdiWeatherLightningRainy,
mdiWeatherNight,
mdiWeatherNightPartlyCloudy,
mdiWeatherPartlyCloudy,
mdiWeatherPouring,
mdiWeatherRainy,
mdiWeatherSnowy,
mdiWeatherSnowyRainy,
mdiWeatherSunny,
mdiWeatherWindy,
mdiWeatherWindyVariant,
} from "@mdi/js";
import {
HassEntityAttributeBase,
@@ -57,7 +69,21 @@ export const weatherSVGs = new Set<string>([
]);
export const weatherIcons = {
"clear-night": mdiWeatherNight,
cloudy: mdiWeatherCloudy,
exceptional: mdiAlertCircleOutline,
fog: mdiWeatherFog,
hail: mdiWeatherHail,
lightning: mdiWeatherLightning,
"lightning-rainy": mdiWeatherLightningRainy,
partlycloudy: mdiWeatherPartlyCloudy,
pouring: mdiWeatherPouring,
rainy: mdiWeatherRainy,
snowy: mdiWeatherSnowy,
"snowy-rainy": mdiWeatherSnowyRainy,
sunny: mdiWeatherSunny,
windy: mdiWeatherWindy,
"windy-variant": mdiWeatherWindyVariant,
};
export const weatherAttrIcons = {
@@ -437,6 +463,13 @@ export const getWeatherStateIcon = (
return undefined;
};
export const weatherIcon = (state?: string, nightTime?: boolean): string =>
!state
? undefined
: nightTime && state === "partlycloudy"
? mdiWeatherNightPartlyCloudy
: weatherIcons[state];
const DAY_IN_MILLISECONDS = 86400000;
export const isForecastHourly = (

View File

@@ -167,6 +167,9 @@ export interface ZwaveJSNodeMetadata {
wakeup: string;
reset: string;
device_database_url: string;
}
export interface ZwaveJSNodeComments {
comments: ZWaveJSNodeComment[];
}
@@ -227,6 +230,20 @@ export interface ZWaveJSHealNetworkStatusMessage {
heal_node_status: { [key: number]: string };
}
export interface ZWaveJSControllerStatisticsUpdatedMessage {
event: "statistics updated";
source: "controller";
messages_tx: number;
messages_rx: number;
messages_dropped_tx: number;
messages_dropped_rx: number;
nak: number;
can: number;
timeout_ack: number;
timeout_response: number;
timeout_callback: number;
}
export interface ZWaveJSRemovedNode {
node_id: number;
manufacturer: string;
@@ -284,12 +301,23 @@ export const migrateZwave = (
export const fetchZwaveNetworkStatus = (
hass: HomeAssistant,
entry_id: string
): Promise<ZWaveJSNetwork> =>
hass.callWS({
device_or_entry_id: {
device_id?: string;
entry_id?: string;
}
): Promise<ZWaveJSNetwork> => {
if (device_or_entry_id.device_id && device_or_entry_id.entry_id) {
throw new Error("Only one of device or entry ID should be supplied.");
}
if (!device_or_entry_id.device_id && !device_or_entry_id.entry_id) {
throw new Error("Either device or entry ID should be supplied.");
}
return hass.callWS({
type: "zwave_js/network_status",
entry_id,
device_id: device_or_entry_id.device_id,
entry_id: device_or_entry_id.entry_id,
});
};
export const fetchZwaveDataCollectionStatus = (
hass: HomeAssistant,
@@ -442,6 +470,15 @@ export const fetchZwaveNodeMetadata = (
device_id,
});
export const fetchZwaveNodeComments = (
hass: HomeAssistant,
device_id: string
): Promise<ZwaveJSNodeComments> =>
hass.callWS({
type: "zwave_js/node_comments",
device_id,
});
export const fetchZwaveNodeConfigParameters = (
hass: HomeAssistant,
device_id: string
@@ -547,6 +584,19 @@ export const subscribeHealZwaveNetworkProgress = (
}
);
export const subscribeZwaveControllerStatistics = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: ZWaveJSControllerStatisticsUpdatedMessage) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(
(message: any) => callbackFunction(message),
{
type: "zwave_js/subscribe_controller_statistics",
entry_id,
}
);
export const getZwaveJsIdentifiersFromDevice = (
device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined => {

View File

@@ -1,23 +1,9 @@
import {
mdiAlertCircleOutline,
mdiEye,
mdiGauge,
mdiThermometer,
mdiWaterPercent,
mdiWeatherCloudy,
mdiWeatherFog,
mdiWeatherHail,
mdiWeatherLightning,
mdiWeatherLightningRainy,
mdiWeatherNight,
mdiWeatherPartlyCloudy,
mdiWeatherPouring,
mdiWeatherRainy,
mdiWeatherSnowy,
mdiWeatherSnowyRainy,
mdiWeatherSunny,
mdiWeatherWindy,
mdiWeatherWindyVariant,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import {
@@ -37,27 +23,10 @@ import {
getWeatherUnit,
getWind,
isForecastHourly,
weatherIcons,
} from "../../../data/weather";
import { HomeAssistant } from "../../../types";
const weatherIcons = {
"clear-night": mdiWeatherNight,
cloudy: mdiWeatherCloudy,
exceptional: mdiAlertCircleOutline,
fog: mdiWeatherFog,
hail: mdiWeatherHail,
lightning: mdiWeatherLightning,
"lightning-rainy": mdiWeatherLightningRainy,
partlycloudy: mdiWeatherPartlyCloudy,
pouring: mdiWeatherPouring,
rainy: mdiWeatherRainy,
snowy: mdiWeatherSnowy,
"snowy-rainy": mdiWeatherSnowyRainy,
sunny: mdiWeatherSunny,
windy: mdiWeatherWindy,
"windy-variant": mdiWeatherWindyVariant,
};
@customElement("more-info-weather")
class MoreInfoWeather extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -235,6 +204,7 @@ class MoreInfoWeather extends LitElement {
return css`
ha-svg-icon {
color: var(--paper-item-icon-color);
margin-left: 8px;
}
.section {
margin: 16px 0 8px 0;

View File

@@ -19,7 +19,6 @@ import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../logbook/ha-logbook";
import {
AreaRegistryEntry,
deleteAreaRegistryEntry,
@@ -43,15 +42,16 @@ import { SceneEntity } from "../../../data/scene";
import { ScriptEntity } from "../../../data/script";
import { findRelated, RelatedResult } from "../../../data/search";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import "../../logbook/ha-logbook";
import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
import { configSections } from "../ha-panel-config";
import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
declare type NameAndEntity<EntityType extends HassEntity> = {
name: string;
@@ -761,10 +761,10 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
border-radius: 50%;
}
ha-logbook {
height: 800px;
height: 400px;
}
:host([narrow]) ha-logbook {
height: 400px;
height: 235px;
overflow: auto;
}
`,

View File

@@ -0,0 +1,13 @@
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import type { DeviceAction } from "../../../ha-config-device-page";
import { showMQTTDeviceDebugInfoDialog } from "./show-dialog-mqtt-device-debug-info";
export const getMQTTDeviceActions = (
el: HTMLElement,
device: DeviceRegistryEntry
): DeviceAction[] => [
{
label: "MQTT Info",
action: async () => showMQTTDeviceDebugInfoDialog(el, { device }),
},
];

View File

@@ -1,36 +0,0 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { showMQTTDeviceDebugInfoDialog } from "./show-dialog-mqtt-device-debug-info";
@customElement("ha-device-actions-mqtt")
export class HaDeviceActionsMqtt extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
protected render(): TemplateResult {
return html`
<mwc-button @click=${this._showDebugInfo}> MQTT Info </mwc-button>
`;
}
private async _showDebugInfo(): Promise<void> {
const device = this.device;
await showMQTTDeviceDebugInfoDialog(this, { device });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: flex;
justify-content: space-between;
}
`,
];
}
}

View File

@@ -0,0 +1,108 @@
import { navigate } from "../../../../../../common/navigate";
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchZHADevice } from "../../../../../../data/zha";
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../../../types";
import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster";
import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children";
import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info";
import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device";
import type { DeviceAction } from "../../../ha-config-device-page";
export const getZHADeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
device: DeviceRegistryEntry
): Promise<DeviceAction[]> => {
const zigbeeConnection = device.connections.find(
(conn) => conn[0] === "zigbee"
);
if (!zigbeeConnection) {
return [];
}
const zhaDevice = await fetchZHADevice(hass, zigbeeConnection[1]);
if (!zhaDevice) {
return [];
}
const actions: DeviceAction[] = [];
if (zhaDevice.device_type !== "Coordinator") {
actions.push({
label: hass.localize("ui.dialogs.zha_device_info.buttons.reconfigure"),
action: () => showZHAReconfigureDeviceDialog(el, { device: zhaDevice }),
});
}
if (
zhaDevice.power_source === "Mains" &&
(zhaDevice.device_type === "Router" ||
zhaDevice.device_type === "Coordinator")
) {
actions.push(
...[
{
label: hass.localize("ui.dialogs.zha_device_info.buttons.add"),
action: () => navigate(`/config/zha/add/${zhaDevice!.ieee}`),
},
{
label: hass.localize(
"ui.dialogs.zha_device_info.buttons.device_children"
),
action: () => showZHADeviceChildrenDialog(el, { device: zhaDevice! }),
},
]
);
}
if (zhaDevice.device_type !== "Coordinator") {
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}`),
},
{
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;
}
await hass.callService("zha", "remove", {
ieee: zhaDevice.ieee,
});
history.back();
},
},
]
);
}
return actions;
};

View File

@@ -1,155 +0,0 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../../../../../common/navigate";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchZHADevice, ZHADevice } from "../../../../../../data/zha";
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster";
import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children";
import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info";
import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device";
@customElement("ha-device-actions-zha")
export class HaDeviceActionsZha extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@state() private _zhaDevice?: ZHADevice;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const zigbeeConnection = this.device.connections.find(
(conn) => conn[0] === "zigbee"
);
if (!zigbeeConnection) {
return;
}
fetchZHADevice(this.hass, zigbeeConnection[1]).then((device) => {
this._zhaDevice = device;
});
}
}
protected render(): TemplateResult {
if (!this._zhaDevice) {
return html``;
}
return html`
${this._zhaDevice.device_type !== "Coordinator"
? html`
<mwc-button @click=${this._onReconfigureNodeClick}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.reconfigure"
)}
</mwc-button>
`
: ""}
${this._zhaDevice.power_source === "Mains" &&
(this._zhaDevice.device_type === "Router" ||
this._zhaDevice.device_type === "Coordinator")
? html`
<mwc-button @click=${this._onAddDevicesClick}>
${this.hass!.localize("ui.dialogs.zha_device_info.buttons.add")}
</mwc-button>
<mwc-button @click=${this._handleDeviceChildrenClicked}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.device_children"
)}
</mwc-button>
`
: ""}
${this._zhaDevice.device_type !== "Coordinator"
? html`
<mwc-button @click=${this._handleZigbeeInfoClicked}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.zigbee_information"
)}
</mwc-button>
<mwc-button @click=${this._showClustersDialog}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.clusters"
)}
</mwc-button>
<mwc-button @click=${this._onViewInVisualizationClick}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.view_in_visualization"
)}
</mwc-button>
<mwc-button class="warning" @click=${this._removeDevice}>
${this.hass!.localize(
"ui.dialogs.zha_device_info.buttons.remove"
)}
</mwc-button>
`
: ""}
`;
}
private async _showClustersDialog(): Promise<void> {
await showZHAClusterDialog(this, { device: this._zhaDevice! });
}
private async _onReconfigureNodeClick(): Promise<void> {
if (!this.hass) {
return;
}
showZHAReconfigureDeviceDialog(this, { device: this._zhaDevice! });
}
private _onAddDevicesClick() {
navigate(`/config/zha/add/${this._zhaDevice!.ieee}`);
}
private _onViewInVisualizationClick() {
navigate(`/config/zha/visualization/${this._zhaDevice!.device_reg_id}`);
}
private async _handleZigbeeInfoClicked() {
showZHADeviceZigbeeInfoDialog(this, { device: this._zhaDevice! });
}
private async _handleDeviceChildrenClicked() {
showZHADeviceChildrenDialog(this, { device: this._zhaDevice! });
}
private async _removeDevice() {
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
),
});
if (!confirmed) {
return;
}
await this.hass.callService("zha", "remove", {
ieee: this._zhaDevice!.ieee,
});
history.back();
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: flex;
flex-direction: column;
align-items: flex-start;
}
`,
];
}
}

View File

@@ -7,6 +7,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../../components/ha-expansion-panel";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchZHADevice, ZHADevice } from "../../../../../../data/zha";
import { haStyle } from "../../../../../../resources/styles";
@@ -40,38 +41,39 @@ export class HaDeviceActionsZha extends LitElement {
return html``;
}
return html`
<h4>Zigbee info</h4>
<div>IEEE: ${this._zhaDevice.ieee}</div>
<div>Nwk: ${formatAsPaddedHex(this._zhaDevice.nwk)}</div>
<div>Device Type: ${this._zhaDevice.device_type}</div>
<div>
LQI:
${this._zhaDevice.lqi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
RSSI:
${this._zhaDevice.rssi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.last_seen")}:
${this._zhaDevice.last_seen ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.power_source")}:
${this._zhaDevice.power_source ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
${this._zhaDevice.quirk_applied
? html`
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.quirk")}:
${this._zhaDevice.quirk_class}
</div>
`
: ""}
<ha-expansion-panel header="Zigbee info">
<div>IEEE: ${this._zhaDevice.ieee}</div>
<div>Nwk: ${formatAsPaddedHex(this._zhaDevice.nwk)}</div>
<div>Device Type: ${this._zhaDevice.device_type}</div>
<div>
LQI:
${this._zhaDevice.lqi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
RSSI:
${this._zhaDevice.rssi ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.last_seen")}:
${this._zhaDevice.last_seen ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.power_source")}:
${this._zhaDevice.power_source ||
this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
</div>
${this._zhaDevice.quirk_applied
? html`
<div>
${this.hass!.localize("ui.dialogs.zha_device_info.quirk")}:
${this._zhaDevice.quirk_class}
</div>
`
: ""}
</ha-expansion-panel>
`;
}
@@ -86,6 +88,11 @@ export class HaDeviceActionsZha extends LitElement {
word-break: break-all;
margin-top: 2px;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0;
--expansion-panel-content-padding: 0;
padding-top: 4px;
}
`,
];
}

View File

@@ -0,0 +1,68 @@
import { getConfigEntries } from "../../../../../../data/config_entries";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchZwaveNodeStatus } from "../../../../../../data/zwave_js";
import type { HomeAssistant } from "../../../../../../types";
import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node";
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
import type { DeviceAction } from "../../../ha-config-device-page";
export const getZwaveDeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
device: DeviceRegistryEntry
): Promise<DeviceAction[]> => {
const configEntries = await getConfigEntries(hass, {
domain: "zwave_js",
});
const configEntry = configEntries.find((entry) =>
device.config_entries.includes(entry.entry_id)
);
if (!configEntry) {
return [];
}
const entryId = configEntry.entry_id;
const node = await fetchZwaveNodeStatus(hass, device.id);
if (!node || node.is_controller_node) {
return [];
}
return [
{
label: hass.localize(
"ui.panel.config.zwave_js.device_info.device_config"
),
href: `/config/zwave_js/node_config/${device.id}?config_entry=${entryId}`,
},
{
label: hass.localize(
"ui.panel.config.zwave_js.device_info.reinterview_device"
),
action: () =>
showZWaveJSReinterviewNodeDialog(el, {
device_id: device.id,
}),
},
{
label: hass.localize("ui.panel.config.zwave_js.device_info.heal_node"),
action: () =>
showZWaveJSHealNodeDialog(el, {
device: device,
}),
},
{
label: hass.localize(
"ui.panel.config.zwave_js.device_info.remove_failed"
),
action: () =>
showZWaveJSRemoveFailedNodeDialog(el, {
device_id: device.id,
}),
},
];
};

View File

@@ -1,138 +0,0 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import {
fetchZwaveNodeStatus,
ZWaveJSNodeStatus,
} from "../../../../../../data/zwave_js";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node";
import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
import { getConfigEntries } from "../../../../../../data/config_entries";
@customElement("ha-device-actions-zwave_js")
export class HaDeviceActionsZWaveJS extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@state() private _entryId?: string;
@state() private _node?: ZWaveJSNodeStatus;
public willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (changedProperties.has("device")) {
this._fetchNodeDetails();
}
}
protected async _fetchNodeDetails() {
if (!this.device) {
return;
}
this._node = undefined;
const configEntries = await getConfigEntries(this.hass, {
domain: "zwave_js",
});
const configEntry = configEntries.find((entry) =>
this.device.config_entries.includes(entry.entry_id)
);
if (!configEntry) {
return;
}
this._entryId = configEntry.entry_id;
this._node = await fetchZwaveNodeStatus(this.hass, this.device.id);
}
protected render(): TemplateResult {
if (!this._node) {
return html``;
}
return html`
${!this._node.is_controller_node
? html`
<a
.href=${`/config/zwave_js/node_config/${this.device.id}?config_entry=${this._entryId}`}
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.device_config"
)}
</mwc-button>
</a>
<mwc-button @click=${this._reinterviewClicked}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.reinterview_device"
)}
</mwc-button>
<mwc-button @click=${this._healNodeClicked}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.heal_node"
)}
</mwc-button>
<mwc-button @click=${this._removeFailedNode}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.remove_failed"
)}
</mwc-button>
`
: ""}
`;
}
private async _reinterviewClicked() {
if (!this.device) {
return;
}
showZWaveJSReinterviewNodeDialog(this, {
device_id: this.device.id,
});
}
private async _healNodeClicked() {
if (!this.device) {
return;
}
showZWaveJSHealNodeDialog(this, {
entry_id: this._entryId!,
device: this.device,
});
}
private async _removeFailedNode() {
if (!this.device) {
return;
}
showZWaveJSRemoveFailedNodeDialog(this, {
device_id: this.device.id,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
a {
text-decoration: none;
}
`,
];
}
}

View File

@@ -0,0 +1,52 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import {
ZwaveJSNodeComments,
fetchZwaveNodeComments,
} from "../../../../../../data/zwave_js";
import { HomeAssistant } from "../../../../../../types";
@customElement("ha-device-alerts-zwave_js")
export class HaDeviceAlertsZWaveJS extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public device!: DeviceRegistryEntry;
@state() private _nodeComments?: ZwaveJSNodeComments;
protected willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (changedProperties.has("device")) {
this._fetchNodeDetails();
}
}
private async _fetchNodeDetails() {
this._nodeComments = await fetchZwaveNodeComments(
this.hass,
this.device.id
);
}
protected render(): TemplateResult {
if (this._nodeComments && this._nodeComments.comments?.length > 0) {
return html`
<div>
${this._nodeComments.comments.map(
(comment) => html`<ha-alert .alertType=${comment.level}>
${comment.text}
</ha-alert>`
)}
</div>
`;
}
return html``;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-alerts-zwave_js": HaDeviceAlertsZWaveJS;
}
}

View File

@@ -7,16 +7,17 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import "../../../../../../components/ha-expansion-panel";
import {
ConfigEntry,
getConfigEntries,
} from "../../../../../../data/config_entries";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import {
fetchZwaveNodeStatus,
nodeStatus,
ZWaveJSNodeStatus,
SecurityClass,
ZWaveJSNodeStatus,
} from "../../../../../../data/zwave_js";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
@@ -69,73 +70,76 @@ export class HaDeviceInfoZWaveJS extends LitElement {
return html``;
}
return html`
<h4>
${this.hass.localize("ui.panel.config.zwave_js.device_info.zwave_info")}
</h4>
${this._multipleConfigEntries
? html`
<div>
${this.hass.localize("ui.panel.config.zwave_js.common.source")}:
${this._configEntry!.title}
</div>
`
: ""}
<div>
${this.hass.localize("ui.panel.config.zwave_js.common.node_id")}:
${this._node.node_id}
</div>
${!this._node.is_controller_node
? html`
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.node_status"
)}:
${this.hass.localize(
`ui.panel.config.zwave_js.node_status.${
nodeStatus[this._node.status]
}`
)}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.node_ready"
)}:
${this._node.ready
? this.hass.localize("ui.common.yes")
: this.hass.localize("ui.common.no")}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.highest_security"
)}:
${this._node.highest_security_class !== null
? this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${
SecurityClass[this._node.highest_security_class]
}.title`
)
: this._node.is_secure === false
? this.hass.localize(
"ui.panel.config.zwave_js.security_classes.none.title"
)
: this.hass.localize(
"ui.panel.config.zwave_js.device_info.unknown"
)}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus"
)}:
${this._node.zwave_plus_version
? this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus_version",
"version",
this._node.zwave_plus_version
)
: this.hass.localize("ui.common.no")}
</div>
`
: ""}
<ha-expansion-panel
.header=${this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_info"
)}
>
${this._multipleConfigEntries
? html`
<div>
${this.hass.localize("ui.panel.config.zwave_js.common.source")}:
${this._configEntry!.title}
</div>
`
: ""}
<div>
${this.hass.localize("ui.panel.config.zwave_js.common.node_id")}:
${this._node.node_id}
</div>
${!this._node.is_controller_node
? html`
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.node_status"
)}:
${this.hass.localize(
`ui.panel.config.zwave_js.node_status.${
nodeStatus[this._node.status]
}`
)}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.node_ready"
)}:
${this._node.ready
? this.hass.localize("ui.common.yes")
: this.hass.localize("ui.common.no")}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.highest_security"
)}:
${this._node.highest_security_class !== null
? this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${
SecurityClass[this._node.highest_security_class]
}.title`
)
: this._node.is_secure === false
? this.hass.localize(
"ui.panel.config.zwave_js.security_classes.none.title"
)
: this.hass.localize(
"ui.panel.config.zwave_js.device_info.unknown"
)}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus"
)}:
${this._node.zwave_plus_version
? this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus_version",
"version",
this._node.zwave_plus_version
)
: this.hass.localize("ui.common.no")}
</div>
`
: ""}
</ha-expansion-panel>
`;
}
@@ -150,6 +154,11 @@ export class HaDeviceInfoZWaveJS extends LitElement {
word-break: break-all;
margin-top: 2px;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0;
--expansion-panel-content-padding: 0;
padding-top: 4px;
}
`,
];
}

View File

@@ -1,4 +1,9 @@
import { mdiOpenInNew, mdiPencil, mdiPlusCircle } from "@mdi/js";
import {
mdiDotsVertical,
mdiOpenInNew,
mdiPencil,
mdiPlusCircle,
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -13,6 +18,7 @@ import { slugify } from "../../../common/string/slugify";
import { groupBy } from "../../../common/util/group-by";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-svg-icon";
@@ -26,14 +32,14 @@ import {
import {
computeDeviceName,
DeviceRegistryEntry,
updateDeviceRegistryEntry,
removeConfigEntryFromDevice,
updateDeviceRegistryEntry,
} from "../../../data/device_registry";
import {
fetchDiagnosticHandler,
getDeviceDiagnosticsDownloadUrl,
getConfigEntryDiagnosticsDownloadUrl,
DiagnosticInfo,
fetchDiagnosticHandler,
getConfigEntryDiagnosticsDownloadUrl,
getDeviceDiagnosticsDownloadUrl,
} from "../../../data/diagnostics";
import {
EntityRegistryEntry,
@@ -51,9 +57,10 @@ import {
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import type { HomeAssistant, Route } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { fileDownload } from "../../../util/file_download";
import "../../logbook/ha-logbook";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "./device-detail/ha-device-entities-card";
@@ -63,12 +70,19 @@ import {
loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog,
} from "./device-registry-detail/show-dialog-device-registry-detail";
import "../../logbook/ha-logbook";
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null;
}
export interface DeviceAction {
href?: string;
action?: (ev: any) => void;
label: string;
trailingIcon?: string;
classes?: string;
}
@customElement("ha-config-device-page")
export class HaConfigDevicePage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -94,11 +108,11 @@ export class HaConfigDevicePage extends LitElement {
@state() private _related?: RelatedResult;
// If a number, it's the request ID so we make sure we don't show older info
@state() private _diagnosticDownloadLinks?:
| number
| (TemplateResult | string)[];
@state() private _diagnosticDownloadLinks?: number | DeviceAction[];
@state() private _deleteButtons?: (TemplateResult | string)[];
@state() private _deleteButtons?: DeviceAction[];
@state() private _deviceActions?: DeviceAction[];
private _logbookTime = { recent: 86400 };
@@ -196,15 +210,17 @@ export class HaConfigDevicePage extends LitElement {
if (
changedProps.has("deviceId") ||
changedProps.has("devices") ||
changedProps.has("deviceId") ||
changedProps.has("entries")
) {
this._diagnosticDownloadLinks = undefined;
this._deleteButtons = undefined;
this._deviceActions = undefined;
}
if (
(this._diagnosticDownloadLinks && this._deleteButtons) ||
(this._diagnosticDownloadLinks &&
this._deleteButtons &&
this._deviceActions) ||
!this.devices ||
!this.deviceId ||
!this.entries
@@ -214,129 +230,10 @@ export class HaConfigDevicePage extends LitElement {
this._diagnosticDownloadLinks = Math.random();
this._deleteButtons = []; // To prevent re-rendering if no delete buttons
this._renderDiagnosticButtons(this._diagnosticDownloadLinks);
this._renderDeleteButtons();
}
private async _renderDiagnosticButtons(requestId: number): Promise<void> {
if (!isComponentLoaded(this.hass, "diagnostics")) {
return;
}
const device = this._device(this.deviceId, this.devices);
if (!device) {
return;
}
let links = await Promise.all(
this._integrations(device, this.entries).map(
async (entry): Promise<boolean | { link: string; domain: string }> => {
if (entry.state !== "loaded") {
return false;
}
let info: DiagnosticInfo;
try {
info = await fetchDiagnosticHandler(this.hass, entry.domain);
} catch (err: any) {
if (err.code === "not_found") {
return false;
}
throw err;
}
if (!info.handlers.device && !info.handlers.config_entry) {
return false;
}
return {
link: info.handlers.device
? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId)
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id),
domain: entry.domain,
};
}
)
);
links = links.filter(Boolean);
if (this._diagnosticDownloadLinks !== requestId) {
return;
}
if (links.length > 0) {
this._diagnosticDownloadLinks = (
links as { link: string; domain: string }[]
).map(
(link) => html`
<a href=${link.link} @click=${this._signUrl}>
<mwc-button>
${links.length > 1
? this.hass.localize(
`ui.panel.config.devices.download_diagnostics_integration`,
{
integration: domainToName(
this.hass.localize,
link.domain
),
}
)
: this.hass.localize(
`ui.panel.config.devices.download_diagnostics`
)}
</mwc-button>
</a>
`
);
}
}
private _renderDeleteButtons() {
const device = this._device(this.deviceId, this.devices);
if (!device) {
return;
}
const buttons: TemplateResult[] = [];
this._integrations(device, this.entries).forEach((entry) => {
if (entry.state !== "loaded" || !entry.supports_remove_device) {
return;
}
buttons.push(html`
<mwc-button
class="warning"
.entryId=${entry.entry_id}
@click=${this._confirmDeleteEntry}
>
${buttons.length > 1
? this.hass.localize(
`ui.panel.config.devices.delete_device_integration`,
{
integration: domainToName(this.hass.localize, entry.domain),
}
)
: this.hass.localize(`ui.panel.config.devices.delete_device`)}
</mwc-button>
`);
});
if (buttons.length > 0) {
this._deleteButtons = buttons;
}
}
private async _confirmDeleteEntry(e: MouseEvent): Promise<void> {
const entryId = (e.currentTarget as any).entryId;
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
});
if (!confirmed) {
return;
}
await removeConfigEntryFromDevice(this.hass!, this.deviceId, entryId);
this._deviceActions = [];
this._getDiagnosticButtons(this._diagnosticDownloadLinks);
this._getDeleteActions();
this._getDeviceActions();
}
protected firstUpdated(changedProps) {
@@ -381,14 +278,18 @@ export class HaConfigDevicePage extends LitElement {
: undefined;
const area = this._computeArea(this.areas, device);
const configurationUrlIsHomeAssistant =
device.configuration_url?.startsWith("homeassistant://") || false;
const configurationUrl = configurationUrlIsHomeAssistant
? device.configuration_url!.replace("homeassistant://", "/")
: device.configuration_url;
const deviceInfo: TemplateResult[] = [];
const deviceAlerts: TemplateResult[] = [];
const actions = [...(this._deviceActions || [])];
if (Array.isArray(this._diagnosticDownloadLinks)) {
actions.push(...this._diagnosticDownloadLinks);
}
if (this._deleteButtons) {
actions.push(...this._deleteButtons);
}
const firstDeviceAction = actions.shift();
if (device.disabled_by) {
deviceInfo.push(
@@ -407,53 +308,19 @@ export class HaConfigDevicePage extends LitElement {
)}
</ha-alert>
${device.disabled_by === "user"
? html` <div class="card-actions" slot="actions">
<mwc-button unelevated @click=${this._enableDevice}>
${this.hass.localize("ui.common.enable")}
</mwc-button>
</div>`
? html`
<div class="card-actions" slot="actions">
<mwc-button unelevated @click=${this._enableDevice}>
${this.hass.localize("ui.common.enable")}
</mwc-button>
</div>
`
: ""}
`
);
}
const deviceActions: (TemplateResult | string)[] = [];
if (configurationUrl) {
deviceActions.push(html`
<a
href=${configurationUrl}
rel="noopener noreferrer"
.target=${configurationUrlIsHomeAssistant ? "_self" : "_blank"}
>
<mwc-button>
${this.hass.localize(
`ui.panel.config.devices.open_configuration_url_${
device.entry_type || "device"
}`
)}
<ha-svg-icon
.path=${mdiOpenInNew}
slot="trailingIcon"
></ha-svg-icon>
</mwc-button>
</a>
`);
}
this._renderIntegrationInfo(
device,
integrations,
deviceInfo,
deviceActions
);
if (Array.isArray(this._diagnosticDownloadLinks)) {
deviceActions.push(...this._diagnosticDownloadLinks);
}
if (this._deleteButtons) {
deviceActions.push(...this._deleteButtons);
}
this._renderIntegrationInfo(device, integrations, deviceInfo, deviceAlerts);
return html`
<hass-tabs-subpage
@@ -477,10 +344,6 @@ export class HaConfigDevicePage extends LitElement {
`
: ""
}
<div class="container">
<div class="header fullwidth">
${
@@ -547,6 +410,11 @@ export class HaConfigDevicePage extends LitElement {
</div>
</div>
<div class="column">
${
deviceAlerts.length
? html` <div class="fullwidth">${deviceAlerts}</div> `
: ""
}
<ha-device-info-card
.hass=${this.hass}
.areas=${this.areas}
@@ -555,57 +423,70 @@ export class HaConfigDevicePage extends LitElement {
>
${deviceInfo}
${
deviceActions.length
firstDeviceAction || actions.length
? html`
<div class="card-actions" slot="actions">
${deviceActions}
<div>
<a href=${ifDefined(firstDeviceAction!.href)}>
<mwc-button
class=${ifDefined(firstDeviceAction!.classes)}
.action=${firstDeviceAction!.action}
@click=${this._deviceActionClicked}
>
${firstDeviceAction!.label}
${firstDeviceAction!.trailingIcon
? html`
<ha-svg-icon
.path=${firstDeviceAction!.trailingIcon}
slot="trailingIcon"
></ha-svg-icon>
`
: ""}
</mwc-button>
</a>
</div>
${actions.length
? html`
<ha-button-menu corner="BOTTOM_START">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize(
"ui.common.menu"
)}
.path=${mdiDotsVertical}
></ha-icon-button>
${actions.map(
(deviceAction) => html`
<a href=${ifDefined(deviceAction.href)}>
<mwc-list-item
class=${ifDefined(
deviceAction.classes
)}
.action=${deviceAction.action}
@click=${this._deviceActionClicked}
>
${deviceAction.label}
${deviceAction.trailingIcon
? html`
<ha-svg-icon
.path=${deviceAction.trailingIcon}
></ha-svg-icon>
`
: ""}
</mwc-list-item>
</a>
`
)}
</ha-button-menu>
`
: ""}
</div>
`
: ""
}
</ha-device-info-card>
</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>
`
: ""
)}
${
isComponentLoaded(this.hass, "logbook")
? html`
<ha-card outlined>
<h1 class="card-header">
${this.hass.localize("panel.logbook")}
</h1>
<ha-logbook
.hass=${this.hass}
.time=${this._logbookTime}
.entityIds=${this._entityIds(entities)}
.deviceIds=${this._deviceIdInList(this.deviceId)}
virtualize
narrow
no-icon
></ha-logbook>
</ha-card>
`
: ""
}
</div>
<div class="column">
${
isComponentLoaded(this.hass, "automation")
? html`
@@ -874,12 +755,227 @@ export class HaConfigDevicePage extends LitElement {
`
: ""
}
</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>
`
: ""
)}
</div>
<div class="column">
${
isComponentLoaded(this.hass, "logbook")
? html`
<ha-card outlined>
<h1 class="card-header">
${this.hass.localize("panel.logbook")}
</h1>
<ha-logbook
.hass=${this.hass}
.time=${this._logbookTime}
.entityIds=${this._entityIds(entities)}
.deviceIds=${this._deviceIdInList(this.deviceId)}
virtualize
narrow
no-icon
></ha-logbook>
</ha-card>
`
: ""
}
</div>
</div>
</ha-config-section>
</hass-tabs-subpage> `;
}
private async _getDiagnosticButtons(requestId: number): Promise<void> {
if (!isComponentLoaded(this.hass, "diagnostics")) {
return;
}
const device = this._device(this.deviceId, this.devices);
if (!device) {
return;
}
let links = await Promise.all(
this._integrations(device, this.entries).map(
async (entry): Promise<boolean | { link: string; domain: string }> => {
if (entry.state !== "loaded") {
return false;
}
let info: DiagnosticInfo;
try {
info = await fetchDiagnosticHandler(this.hass, entry.domain);
} catch (err: any) {
if (err.code === "not_found") {
return false;
}
throw err;
}
if (!info.handlers.device && !info.handlers.config_entry) {
return false;
}
return {
link: info.handlers.device
? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId)
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id),
domain: entry.domain,
};
}
)
);
links = links.filter(Boolean);
if (this._diagnosticDownloadLinks !== requestId) {
return;
}
if (links.length > 0) {
this._diagnosticDownloadLinks = (
links as { link: string; domain: string }[]
).map((link) => ({
href: link.link,
action: (ev) => this._signUrl(ev),
label:
links.length > 1
? this.hass.localize(
`ui.panel.config.devices.download_diagnostics_integration`,
{
integration: domainToName(this.hass.localize, link.domain),
}
)
: this.hass.localize(
`ui.panel.config.devices.download_diagnostics`
),
}));
}
}
private _getDeleteActions() {
const device = this._device(this.deviceId, this.devices);
if (!device) {
return;
}
const buttons: DeviceAction[] = [];
this._integrations(device, this.entries).forEach((entry) => {
if (entry.state !== "loaded" || !entry.supports_remove_device) {
return;
}
buttons.push({
action: async () => {
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
});
if (!confirmed) {
return;
}
await removeConfigEntryFromDevice(
this.hass!,
this.deviceId,
entry.entry_id
);
},
classes: "warning",
label:
buttons.length > 1
? this.hass.localize(
`ui.panel.config.devices.delete_device_integration`,
{
integration: domainToName(this.hass.localize, entry.domain),
}
)
: this.hass.localize(`ui.panel.config.devices.delete_device`),
});
});
if (buttons.length > 0) {
this._deleteButtons = buttons;
}
}
private async _getDeviceActions() {
const device = this._device(this.deviceId, this.devices);
if (!device) {
return;
}
const deviceActions: DeviceAction[] = [];
const configurationUrlIsHomeAssistant =
device.configuration_url?.startsWith("homeassistant://") || false;
const configurationUrl = configurationUrlIsHomeAssistant
? device.configuration_url!.replace("homeassistant://", "/")
: device.configuration_url;
if (configurationUrl) {
deviceActions.push({
href: configurationUrl,
label: this.hass.localize(
`ui.panel.config.devices.open_configuration_url_${
device.entry_type || "device"
}`
),
trailingIcon: mdiOpenInNew,
});
}
const domains = this._integrations(device, this.entries).map(
(int) => int.domain
);
if (domains.includes("mqtt")) {
const mqtt = await import(
"./device-detail/integration-elements/mqtt/device-actions"
);
const actions = mqtt.getMQTTDeviceActions(this, device);
deviceActions.push(...actions);
}
if (domains.includes("zha")) {
const zha = await import(
"./device-detail/integration-elements/zha/device-actions"
);
const actions = await zha.getZHADeviceActions(this, this.hass, device);
deviceActions.push(...actions);
}
if (domains.includes("zwave_js")) {
const zwave = await import(
"./device-detail/integration-elements/zwave_js/device-actions"
);
const actions = await zwave.getZwaveDeviceActions(
this,
this.hass,
device
);
deviceActions.push(...actions);
}
this._deviceActions = deviceActions;
}
private _computeEntityName(entity: EntityRegistryEntry) {
if (entity.name) {
return entity.name;
@@ -928,22 +1024,10 @@ export class HaConfigDevicePage extends LitElement {
device: DeviceRegistryEntry,
integrations: ConfigEntry[],
deviceInfo: TemplateResult[],
deviceActions: (string | TemplateResult)[]
deviceAlerts: TemplateResult[]
) {
const domains = integrations.map((int) => int.domain);
if (domains.includes("mqtt")) {
import(
"./device-detail/integration-elements/mqtt/ha-device-actions-mqtt"
);
deviceActions.push(html`
<ha-device-actions-mqtt
.hass=${this.hass}
.device=${device}
></ha-device-actions-mqtt>
`);
}
if (domains.includes("zha")) {
import("./device-detail/integration-elements/zha/ha-device-actions-zha");
import("./device-detail/integration-elements/zha/ha-device-info-zha");
deviceInfo.push(html`
<ha-device-info-zha
@@ -951,32 +1035,26 @@ export class HaConfigDevicePage extends LitElement {
.device=${device}
></ha-device-info-zha>
`);
deviceActions.push(html`
<ha-device-actions-zha
.hass=${this.hass}
.device=${device}
></ha-device-actions-zha>
`);
}
if (domains.includes("zwave_js")) {
import(
"./device-detail/integration-elements/zwave_js/ha-device-info-zwave_js"
"./device-detail/integration-elements/zwave_js/ha-device-alerts-zwave_js"
);
import(
"./device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js"
"./device-detail/integration-elements/zwave_js/ha-device-info-zwave_js"
);
deviceAlerts.push(html`
<ha-device-alerts-zwave_js
.hass=${this.hass}
.device=${device}
></ha-device-alerts-zwave_js>
`);
deviceInfo.push(html`
<ha-device-info-zwave_js
.hass=${this.hass}
.device=${device}
></ha-device-info-zwave_js>
`);
deviceActions.push(html`
<ha-device-actions-zwave_js
.hass=${this.hass}
.device=${device}
></ha-device-actions-zwave_js>
`);
}
}
@@ -1115,8 +1193,7 @@ export class HaConfigDevicePage extends LitElement {
}
private async _signUrl(ev) {
const anchor = ev.target.closest("a");
ev.preventDefault();
const anchor = ev.currentTarget.closest("a");
const signedUrl = await getSignedPath(
this.hass,
anchor.getAttribute("href")
@@ -1124,6 +1201,16 @@ export class HaConfigDevicePage extends LitElement {
fileDownload(signedUrl.path);
}
private _deviceActionClicked(ev) {
if (!ev.currentTarget.action) {
return;
}
ev.preventDefault();
(ev.currentTarget as any).action(ev);
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -1154,9 +1241,6 @@ export class HaConfigDevicePage extends LitElement {
padding: 16px;
}
.show-more {
}
h1 {
margin: 0;
font-family: var(--paper-font-headline_-_font-family);
@@ -1265,7 +1349,19 @@ export class HaConfigDevicePage extends LitElement {
:host([narrow]) ha-logbook {
height: 235px;
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-device-page": HaConfigDevicePage;
}
}

View File

@@ -1,14 +1,18 @@
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-clickable-list-item";
import "../../../components/ha-icon-next";
import "../../../components/ha-settings-row";
import { BOARD_NAMES } from "../../../data/hardware";
import { BOARD_NAMES, HardwareInfo } from "../../../data/hardware";
import {
extractApiErrorMessage,
ignoreSupervisorError,
@@ -28,6 +32,8 @@ import {
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { hardwareBrandsUrl } from "../../../util/brands-url";
import { showToast } from "../../../util/toast";
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
@customElement("ha-config-hardware")
@@ -42,14 +48,36 @@ class HaConfigHardware extends LitElement {
@state() private _hostData?: HassioHostInfo;
@state() private _hardwareInfo?: HardwareInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (isComponentLoaded(this.hass, "hassio")) {
this._load();
}
this._load();
}
protected render(): TemplateResult {
let boardId: string | undefined;
let boardName: string | undefined;
let imageURL: string | undefined;
let documentationURL: string | undefined;
if (this._hardwareInfo?.hardware.length) {
const boardData = this._hardwareInfo!.hardware[0];
boardId = boardData.board.hassio_board_id;
boardName = boardData.name;
documentationURL = boardData.url;
imageURL = hardwareBrandsUrl({
category: "boards",
manufacturer: boardData.board.manufacturer,
model: boardData.board.model,
darkOptimized: this.hass.themes?.darkMode,
});
} else if (this._OSData?.board) {
boardId = this._OSData.board;
boardName = BOARD_NAMES[this._OSData.board];
}
return html`
<hass-subpage
back-path="/config/system"
@@ -68,6 +96,20 @@ class HaConfigHardware extends LitElement {
"ui.panel.config.hardware.available_hardware.title"
)}</mwc-list-item
>
${this._hostData
? html`
<mwc-list-item class="warning" @click=${this._hostReboot}
>${this.hass.localize(
"ui.panel.config.hardware.reboot_host"
)}</mwc-list-item
>
<mwc-list-item class="warning" @click=${this._hostShutdown}
>${this.hass.localize(
"ui.panel.config.hardware.shutdown_host"
)}</mwc-list-item
>
`
: ""}
</ha-button-menu>
${this._error
? html`
@@ -76,57 +118,55 @@ class HaConfigHardware extends LitElement {
>
`
: ""}
${this._OSData || this._hostData
${boardName
? html`
<div class="content">
<ha-card outlined>
${this._OSData?.board
? html`
<div class="card-content">
<ha-settings-row>
<span slot="heading"
>${BOARD_NAMES[this._OSData.board] ||
this.hass.localize(
"ui.panel.config.hardware.board"
)}</span
<div class="card-content">
<mwc-list>
<mwc-list-item
graphic=${ifDefined(imageURL ? "medium" : undefined)}
.twoline=${Boolean(boardId)}
>
${imageURL
? html`<img slot="graphic" src=${imageURL} />`
: ""}
<span class="primary-text">
${boardName ||
this.hass.localize("ui.panel.config.hardware.board")}
</span>
${boardId
? html`
<span class="secondary-text" slot="secondary"
>${boardId}</span
>
`
: ""}
</mwc-list-item>
${documentationURL
? html`
<ha-clickable-list-item
.href=${documentationURL}
openNewTab
twoline
hasMeta
>
<div slot="description">
<span class="value">${this._OSData.board}</span>
</div>
</ha-settings-row>
</div>
`
: ""}
${this._hostData
? html`
<div class="card-actions">
${this._hostData.features.includes("reboot")
? html`
<ha-progress-button
class="warning"
@click=${this._hostReboot}
>
${this.hass.localize(
"ui.panel.config.hardware.reboot_host"
)}
</ha-progress-button>
`
: ""}
${this._hostData.features.includes("shutdown")
? html`
<ha-progress-button
class="warning"
@click=${this._hostShutdown}
>
${this.hass.localize(
"ui.panel.config.hardware.shutdown_host"
)}
</ha-progress-button>
`
: ""}
</div>
`
: ""}
<span
>${this.hass.localize(
"ui.panel.config.hardware.documentation"
)}</span
>
<span slot="secondary"
>${this.hass.localize(
"ui.panel.config.hardware.documentation_description"
)}</span
>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-clickable-list-item>
`
: ""}
</mwc-list>
</div>
</ha-card>
</div>
`
@@ -136,9 +176,17 @@ class HaConfigHardware extends LitElement {
}
private async _load() {
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
try {
this._OSData = await fetchHassioHassOsInfo(this.hass);
this._hostData = await fetchHassioHostInfo(this.hass);
if (isComponentLoaded(this.hass, "hardware")) {
this._hardwareInfo = await this.hass.callWS({ type: "hardware/info" });
} else if (isHassioLoaded) {
this._OSData = await fetchHassioHassOsInfo(this.hass);
}
if (isHassioLoaded) {
this._hostData = await fetchHassioHostInfo(this.hass);
}
} catch (err: any) {
this._error = err.message || err;
}
@@ -148,10 +196,7 @@ class HaConfigHardware extends LitElement {
showhardwareAvailableDialog(this);
}
private async _hostReboot(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _hostReboot(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.hardware.reboot_host"),
text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"),
@@ -160,10 +205,14 @@ class HaConfigHardware extends LitElement {
});
if (!confirmed) {
button.progress = false;
return;
}
showToast(this, {
message: this.hass.localize("ui.panel.config.hardware.rebooting_host"),
duration: 0,
});
try {
await rebootHost(this.hass);
} catch (err: any) {
@@ -177,13 +226,9 @@ class HaConfigHardware extends LitElement {
});
}
}
button.progress = false;
}
private async _hostShutdown(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
private async _hostShutdown(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
text: this.hass.localize(
@@ -194,10 +239,16 @@ class HaConfigHardware extends LitElement {
});
if (!confirmed) {
button.progress = false;
return;
}
showToast(this, {
message: this.hass.localize(
"ui.panel.config.hardware.host_shutting_down"
),
duration: 0,
});
try {
await shutdownHost(this.hass);
} catch (err: any) {
@@ -211,7 +262,6 @@ class HaConfigHardware extends LitElement {
});
}
}
button.progress = false;
}
static styles = [
@@ -234,17 +284,18 @@ class HaConfigHardware extends LitElement {
display: flex;
justify-content: space-between;
flex-direction: column;
padding: 16px 16px 0 16px;
padding: 16px;
}
ha-button-menu {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
}
.card-actions {
height: 48px;
display: flex;
justify-content: space-between;
align-items: center;
.primary-text {
font-size: 16px;
}
.secondary-text {
font-size: 14px;
}
`,
];

View File

@@ -202,10 +202,9 @@ class DialogZWaveJSHealNetwork extends LitElement {
if (!this.hass) {
return;
}
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(
this.hass!,
this.entry_id!
);
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(this.hass!, {
entry_id: this.entry_id!,
});
if (network.controller.is_heal_network_active) {
this._status = "started";
this._subscribed = subscribeHealZwaveNetworkProgress(

View File

@@ -22,8 +22,6 @@ import { ZWaveJSHealNodeDialogParams } from "./show-dialog-zwave_js-heal-node";
class DialogZWaveJSHealNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private entry_id?: string;
@state() private device?: DeviceRegistryEntry;
@state() private _status?: string;
@@ -31,13 +29,11 @@ class DialogZWaveJSHealNode extends LitElement {
@state() private _error?: string;
public showDialog(params: ZWaveJSHealNodeDialogParams): void {
this.entry_id = params.entry_id;
this.device = params.device;
this._fetchData();
}
public closeDialog(): void {
this.entry_id = undefined;
this._status = undefined;
this.device = undefined;
this._error = undefined;
@@ -46,7 +42,7 @@ class DialogZWaveJSHealNode extends LitElement {
}
protected render(): TemplateResult {
if (!this.entry_id || !this.device) {
if (!this.device) {
return html``;
}
@@ -202,10 +198,9 @@ class DialogZWaveJSHealNode extends LitElement {
if (!this.hass) {
return;
}
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(
this.hass!,
this.entry_id!
);
const network: ZWaveJSNetwork = await fetchZwaveNetworkStatus(this.hass!, {
device_id: this.device!.id,
});
if (network.controller.is_heal_network_active) {
this._status = "network-healing";
}

View File

@@ -2,7 +2,6 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
export interface ZWaveJSHealNodeDialogParams {
entry_id: string;
device: DeviceRegistryEntry;
}

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import {
mdiAlertCircle,
mdiCheckCircle,
@@ -11,9 +12,12 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-fab";
import "../../../../../components/ha-help-tooltip";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-svg-icon";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
fetchZwaveDataCollectionStatus,
fetchZwaveNetworkStatus,
@@ -22,7 +26,9 @@ import {
setZwaveDataCollectionPreference,
stopZwaveExclusion,
stopZwaveInclusion,
subscribeZwaveControllerStatistics,
ZWaveJSClient,
ZWaveJSControllerStatisticsUpdatedMessage,
ZWaveJSNetwork,
ZwaveJSProvisioningEntry,
} from "../../../../../data/zwave_js";
@@ -41,9 +47,10 @@ import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node"
import { configTabs } from "./zwave_js-config-router";
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
import { computeRTL } from "../../../../../common/util/compute_rtl";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
@customElement("zwave_js-config-dashboard")
class ZWaveJSConfigDashboard extends LitElement {
class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@@ -52,7 +59,7 @@ class ZWaveJSConfigDashboard extends LitElement {
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public configEntryId!: string;
@state() private _configEntry?: ConfigEntry;
@@ -66,12 +73,30 @@ class ZWaveJSConfigDashboard extends LitElement {
@state() private _dataCollectionOptIn?: boolean;
@state()
private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage;
protected firstUpdated() {
if (this.hass) {
this._fetchData();
}
}
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
return [
subscribeZwaveControllerStatistics(
this.hass,
this.configEntryId,
(message) => {
if (!this.hasUpdated) {
return;
}
this._statistics = message;
}
),
];
}
protected render(): TemplateResult {
if (!this._configEntry) {
return html``;
@@ -211,22 +236,178 @@ class ZWaveJSConfigDashboard extends LitElement {
</ha-card>
<ha-card header="Diagnostics">
<div class="card-content">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.driver_version"
)}:
${this._network.client.driver_version}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_version"
)}:
${this._network.client.server_version}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.home_id"
)}:
${this._network.controller.home_id}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_url"
)}:
${this._network.client.ws_server_url}<br />
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.driver_version"
)}:
</span>
<span>${this._network.client.driver_version}</span>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_version"
)}:
</span>
<span>${this._network.client.server_version}</span>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.home_id"
)}:
</span>
<span>${this._network.controller.home_id}</span>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_url"
)}:
</span>
<span>${this._network.client.ws_server_url}</span>
</div>
<br />
<ha-expansion-panel
.header=${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.title"
)}
>
<mwc-list noninteractive>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_tx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_tx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_tx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_rx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_rx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_rx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_tx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_tx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_dropped_tx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_rx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_rx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_dropped_rx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.nak.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.nak.tooltip"
)}
</span>
<span slot="meta">${this._statistics?.nak ?? 0}</span>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.can.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.can.tooltip"
)}
</span>
<span slot="meta">${this._statistics?.can ?? 0}</span>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_ack.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_ack.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.timeout_ack ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_response.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_response.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.timeout_response ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_callback.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_callback.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.timeout_callback ?? 0}</span
>
</mwc-list-item>
</mwc-list>
</ha-expansion-panel>
</div>
<div class="card-actions">
<mwc-button
@@ -383,7 +564,7 @@ class ZWaveJSConfigDashboard extends LitElement {
domain: "zwave_js",
});
this._configEntry = configEntries.find(
(entry) => entry.entry_id === this.configEntryId!
(entry) => entry.entry_id === this.configEntryId
);
if (ERROR_STATES.includes(this._configEntry!.state)) {
@@ -392,7 +573,7 @@ class ZWaveJSConfigDashboard extends LitElement {
const [network, dataCollectionStatus, provisioningEntries] =
await Promise.all([
fetchZwaveNetworkStatus(this.hass!, this.configEntryId),
fetchZwaveNetworkStatus(this.hass!, { entry_id: this.configEntryId }),
fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId),
fetchZwaveProvisioningEntries(this.hass!, this.configEntryId),
]);
@@ -508,6 +689,11 @@ class ZWaveJSConfigDashboard extends LitElement {
padding-right: 40px;
}
.row {
display: flex;
justify-content: space-between;
}
.network-status div.heading {
display: flex;
align-items: center;
@@ -530,6 +716,10 @@ class ZWaveJSConfigDashboard extends LitElement {
font-size: 1rem;
}
mwc-list-item {
height: 60px;
}
.card-header {
display: flex;
}
@@ -546,12 +736,6 @@ class ZWaveJSConfigDashboard extends LitElement {
max-width: 600px;
}
button.dump {
width: 100%;
text-align: center;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}

View File

@@ -166,17 +166,6 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
</em>
</p>
</div>
${this._nodeMetadata.comments?.length > 0
? html`
<div>
${this._nodeMetadata.comments.map(
(comment) => html`<ha-alert .alertType=${comment.level}>
${comment.text}
</ha-alert>`
)}
</div>
`
: ``}
<ha-card>
${Object.entries(this._config).map(
([id, item]) => html` <ha-settings-row

View File

@@ -556,10 +556,6 @@ class HaLogbookRenderer extends LitElement {
padding: 0 16px;
}
.narrow .date {
padding: 0 8px;
}
.rtl .date {
direction: rtl;
}
@@ -590,6 +586,12 @@ class HaLogbookRenderer extends LitElement {
a {
color: var(--primary-color);
text-decoration: none;
}
button.link {
color: var(--paper-item-icon-color);
text-decoration: none;
}
.container {
@@ -607,7 +609,6 @@ class HaLogbookRenderer extends LitElement {
.narrow .entry {
line-height: 1.5;
padding: 8px;
}
.narrow .icon-message state-badge {

View File

@@ -155,7 +155,7 @@ class HuiGenericEntityRow extends LitElement {
</div>`
: html``}
${this.catchInteraction ?? !DOMAINS_INPUT_ROW.includes(domain)
? html` <div
? html`<div
class="text-content ${classMap({
pointer,
})}"
@@ -165,7 +165,7 @@ class HuiGenericEntityRow extends LitElement {
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
>
<slot></slot>
<div class="state"><slot></slot></div>
</div>`
: html`<slot></slot>`}
`;
@@ -230,6 +230,12 @@ class HuiGenericEntityRow extends LitElement {
.pointer {
cursor: pointer;
}
.state {
text-align: right;
}
.state.rtl {
text-align: left;
}
`;
}
}

View File

@@ -1,4 +1,4 @@
import { union, object, string, optional, boolean, enums } from "superstruct";
import { boolean, enums, object, optional, string, union } from "superstruct";
import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types";
import { actionConfigStruct } from "./action-struct";
@@ -14,6 +14,8 @@ export const entitiesConfigStruct = union([
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
show_icon: optional(boolean()),
show_name: optional(boolean()),
}),
string(),
]);

View File

@@ -158,6 +158,7 @@ export const buttonLinkStyle = css`
text-align: left;
text-decoration: underline;
cursor: pointer;
outline: none;
}
`;

View File

@@ -90,6 +90,8 @@ export class StateCardDisplay extends LitElement {
text-align: right;
flex: 0 0 auto;
overflow-wrap: break-word;
display: flex;
align-items: center;
}
:host([rtl]) .state {
margin-right: 16px;

View File

@@ -24,8 +24,9 @@ class StateCardInputNumber extends mixinBehaviors(
@apply --paper-font-body1;
color: var(--primary-text-color);
text-align: right;
line-height: 40px;
display: flex;
align-items: center;
justify-content: end;
}
.sliderstate {
min-width: 45px;

View File

@@ -23,8 +23,9 @@ class StateCardNumber extends mixinBehaviors(
@apply --paper-font-body1;
color: var(--primary-text-color);
text-align: right;
line-height: 40px;
display: flex;
align-items: center;
justify-content: end;
}
.sliderstate {
min-width: 45px;

View File

@@ -1569,12 +1569,16 @@
"attributes": "Attributes"
},
"reboot_host": "Reboot host",
"rebooting_host": "Rebooting host",
"reboot_host_confirm": "Are you sure you want to reboot your host?",
"failed_to_reboot_host": "Failed to reboot host",
"shutdown_host": "Shutdown host",
"host_shutting_down": "Host shutting down",
"shutdown_host_confirm": "Are you sure you want to shutdown your host?",
"failed_to_shutdown_host": "Failed to shutdown host",
"board": "Board"
"board": "Board",
"documentation": "Documentation",
"documentation_description": "Find extra information about your device"
},
"info": {
"caption": "About",
@@ -2905,7 +2909,7 @@
},
"remove_selected": {
"button": "Remove selected",
"confirm_title": "Do you want to remove {number} {number, plural,\n one {credential}\n other {credentialss}\n}?",
"confirm_title": "Do you want to remove {number} {number, plural,\n one {credential}\n other {credentials}\n}?",
"confirm_text": "Application Credentials in use by an integration may not be removed.",
"error_title": "Removing Application Credential failed"
},
@@ -3045,7 +3049,46 @@
"server_url": "Server URL",
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"provisioned_devices": "Provisioned devices",
"not_ready": "{count} not ready"
"not_ready": "{count} not ready",
"statistics": {
"title": "Controller Statistics",
"messages_tx": {
"label": "Messages TX",
"tooltip": "Number of messages successfully sent to the controller"
},
"messages_rx": {
"label": "Messages RX",
"tooltip": "Number of messages successfully received by the controller"
},
"messages_dropped_tx": {
"label": "Dropped Messages TX",
"tooltip": "Number of messages from the controller that were dropped by the host"
},
"messages_dropped_rx": {
"label": "Dropped Messages RX",
"tooltip": "Number of outgoing messages that were dropped because they could not be sent"
},
"nak": {
"label": "NAK",
"tooltip": "Number of messages that the controller did not accept"
},
"can": {
"label": "CAN",
"tooltip": "Number of collisions while sending a message to the controller"
},
"timeout_ack": {
"label": "Timeout ACK",
"tooltip": "Number of transmission attempts where an ACK was missing from the controller"
},
"timeout_response": {
"label": "Timeout Response",
"tooltip": "Number of transmission attempts where the controller response did not come in time"
},
"timeout_callback": {
"label": "Timeout Callback",
"tooltip": "Number of transmission attempts where the controller callback did not come in time"
}
}
},
"device_info": {
"zwave_info": "Z-Wave Info",

View File

@@ -5,9 +5,21 @@ export interface BrandsOptions {
darkOptimized?: boolean;
}
export interface HardwareBrandsOptions {
category: string;
model?: string;
manufacturer: string;
darkOptimized?: boolean;
}
export const brandsUrl = (options: BrandsOptions): string =>
`https://brands.home-assistant.io/${options.useFallback ? "_/" : ""}${
options.domain
}/${options.darkOptimized ? "dark_" : ""}${options.type}.png`;
export const hardwareBrandsUrl = (options: HardwareBrandsOptions): string =>
`https://brands.home-assistant.io/hardware/${options.category}/${
options.darkOptimized ? "dark_" : ""
}${options.manufacturer}${options.model ? `_${options.model}` : ""}.png`;
export const extractDomainFromBrandUrl = (url: string) => url.split("/")[4];