Skip to content

Commit

Permalink
feat: Allow custom per device MQTT subscription
Browse files Browse the repository at this point in the history
  • Loading branch information
mman authored Dec 16, 2024
1 parent 02a6d83 commit 1b06965
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 21 deletions.
1 change: 0 additions & 1 deletion src/client/views/settings/AutoExpiryOptionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export interface AutoExpiryOptionListProps {
portalId: string
configuredExpiryTime?: number
defaultExpiryDuration?: number
value?: number
onSelectionDidChange: (_event: React.ChangeEvent<HTMLSelectElement>, _index: number, _portalId: string) => void
}

Expand Down
22 changes: 21 additions & 1 deletion src/client/views/settings/DeviceList.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import React from "react"
import { CFormCheck, CTable, CTableHead, CTableBody, CTableHeaderCell, CTableDataCell, CTableRow } from "@coreui/react"
import { AppDataCollectionExpiryConfig, AppUPNPConfig, AppVRMConfig } from "../../../shared/types"
import AppDeviceSubscriptionsConfig, {
AppDataCollectionExpiryConfig,
AppUPNPConfig,
AppVRMConfig,
} from "../../../shared/types"
import { DiscoveredDevice } from "../../../shared/state"
import { AutoExpiryOptionList } from "./AutoExpiryOptionList"
import { MQTTSubscriptionsOptionList } from "./MQTTSubscriptionsOptionList"

interface DeviceListProps {
hidden?: boolean
settings: AppUPNPConfig | AppVRMConfig
referenceTime: number
expirySettings: AppDataCollectionExpiryConfig
mqttSubscriptionsSettings: AppDeviceSubscriptionsConfig
onEnablePortalChange: React.ChangeEventHandler<HTMLInputElement>
onEnableAllPortalsChange: React.ChangeEventHandler<HTMLInputElement>
availablePortalIds: DiscoveredDevice[]
defaultExpiryDuration?: number
onPortalMQTTSubscriptionsChange: (
_event: React.ChangeEvent<HTMLSelectElement>,
_index: number,
_portalId: string,
) => void
onPortalExpiryChange: (_event: React.ChangeEvent<HTMLSelectElement>, _index: number, _portalId: string) => void
}

