diff --git a/src/data/dhcp.ts b/src/data/dhcp.ts new file mode 100644 index 0000000000..cd5d322cbc --- /dev/null +++ b/src/data/dhcp.ts @@ -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 +): Promise => + conn.subscribeMessage( + (event) => { + const data = [...(store.state || [])]; + if (event.add) { + for (const device_data of event.add) { + const index = data.findIndex( + (d) => d.mac_address === device_data.mac_address + ); + if (index === -1) { + data.push(device_data); + } else { + data[index] = device_data; + } + } + } + if (event.change) { + for (const device_data of event.change) { + const index = data.findIndex( + (d) => d.mac_address === device_data.mac_address + ); + if (index !== -1) { + data[index] = device_data; + } + } + } + if (event.remove) { + for (const device_data of event.remove) { + const index = data.findIndex( + (d) => d.mac_address === device_data.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( + "_dhcpDiscoveryRows", + () => Promise.resolve([]), // empty array as initial state + + subscribeDHCPDiscoveryUpdates, + conn, + callbackFunction + ); diff --git a/src/data/integration.ts b/src/data/integration.ts index 75cb334060..38370183d6 100644 --- a/src/data/integration.ts +++ b/src/data/integration.ts @@ -6,6 +6,7 @@ import { debounce } from "../common/util/debounce"; export const integrationsWithPanel = { bluetooth: "config/bluetooth", + dhcp: "config/dhcp", matter: "config/matter", mqtt: "config/mqtt", thread: "config/thread", diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index b40b01d63c..586755bdac 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -555,6 +555,11 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) { "./integrations/integration-panels/bluetooth/bluetooth-config-dashboard-router" ), }, + dhcp: { + tag: "dhcp-config-panel", + load: () => + import("./integrations/integration-panels/dhcp/dhcp-config-panel"), + }, application_credentials: { tag: "ha-config-application-credentials", load: () => diff --git a/src/panels/config/integrations/integration-panels/dhcp/dhcp-config-panel.ts b/src/panels/config/integrations/integration-panels/dhcp/dhcp-config-panel.ts new file mode 100644 index 0000000000..124028cc7e --- /dev/null +++ b/src/panels/config/integrations/integration-panels/dhcp/dhcp-config-panel.ts @@ -0,0 +1,105 @@ +import type { CSSResultGroup, TemplateResult } 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 "../../../../../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 { subscribeDHCPDiscovery } from "../../../../../data/dhcp"; + +@customElement("dhcp-config-panel") +export class DHCPConfigPanel extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public route!: Route; + + @property({ type: Boolean }) public narrow = false; + + @property({ attribute: "is-wide", type: Boolean }) public isWide = false; + + @state() private _data: DHCPDiscoveryData[] = []; + + private _unsub?: UnsubscribeFunc; + + public connectedCallback(): void { + super.connectedCallback(); + if (this.hass) { + this._unsub = subscribeDHCPDiscovery(this.hass.connection, (data) => { + this._data = data; + }); + } + } + + public disconnectedCallback() { + super.disconnectedCallback(); + if (this._unsub) { + this._unsub(); + this._unsub = undefined; + } + } + + private _columns = memoizeOne( + (localize: LocalizeFunc): DataTableColumnContainer => { + const columns: DataTableColumnContainer = { + mac_address: { + title: localize("ui.panel.config.dhcp.mac_address"), + sortable: true, + filterable: true, + showNarrow: true, + main: true, + hideable: false, + moveable: false, + direction: "asc", + flex: 2, + }, + 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.address, + })) + ); + + protected render(): TemplateResult { + return html` + + `; + } + + static styles: CSSResultGroup = haStyle; +} + +declare global { + interface HTMLElementTagNameMap { + "dhcp-config-panel": DHCPConfigPanel; + } +} diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index f8480455e4..8dbdd28859 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -110,6 +110,10 @@ export const getMyRedirects = (): Redirects => ({ component: "bluetooth", redirect: "/config/bluetooth", }, + config_dhcp: { + component: "dhcp", + redirect: "/config/dhcp", + }, config_energy: { component: "energy", redirect: "/config/energy/dashboard", diff --git a/src/translations/en.json b/src/translations/en.json index 80f1a0642b..387b38534d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5474,6 +5474,12 @@ "service_uuids": "Service UUIDs", "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": { "other_networks": "Other networks", "my_network": "Preferred network",