Add initial DHCP discovery panel (#25086)

This commit is contained in:
J. Nick Koston 2025-04-21 08:09:58 -10:00 committed by GitHub
parent 3647722824
commit 1c15116052
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 223 additions and 3 deletions

83
src/data/dhcp.ts Normal file
View File

@ -0,0 +1,83 @@
import {
createCollection,
type Connection,
type UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { Store } from "home-assistant-js-websocket/dist/store";
import type { DataTableRowData } from "../components/data-table/ha-data-table";
export interface DHCPDiscoveryData extends DataTableRowData {
mac_address: string;
hostname: string;
ip_address: string;
}
interface DHCPRemoveDiscoveryData {
mac_address: string;
}
interface DHCPSubscriptionMessage {
add?: DHCPDiscoveryData[];
change?: DHCPDiscoveryData[];
remove?: DHCPRemoveDiscoveryData[];
}
const subscribeDHCPDiscoveryUpdates = (
conn: Connection,
store: Store<DHCPDiscoveryData[]>
): Promise<UnsubscribeFunc> =>
conn.subscribeMessage<DHCPSubscriptionMessage>(
(event) => {
const data = [...(store.state || [])];
if (event.add) {
for (const deviceData of event.add) {
const index = data.findIndex(
(d) => d.mac_address === deviceData.mac_address
);
if (index === -1) {
data.push(deviceData);
} else {
data[index] = deviceData;
}
}
}
if (event.change) {
for (const deviceData of event.change) {
const index = data.findIndex(
(d) => d.mac_address === deviceData.mac_address
);
if (index !== -1) {
data[index] = deviceData;
}
}
}
if (event.remove) {
for (const deviceData of event.remove) {
const index = data.findIndex(
(d) => d.mac_address === deviceData.mac_address
);
if (index !== -1) {
data.splice(index, 1);
}
}
}
store.setState(data, true);
},
{
type: `dhcp/subscribe_discovery`,
}
);
export const subscribeDHCPDiscovery = (
conn: Connection,
callbackFunction: (dhcpDiscoveryData: DHCPDiscoveryData[]) => void
) =>
createCollection<DHCPDiscoveryData[]>(
"_dhcpDiscoveryRows",
() => Promise.resolve<DHCPDiscoveryData[]>([]), // empty array as initial state
subscribeDHCPDiscoveryUpdates,
conn,
callbackFunction
);

View File

@ -6,6 +6,7 @@ import { debounce } from "../common/util/debounce";
export const integrationsWithPanel = { export const integrationsWithPanel = {
bluetooth: "config/bluetooth", bluetooth: "config/bluetooth",
dhcp: "config/dhcp",
matter: "config/matter", matter: "config/matter",
mqtt: "config/mqtt", mqtt: "config/mqtt",
thread: "config/thread", thread: "config/thread",

View File

@ -8,6 +8,7 @@ import type { DeviceRegistryEntry } from "../../../../data/device_registry";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { createSearchParam } from "../../../../common/url/search-params"; import { createSearchParam } from "../../../../common/url/search-params";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
@customElement("ha-device-info-card") @customElement("ha-device-info-card")
export class HaDeviceCard extends LitElement { export class HaDeviceCard extends LitElement {
@ -103,7 +104,8 @@ export class HaDeviceCard extends LitElement {
${this._getAddresses().map( ${this._getAddresses().map(
([type, value]) => html` ([type, value]) => html`
<div class="extra-info"> <div class="extra-info">
${type === "bluetooth" ${type === "bluetooth" &&
isComponentLoaded(this.hass, "bluetooth")
? html`${titleCase(type)} ? html`${titleCase(type)}
<a <a
href="/config/bluetooth/advertisement-monitor?${createSearchParam( href="/config/bluetooth/advertisement-monitor?${createSearchParam(
@ -111,8 +113,16 @@ export class HaDeviceCard extends LitElement {
)}" )}"
>${value.toUpperCase()}</a >${value.toUpperCase()}</a
>` >`
: html`${type === "mac" ? "MAC" : titleCase(type)}: : type === "mac" && isComponentLoaded(this.hass, "dhcp")
${value.toUpperCase()}`} ? html`${titleCase(type)}
<a
href="/config/dhcp?${createSearchParam({
mac_address: value,
})}"
>${value.toUpperCase()}</a
>`
: html`${type === "mac" ? "MAC" : titleCase(type)}:
${value.toUpperCase()}`}
</div> </div>
` `
)} )}

View File

@ -555,6 +555,11 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
"./integrations/integration-panels/bluetooth/bluetooth-config-dashboard-router" "./integrations/integration-panels/bluetooth/bluetooth-config-dashboard-router"
), ),
}, },
dhcp: {
tag: "dhcp-config-panel",
load: () =>
import("./integrations/integration-panels/dhcp/dhcp-config-panel"),
},
application_credentials: { application_credentials: {
tag: "ha-config-application-credentials", tag: "ha-config-application-credentials",
load: () => load: () =>

View File

@ -0,0 +1,111 @@
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { DataTableColumnContainer } from "../../../../../components/data-table/ha-data-table";
import { extractSearchParamsObject } from "../../../../../common/url/search-params";
import "../../../../../components/ha-fab";
import "../../../../../components/ha-icon-button";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import type { DHCPDiscoveryData } from "../../../../../data/dhcp";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { subscribeDHCPDiscovery } from "../../../../../data/dhcp";
@customElement("dhcp-config-panel")
export class DHCPConfigPanel extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@state() private _macAddress?: string;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@state() private _data: DHCPDiscoveryData[] = [];
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDHCPDiscovery(this.hass.connection, (data) => {
this._data = data;
}),
];
}
private _columns = memoizeOne(
(localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<DHCPDiscoveryData> = {
mac_address: {
title: localize("ui.panel.config.dhcp.mac_address"),
sortable: true,
filterable: true,
showNarrow: true,
main: true,
hideable: false,
moveable: false,
direction: "asc",
},
hostname: {
title: localize("ui.panel.config.dhcp.hostname"),
filterable: true,
sortable: true,
},
ip_address: {
title: localize("ui.panel.config.dhcp.ip_address"),
filterable: true,
sortable: true,
},
};
return columns;
}
);
private _dataWithIds = memoizeOne((data) =>
data.map((row) => ({
...row,
id: row.mac_address,
}))
);
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (this.hasUpdated) {
return;
}
const searchParams = extractSearchParamsObject();
const macAddress = searchParams.mac_address;
if (macAddress) {
this._macAddress = macAddress.toUpperCase();
}
}
protected render(): TemplateResult {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.columns=${this._columns(this.hass.localize)}
.data=${this._dataWithIds(this._data)}
filter=${this._macAddress || ""}
></hass-tabs-subpage-data-table>
`;
}
static styles: CSSResultGroup = haStyle;
}
declare global {
interface HTMLElementTagNameMap {
"dhcp-config-panel": DHCPConfigPanel;
}
}

View File

@ -110,6 +110,10 @@ export const getMyRedirects = (): Redirects => ({
component: "bluetooth", component: "bluetooth",
redirect: "/config/bluetooth", redirect: "/config/bluetooth",
}, },
config_dhcp: {
component: "dhcp",
redirect: "/config/dhcp",
},
config_energy: { config_energy: {
component: "energy", component: "energy",
redirect: "/config/energy/dashboard", redirect: "/config/energy/dashboard",

View File

@ -5482,6 +5482,12 @@
"service_uuids": "Service UUIDs", "service_uuids": "Service UUIDs",
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]" "copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]"
}, },
"dhcp": {
"title": "DHCP discovery",
"mac_address": "MAC Address",
"hostname": "Hostname",
"ip_address": "IP Address"
},
"thread": { "thread": {
"other_networks": "Other networks", "other_networks": "Other networks",
"my_network": "Preferred network", "my_network": "Preferred network",