Expand All @@ -23,6 +34,7 @@ export function DeviceList(props: DeviceListProps) {
<CTableRow>
<CTableHeaderCell>Installation Name</CTableHeaderCell>
<CTableHeaderCell>Portal ID</CTableHeaderCell>
<CTableHeaderCell>Subscription</CTableHeaderCell>
{props.defaultExpiryDuration && <CTableHeaderCell>Auto Expire Data Collection</CTableHeaderCell>}
<CTableHeaderCell>
<CFormCheck
Expand All @@ -45,6 +57,14 @@ export function DeviceList(props: DeviceListProps) {
<CTableRow key={element.portalId}>
<CTableDataCell>{element.name}</CTableDataCell>
<CTableDataCell>{element.portalId}</CTableDataCell>
<CTableDataCell>
<MQTTSubscriptionsOptionList
index={index}
portalId={element.portalId}
configuredMQTTSubscriptions={props.mqttSubscriptionsSettings[element.portalId]}
onSelectionDidChange={props.onPortalMQTTSubscriptionsChange}
/>
</CTableDataCell>
{props.defaultExpiryDuration && (
<CTableDataCell>
<AutoExpiryOptionList
Expand Down
20 changes: 19 additions & 1 deletion src/client/views/settings/Discovery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useGetConfig, usePutConfig } from "../../hooks/useAdminApi"
import { useFormValidation, extractParameterNameAndValue } from "../../hooks/useFormValidation"
import { DeviceList } from "./DeviceList"
import { useEffect, useState } from "react"
import { AppConfig } from "../../../shared/types"
import { AppConfig, VenusMQTTTopic } from "../../../shared/types"
import { AppState } from "../../store"
import { WebSocketStatus } from "./WebsocketStatus"

Expand Down Expand Up @@ -97,6 +97,22 @@ function Discovery() {
setIsTemporaryConfigDirty(true)
}

function handlePortalSubscriptionChange(
event: React.ChangeEvent<HTMLSelectElement>,
_index: number,
portalId: string,
) {
const clone = { ...temporaryConfig!! }
const value = String(event.target.value) as VenusMQTTTopic
if (value) {
clone.upnp.subscriptions[portalId] = [value]
} else {
delete clone.upnp.subscriptions[portalId]
}
setTemporaryConfig(clone)
setIsTemporaryConfigDirty(true)
}

function handlePortalExpiryChange(event: React.ChangeEvent<HTMLSelectElement>, _index: number, portalId: string) {
const clone = { ...temporaryConfig!! }
const value = Number(event.target.value)
Expand Down Expand Up @@ -134,11 +150,13 @@ function Discovery() {
settings={temporaryConfig.upnp}
referenceTime={referenceTime}
expirySettings={temporaryConfig.upnp.expiry}
mqttSubscriptionsSettings={temporaryConfig.upnp.subscriptions}
availablePortalIds={upnpDiscovered}
onEnablePortalChange={handleEnablePortalChange}
onEnableAllPortalsChange={handleEnableAllPortalsChange}
defaultExpiryDuration={defaultExpiryDuration}
onPortalExpiryChange={handlePortalExpiryChange}
onPortalMQTTSubscriptionsChange={handlePortalSubscriptionChange}
/>
</CForm>
</CCardBody>
Expand Down
44 changes: 43 additions & 1 deletion src/client/views/settings/EditableDeviceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,21 @@ import {
CFormInput,
CButton,
} from "@coreui/react"
import { AppDataCollectionExpiryConfig, AppDeviceConfig, AppInstallationConfig } from "../../../shared/types"
import AppDeviceSubscriptionsConfig, {
AppDataCollectionExpiryConfig,
AppDeviceConfig,
AppInstallationConfig,
} from "../../../shared/types"
import { AutoExpiryOptionList } from "./AutoExpiryOptionList"
import { DiscoveredDevice } from "../../../shared/state"
import { MQTTSubscriptionsOptionList } from "./MQTTSubscriptionsOptionList"

interface EditableDeviceListProps {
hidden?: boolean
entries: AppDeviceConfig[] | AppInstallationConfig[]
referenceTime: number
expirySettings: (number | undefined)[]
mqttSubscriptionsSettings: AppDeviceSubscriptionsConfig
onEntryValueChange: (_event: React.ChangeEvent<HTMLInputElement>, _index: number) => void
onEnableEntryChange: (_event: React.ChangeEvent<HTMLInputElement>, _index: number) => void
onEnableAllEntriesChange: (_event: React.ChangeEvent<HTMLInputElement>) => void
Expand All @@ -27,6 +33,11 @@ interface EditableDeviceListProps {
entryTitleText: string
addEntryButtonText: string
defaultExpiryDuration?: number
onPortalMQTTSubscriptionsChange: (
_event: React.ChangeEvent<HTMLSelectElement>,
_index: number,
_portalId: string,
) => void
onPortalExpiryChange: (_event: React.ChangeEvent<HTMLSelectElement>, _index: number, _portalId: string) => void
}

Expand All @@ -37,6 +48,7 @@ export function EditableDeviceList(props: EditableDeviceListProps) {
<CTableHead>
<CTableRow>
<CTableHeaderCell>{props.entryTitleText}</CTableHeaderCell>
<CTableHeaderCell>Subscription</CTableHeaderCell>
{props.defaultExpiryDuration && <CTableHeaderCell>Auto Expire Data Collection</CTableHeaderCell>}
<CTableHeaderCell>
<CFormCheck
Expand Down Expand Up @@ -69,6 +81,14 @@ export function EditableDeviceList(props: EditableDeviceListProps) {
onChange={(event) => props.onEntryValueChange(event, index)}
/>
</CTableDataCell>
<CTableDataCell>
<MQTTSubscriptionsOptionList
index={index}
portalId={key}
configuredMQTTSubscriptions={props.mqttSubscriptionsSettings[index]}
onSelectionDidChange={props.onPortalMQTTSubscriptionsChange}
/>
</CTableDataCell>
{props.defaultExpiryDuration && (
<CTableDataCell>
<AutoExpiryOptionList
Expand Down Expand Up @@ -125,3 +145,25 @@ export function keyedExpiryToArray(
// @ts-expect-error
return devices.map((device) => expiry[device.hostName ?? device.portalId])
}

export function arraySubscriptionsToKeyed(
subscriptions: AppDeviceSubscriptionsConfig,
devices: AppDeviceConfig[] | AppInstallationConfig[],
existingSubscriptions: AppDeviceSubscriptionsConfig = {},
discoveredDevices: DiscoveredDevice[] = [],
): AppDeviceSubscriptionsConfig {
const a = Object.fromEntries(
discoveredDevices.map((device) => [device.portalId, existingSubscriptions[device.portalId]]),
)
// @ts-expect-error
const b = Object.fromEntries(devices.map((device, i) => [device.hostName ?? device.portalId, subscriptions[i]]))
return { ...a, ...b }
}

export function keyedSubscriptionsToArray(
subscriptions: AppDeviceSubscriptionsConfig,
devices: AppDeviceConfig[] | AppInstallationConfig[],
): AppDeviceSubscriptionsConfig {
// @ts-expect-error
return devices.map((device) => subscriptions[device.hostName ?? device.portalId])
}
46 changes: 46 additions & 0 deletions src/client/views/settings/MQTTSubscriptionsOptionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from "react"
import { CFormSelect } from "@coreui/react"
import { useEffect, useState } from "react"
import { VenusMQTTTopic, VenusMQTTTopics } from "../../../shared/types"

export interface MQTTSubscriptionsOptionListProps {
index: number
portalId: string
configuredMQTTSubscriptions: VenusMQTTTopic[]
onSelectionDidChange: (_event: React.ChangeEvent<HTMLSelectElement>, _index: number, _portalId: string) => void
}

interface MQTTSubscriptionsListOption {
label: string
value: string
}

interface MQTTSubscriptionsOptionListOptions {
default: string
options: MQTTSubscriptionsListOption[]
}

function generateOptions(configuredMQTTSubscriptions: VenusMQTTTopic[]): MQTTSubscriptionsOptionListOptions {
let defaultValue = `/#`
if (configuredMQTTSubscriptions && configuredMQTTSubscriptions.length > 0) {
defaultValue = configuredMQTTSubscriptions[0]
}
const options = VenusMQTTTopics.map((x) => {
return { label: x, value: x }
})
return { default: defaultValue, options: options }
}

export function MQTTSubscriptionsOptionList(props: MQTTSubscriptionsOptionListProps) {
const [options, setOptions] = useState<MQTTSubscriptionsOptionListOptions>({ default: "", options: [] })
useEffect(() => {
setOptions(generateOptions(props.configuredMQTTSubscriptions))
}, [props.configuredMQTTSubscriptions])
return (
<CFormSelect
options={options.options}
value={options.default}
onChange={(event) => props.onSelectionDidChange(event, props.index, props.portalId)}
/>
)
}
29 changes: 27 additions & 2 deletions src/client/views/settings/Manual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import { CCard, CCardBody, CCardHeader, CCardFooter, CForm, CButton, CFormCheck

import { useGetConfig, usePutConfig } from "../../hooks/useAdminApi"
import { useFormValidation, extractParameterNameAndValue } from "../../hooks/useFormValidation"
import { arrayExpiryToKeyed, EditableDeviceList, keyedExpiryToArray } from "./EditableDeviceList"
import {
arrayExpiryToKeyed,
arraySubscriptionsToKeyed,
EditableDeviceList,
keyedExpiryToArray,
keyedSubscriptionsToArray,
} from "./EditableDeviceList"
import { useEffect, useState } from "react"
import { AppConfig } from "../../../shared/types"
import AppDeviceSubscriptionsConfig, { AppConfig, VenusMQTTTopic } from "../../../shared/types"
import { WebSocketStatus } from "./WebsocketStatus"
import { useSelector } from "react-redux"
import { AppState } from "../../store"
Expand All @@ -29,12 +35,14 @@ function Manual() {

const [referenceTime, setReferenceTime] = useState<number>(0)
const [temporaryExpiry, setTemporaryExpiry] = useState<(number | undefined)[]>([])
const [temporarySubscriptions, setTemporarySubscriptions] = useState<AppDeviceSubscriptionsConfig>({})
const [temporaryConfig, setTemporaryConfig] = useState<AppConfig>()
useEffect(() => {
setReferenceTime(Date.now())
populateDefaultExpiry(config)
setTemporaryConfig(config)
setTemporaryExpiry(keyedExpiryToArray(config?.manual.expiry ?? {}, config?.manual.hosts ?? []))
setTemporarySubscriptions(keyedSubscriptionsToArray(config?.manual.subscriptions ?? {}, config?.manual.hosts ?? []))
setIsTemporaryConfigDirty(false)
}, [config])

Expand Down Expand Up @@ -111,6 +119,21 @@ function Manual() {
setIsTemporaryConfigDirty(true)
}

function handlePortalSubscriptionChange(
event: React.ChangeEvent<HTMLSelectElement>,
index: number,
_portalId: string,
) {
const clone = { ...temporaryConfig!! }
const value = String(event.target.value) as VenusMQTTTopic
const newSubscriptions = { ...temporarySubscriptions!! }
newSubscriptions[index] = [value]
clone.manual.subscriptions = arraySubscriptionsToKeyed(newSubscriptions, clone.manual.hosts)
setTemporarySubscriptions(newSubscriptions)
setTemporaryConfig(clone)
setIsTemporaryConfigDirty(true)
}

function handlePortalExpiryChange(event: React.ChangeEvent<HTMLSelectElement>, index: number, _portalId: string) {
const clone = { ...temporaryConfig!! }
const value = Number(event.target.value)
Expand Down Expand Up @@ -147,6 +170,7 @@ function Manual() {
entries={temporaryConfig.manual.hosts}
referenceTime={referenceTime}
expirySettings={temporaryExpiry}
mqttSubscriptionsSettings={temporarySubscriptions}
onEntryValueChange={handleHostNameChange}
onEnableEntryChange={handleEnableHostChange}
onEnableAllEntriesChange={handleEnableAllHostsChange}
Expand All @@ -156,6 +180,7 @@ function Manual() {
addEntryButtonText="Add Host"
defaultExpiryDuration={defaultExpiryDuration}
onPortalExpiryChange={handlePortalExpiryChange}
onPortalMQTTSubscriptionsChange={handlePortalSubscriptionChange}
/>
</CForm>
</CCardBody>
Expand Down
Loading

0 comments on commit 1b06965

Please sign in to comment.