From fd7d8ca532fe996fbbd6910c0208296ba1e1d2d7 Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 4 Oct 2024 13:32:42 +0200 Subject: [PATCH] enable write in dashboard --- interface/package.json | 2 +- interface/src/app/main/Dashboard.tsx | 176 +++++++++++++----- interface/src/app/main/DeviceIcon.tsx | 71 +++---- interface/src/app/main/Devices.tsx | 104 +++++------ interface/src/app/main/types.ts | 8 +- .../src/components/layout/LayoutMenu.tsx | 4 +- interface/yarn.lock | 10 +- mock-api/rest_server.ts | 76 ++++++-- 8 files changed, 286 insertions(+), 165 deletions(-) diff --git a/interface/package.json b/interface/package.json index 2b2351c04..f6a92f559 100644 --- a/interface/package.json +++ b/interface/package.json @@ -31,7 +31,7 @@ "async-validator": "^4.2.5", "jwt-decode": "^4.0.0", "mime-types": "^2.1.35", - "preact": "^10.24.1", + "preact": "^10.24.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.3.0", diff --git a/interface/src/app/main/Dashboard.tsx b/interface/src/app/main/Dashboard.tsx index a66acced7..0c6b61b84 100644 --- a/interface/src/app/main/Dashboard.tsx +++ b/interface/src/app/main/Dashboard.tsx @@ -1,24 +1,38 @@ -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; +import { IconContext } from 'react-icons/lib'; +import { toast } from 'react-toastify'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import EditIcon from '@mui/icons-material/Edit'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import UnfoldLessIcon from '@mui/icons-material/UnfoldLess'; import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; -import { Box, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; +import { + Box, + IconButton, + ToggleButton, + ToggleButtonGroup, + Typography +} from '@mui/material'; import { Body, Cell, Row, Table } from '@table-library/react-table-library/table'; import { useTheme } from '@table-library/react-table-library/theme'; import { CellTree, useTree } from '@table-library/react-table-library/tree'; -import { useAutoRequest } from 'alova/client'; +import { useAutoRequest, useRequest } from 'alova/client'; import { FormLoader, SectionContent, useLayoutTitle } from 'components'; +import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; -import { readDashboard } from '../../api/app'; +import { readDashboard, writeDeviceValue } from '../../api/app'; +import DeviceIcon from './DeviceIcon'; +import DashboardDevicesDialog from './DevicesDialog'; import { formatValue } from './deviceValue'; -import type { DashboardItem } from './types'; +import { type DashboardItem, type DeviceValue } from './types'; +import { deviceValueItemValidation } from './validators'; const Dashboard = () => { const { LL } = useI18nContext(); + const { me } = useContext(AuthenticatedContext); useLayoutTitle('Dashboard'); // TODO translate @@ -34,9 +48,39 @@ const Dashboard = () => { pollingTime: 1500 }); + const { loading: submitting, send: sendDeviceValue } = useRequest( + (data: { id: number; c: string; v: unknown }) => writeDeviceValue(data), + { + immediate: false + } + ); + + const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false); + const [selectedDeviceValue, setSelectedDeviceValue] = useState(); + + const deviceValueDialogClose = () => { + setDeviceValueDialogOpen(false); + void sendDeviceData(selectedDevice); + }; + const deviceValueDialogSave = async (devicevalue: DeviceValue) => { + const id = Number(device_select.state.id); + await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v }) + .then(() => { + toast.success(LL.WRITE_CMD_SENT()); + }) + .catch((error: Error) => { + toast.error(error.message); + }) + .finally(async () => { + setDeviceValueDialogOpen(false); + await sendDeviceData(id); + setSelectedDeviceValue(undefined); + }); + }; + const dashboard_theme = useTheme({ Table: ` - --data-table-library_grid-template-columns: minmax(80px, auto) 120px; + --data-table-library_grid-template-columns: minmax(80px, auto) 120px 40px; `, BaseRow: ` font-size: 14px; @@ -46,10 +90,8 @@ const Dashboard = () => { `, Row: ` background-color: #1e1e1e; - // position: relative; - // cursor: pointer; .td { - height: 24px; + height: 22px; } &:hover .td { border-top: 1px solid #177ac9; @@ -58,15 +100,8 @@ const Dashboard = () => { ` }); - function onTreeChange(action, state) { - // do nothing for now - } - const tree = useTree( { nodes: data }, - { - onChange: onTreeChange - }, { treeIcon: { margin: '4px', @@ -84,22 +119,38 @@ const Dashboard = () => { tree.fns.onToggleAll({}); setFirstLoad(false); } - }, [data]); + }); const showName = (di: DashboardItem) => { if (di.id < 100) { + // if its a device row if (di.nodes?.length) { return ( - - {di.n} + <> + + +   {di.n} +  ({di.nodes?.length}) - + ); } - return
{di.n}
; } + return
{di.n}
; + }; - return
{di.n}
; + const showDeviceValue = (di: DashboardItem) => { + // convert di to dv + // TODO should we not just use dv? + const dv: DeviceValue = { + id: ' ' + di.n, + v: di.v, + u: di.u, + c: di.c, + l: di.l + }; + setSelectedDeviceValue(dv); + setDeviceValueDialogOpen(true); }; const handleShowAll = ( @@ -129,6 +180,7 @@ const Dashboard = () => { return ( <> + {/* TODO translate */} The dashboard shows all EMS entities that are marked as favorite, and the sensors. @@ -158,33 +210,71 @@ const Dashboard = () => { border: '1px solid grey' }} > - - {(tableList: DashboardItem[]) => ( - - {tableList.map((di: DashboardItem) => ( - - {di.nodes?.length === 0 ? ( - {showName(di)} - ) : ( - {showName(di)} - )} - {formatValue(LL, di.v, di.u)} - - ))} - - )} -
+ + {(tableList: DashboardItem[]) => ( + + {tableList.map((di: DashboardItem) => ( + + {di.nodes?.length === 0 ? ( + {showName(di)} + ) : ( + {showName(di)} + )} + +
+ {formatValue(LL, di.v, di.u)} +
+
+ + + {me.admin && di.c && ( + showDeviceValue(di)} + > + + + )} + +
+ ))} + + )} +
+ ); }; - return {renderContent()}; + return ( + + {renderContent()} + {selectedDeviceValue && ( + + )} + + ); }; export default Dashboard; diff --git a/interface/src/app/main/DeviceIcon.tsx b/interface/src/app/main/DeviceIcon.tsx index a9429b67c..b498f6316 100644 --- a/interface/src/app/main/DeviceIcon.tsx +++ b/interface/src/app/main/DeviceIcon.tsx @@ -2,6 +2,7 @@ import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ import { CgSmartHomeBoiler } from 'react-icons/cg'; import { FaSolarPanel } from 'react-icons/fa'; import { GiHeatHaze, GiTap } from 'react-icons/gi'; +import { MdPlaylistAdd } from 'react-icons/md'; import { MdOutlineDevices, MdOutlinePool, @@ -11,50 +12,40 @@ import { import { TiFlowSwitch } from 'react-icons/ti'; import { VscVmConnect } from 'react-icons/vsc'; -import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; +import type { SvgIconProps } from '@mui/material'; import { DeviceType } from './types'; +const deviceIconLookup: { + [key in DeviceType]: React.ComponentType | undefined; +} = { + [DeviceType.TEMPERATURESENSOR]: MdOutlineSensors, + [DeviceType.ANALOGSENSOR]: MdOutlineSensors, + [DeviceType.BOILER]: CgSmartHomeBoiler, + [DeviceType.HEATSOURCE]: CgSmartHomeBoiler, + [DeviceType.THERMOSTAT]: MdThermostatAuto, + [DeviceType.MIXER]: AiOutlineControl, + [DeviceType.SOLAR]: FaSolarPanel, + [DeviceType.HEATPUMP]: GiHeatHaze, + [DeviceType.GATEWAY]: AiOutlineGateway, + [DeviceType.SWITCH]: TiFlowSwitch, + [DeviceType.CONTROLLER]: VscVmConnect, + [DeviceType.CONNECT]: VscVmConnect, + [DeviceType.ALERT]: AiOutlineAlert, + [DeviceType.EXTENSION]: MdOutlineDevices, + [DeviceType.WATER]: GiTap, + [DeviceType.POOL]: MdOutlinePool, + [DeviceType.CUSTOM]: MdPlaylistAdd, + [DeviceType.UNKNOWN]: undefined, + [DeviceType.SYSTEM]: undefined, + [DeviceType.SCHEDULER]: undefined, + [DeviceType.GENERIC]: undefined, + [DeviceType.VENTILATION]: undefined +}; + const DeviceIcon = ({ type_id }: { type_id: DeviceType }) => { - switch (type_id) { - case DeviceType.TEMPERATURESENSOR: - case DeviceType.ANALOGSENSOR: - return ; - case DeviceType.BOILER: - case DeviceType.HEATSOURCE: - return ; - case DeviceType.THERMOSTAT: - return ; - case DeviceType.MIXER: - return ; - case DeviceType.SOLAR: - return ; - case DeviceType.HEATPUMP: - return ; - case DeviceType.GATEWAY: - return ; - case DeviceType.SWITCH: - return ; - case DeviceType.CONTROLLER: - case DeviceType.CONNECT: - return ; - case DeviceType.ALERT: - return ; - case DeviceType.EXTENSION: - return ; - case DeviceType.WATER: - return ; - case DeviceType.POOL: - return ; - case DeviceType.CUSTOM: - return ( - - ); - default: - return null; - } + const Icon = deviceIconLookup[type_id]; + return Icon ? : null; }; export default DeviceIcon; diff --git a/interface/src/app/main/Devices.tsx b/interface/src/app/main/Devices.tsx index fe306dbda..457286824 100644 --- a/interface/src/app/main/Devices.tsx +++ b/interface/src/app/main/Devices.tsx @@ -170,7 +170,7 @@ const Devices = () => { common_theme, { Table: ` - --data-table-library_grid-template-columns: 40px repeat(1, minmax(0, 1fr)) 130px; + --data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px; `, BaseRow: ` .td { @@ -527,57 +527,57 @@ const Devices = () => { }; const renderCoreData = () => ( - - {!coreData.connected && ( - - )} + <> + + {!coreData.connected && ( + + )} - {coreData.connected && ( - - {(tableList: Device[]) => ( - <> -
- - - {LL.DESCRIPTION()} - {LL.TYPE(0)} - -
- - {tableList.length === 0 && ( - - )} - {tableList.map((device: Device) => ( - - - - - - {device.n} - -   ({device.e}) - - - {device.tn} - - ))} - - - )} -
- )} -
+ {coreData.connected && ( + + {(tableList: Device[]) => ( + <> +
+ + {LL.DESCRIPTION()} + {LL.TYPE(0)} + +
+ + {tableList.length === 0 && ( + + )} + {tableList.map((device: Device) => ( + + + +    + {device.n} + +   ({device.e}) + + + {device.tn} + + ))} + + + )} +
+ )} +
+ ); const deviceValueDialogClose = () => { @@ -733,7 +733,7 @@ const Devices = () => { size="small" onClick={() => showDeviceValue(dv)} > - {dv.v === '' && dv.c ? ( + {dv.v === '' ? ( ) : ( diff --git a/interface/src/app/main/types.ts b/interface/src/app/main/types.ts index 30efb7e59..8b88b6392 100644 --- a/interface/src/app/main/types.ts +++ b/interface/src/app/main/types.ts @@ -117,9 +117,13 @@ export interface CoreData { export interface DashboardItem { id: number; // unique index n: string; // name - v?: unknown; // value + v?: unknown; // value, optional u: number; // uom - nodes?: DashboardItem[]; // nodes + t: number; // type from DeviceType + c?: string; // command, optional + l?: string[]; // list, optional + h?: string; // help text, optional + nodes?: DashboardItem[]; // nodes, optional } export interface DashboardData { diff --git a/interface/src/components/layout/LayoutMenu.tsx b/interface/src/components/layout/LayoutMenu.tsx index 1817abc2e..d0e016491 100644 --- a/interface/src/components/layout/LayoutMenu.tsx +++ b/interface/src/components/layout/LayoutMenu.tsx @@ -144,8 +144,8 @@ const LayoutMenu = () => { - - + + diff --git a/interface/yarn.lock b/interface/yarn.lock index a2062e721..643fa47d6 100644 --- a/interface/yarn.lock +++ b/interface/yarn.lock @@ -1864,7 +1864,7 @@ __metadata: formidable: "npm:^3.5.1" jwt-decode: "npm:^4.0.0" mime-types: "npm:^2.1.35" - preact: "npm:^10.24.1" + preact: "npm:^10.24.2" prettier: "npm:^3.3.3" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" @@ -5793,10 +5793,10 @@ __metadata: languageName: node linkType: hard -"preact@npm:^10.24.1": - version: 10.24.1 - resolution: "preact@npm:10.24.1" - checksum: 10c0/f9bc8b2f88d340f1b8f854208889244059c46916449b8f8f2174fcacbc0904c445c5870896fb0cfeaf442eeade975857e8e03f0785135c41d63cd32d9414c9c6 +"preact@npm:^10.24.2": + version: 10.24.2 + resolution: "preact@npm:10.24.2" + checksum: 10c0/d1d22c5e1abc10eb8f83501857ef22c54a3fda2d20449d06f5b3c7d5ae812bd702c16c05b672138b8906504f9c893e072e9cebcbcada8cac320edf36265788fb languageName: node linkType: hard diff --git a/mock-api/rest_server.ts b/mock-api/rest_server.ts index 9837ed0b8..ccd725973 100644 --- a/mock-api/rest_server.ts +++ b/mock-api/rest_server.ts @@ -32,6 +32,32 @@ const headers = { let countWifiScanPoll = 0; // wifi network scan let countHardwarePoll = 0; // for during an upload +// DeviceTypes +const enum DeviceType { + SYSTEM = 0, + TEMPERATURESENSOR, + ANALOGSENSOR, + SCHEDULER, + CUSTOM, + BOILER, + THERMOSTAT, + MIXER, + SOLAR, + HEATPUMP, + GATEWAY, + SWITCH, + CONTROLLER, + CONNECT, + ALERT, + EXTENSION, + GENERIC, + HEATSOURCE, + VENTILATION, + WATER, + POOL, + UNKNOWN +} + function updateMask(entity: any, de: any, dd: any) { const current_mask = parseInt(entity.slice(0, 2), 16); @@ -1708,7 +1734,7 @@ const emsesp_devicedata_3 = { { v: 'hot', u: 0, - id: '00dhw comfort', + id: '08dhw comfort', c: 'dhw/comfort', l: ['hot', 'eco', 'intelligent'] }, @@ -4268,11 +4294,11 @@ function getDashboardEntityData(id: number) { else if (id == 10) device_data = emsesp_devicedata_10; else if (id == 99) device_data = emsesp_devicedata_99; - // filter device_data, just want id, v, u + // filter device_data // and only favorite items (bit 8 set), only for non-Custom Entities // and replace id by striping off the 2-char mask let new_data = (device_data as any).data - .map(({ id, c, m, x, s, h, l, ...rest }) => ({ + .map(({ id, m, x, s, ...rest }) => ({ ...rest, id2: id })) @@ -4281,9 +4307,14 @@ function getDashboardEntityData(id: number) { id: id * 100 + index, // unique id n: item.id2.slice(2), // name v: item.v, // value - u: item.u // uom + u: item.u, // uom + c: item.c, // command + l: item.l, // list + h: item.h // help })); + // TODO only and command if not marked as READONLY + return new_data; } @@ -4325,10 +4356,12 @@ router params.id ? deviceEntities(Number(params.id)) : status(404) ) .get(EMSESP_DASHBOARD_DATA_ENDPOINT, () => { - let dashboard_data = []; - let dashboard_object = {}; + let dashboard_data: { id?: number; n?: string; t?: number; nodes?: any[] }[] = + []; + let dashboard_object: { id?: number; n?: string; t?: number; nodes?: any[] } = + {}; let fake = false; - // let fake = true; + // let fake = true; // fakes no data if (!fake) { // pick EMS devices from coredata @@ -4338,11 +4371,12 @@ router dashboard_object = { id: id, n: element.n, + t: element.t, nodes: getDashboardEntityData(id) }; - // only add to dashboard if we have values - if (dashboard_object.nodes.length > 0) { + // only add to dashboard if we have values + if ((dashboard_object.nodes ?? []).length > 0) { dashboard_data.push(dashboard_object); } } @@ -4351,15 +4385,16 @@ router dashboard_object = { id: 99, n: 'Custom Entities', + t: 4, // DeviceType::CUSTOM nodes: getDashboardEntityData(99) }; - // only add to dashboard if we have values - if (dashboard_object.nodes.length > 0) { + // only add to dashboard if we have values + if ((dashboard_object.nodes ?? []).length > 0) { dashboard_data.push(dashboard_object); } - // add temperature sensor data - let sensor_data = {}; + // add temperature sensor data. no command c + let sensor_data: any[] = []; sensor_data = emsesp_sensordata.ts.map((item, index) => ({ id: 980 + index, n: item.n ? item.n : item.id, // name may not be set @@ -4369,17 +4404,17 @@ router dashboard_object = { id: 98, n: 'Temperature Sensors', + t: 1, // DeviceType::TEMPERATURESENSOR nodes: sensor_data }; - // only add to dashboard if we have values - if (dashboard_object.nodes.length > 0) { + // only add to dashboard if we have values + if ((dashboard_object.nodes ?? []).length > 0) { dashboard_data.push(dashboard_object); } - // add analog sensor data - // remove disabled sensors (t = 0) + // add analog sensor data. no command c + // remove disabled sensors first (t = 0) sensor_data = emsesp_sensordata.as.filter((item) => item.t !== 0); - sensor_data = sensor_data.map((item, index) => ({ id: 970 + index, n: item.n, @@ -4390,10 +4425,11 @@ router dashboard_object = { id: 97, n: 'Analog Sensors', + t: 2, // DeviceType::ANALOGSENSOR nodes: sensor_data }; - // only add to dashboard if we have values - if (dashboard_object.nodes.length > 0) { + // only add to dashboard if we have values + if ((dashboard_object.nodes ?? []).length > 0) { dashboard_data.push(dashboard_object); } }