fic dialog deviceentities

This commit is contained in:
proddy
2023-04-29 14:34:56 +02:00
parent ff058b06a1
commit d06dc3e2cf
5 changed files with 183 additions and 107 deletions

View File

@@ -27,7 +27,7 @@ import { useRowSelect } from '@table-library/react-table-library/select';
import { useSort, SortToggleType } from '@table-library/react-table-library/sort';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useState, useContext, useCallback, useEffect } from 'react';
import { useState, useContext, useEffect } from 'react';
import { IconContext } from 'react-icons';
import { toast } from 'react-toastify';
@@ -35,8 +35,10 @@ import DashboarDevicesDialog from './DashboardDevicesDialog';
import DeviceIcon from './DeviceIcon';
import * as EMSESP from './api';
import { formatValue, isNumberUOM } from './deviceValue';
import { DeviceValueUOM, DeviceValueUOM_s, DeviceEntityMask } from './types';
import { DeviceValueUOM_s, DeviceEntityMask } from './types';
import { deviceValueItemValidation } from './validators';
import type { Device, CoreData, DeviceData, DeviceValue } from './types';
import type { FC } from 'react';
import { ButtonRow, SectionContent, MessageBox } from 'components';
@@ -174,19 +176,12 @@ const DashboardDevices: FC = () => {
},
sortToggleType: SortToggleType.AlternateWithReset,
sortFns: {
NAME: (array) => array.sort((a, b) => a.id.slice(2).localeCompare(b.id.slice(2))),
NAME: (array) => array.sort((a, b) => a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))),
VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString()))
}
}
);
const device_select = useRowSelect(
{ nodes: coreData.devices },
{
onChange: onSelectChange
}
);
const fetchDeviceData = async (id: number) => {
try {
setDeviceData((await EMSESP.readDeviceData({ id })).data);
@@ -195,13 +190,13 @@ const DashboardDevices: FC = () => {
}
};
const fetchCoreData = useCallback(async () => {
const fetchCoreData = async () => {
try {
setCoreData((await EMSESP.readCoreData()).data);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
};
useEffect(() => {
void fetchCoreData();
@@ -216,9 +211,18 @@ const DashboardDevices: FC = () => {
};
function onSelectChange(action: any, state: any) {
setSelectedDevice(device_select.state.id);
refreshData();
setSelectedDevice(state.id);
if (action.type === 'ADD_BY_ID_EXCLUSIVELY') {
void fetchDeviceData(state.id);
}
}
const device_select = useRowSelect(
{ nodes: coreData.devices },
{
onChange: onSelectChange
}
);
const escapeCsvCell = (cell: any) => {
if (cell == null) {
@@ -278,57 +282,6 @@ const DashboardDevices: FC = () => {
};
}, []);
const isCmdOnly = (dv: DeviceValue) => dv.v === '' && dv.c;
const formatDurationMin = (duration_min: number) => {
const days = Math.trunc((duration_min * 60000) / 86400000);
const hours = Math.trunc((duration_min * 60000) / 3600000) % 24;
const minutes = Math.trunc((duration_min * 60000) / 60000) % 60;
let formatted = '';
if (days) {
formatted += LL.NUM_DAYS({ num: days }) + ' ';
}
if (hours) {
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
}
if (minutes) {
formatted += LL.NUM_MINUTES({ num: minutes });
}
return formatted;
};
function formatValue(value: any, uom: number) {
if (value === undefined) {
return '';
}
switch (uom) {
case DeviceValueUOM.HOURS:
return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 });
case DeviceValueUOM.MINUTES:
return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 });
case DeviceValueUOM.SECONDS:
return LL.NUM_SECONDS({ num: value });
case DeviceValueUOM.NONE:
if (typeof value === 'number') {
return new Intl.NumberFormat().format(value);
}
return value;
case DeviceValueUOM.DEGREES:
case DeviceValueUOM.DEGREES_R:
case DeviceValueUOM.FAHRENHEIT:
return (
new Intl.NumberFormat(undefined, {
minimumFractionDigits: 1
}).format(value) +
' ' +
DeviceValueUOM_s[uom]
);
default:
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
}
}
const deviceValueDialogSave = async (dv: DeviceValue) => {
try {
const response = await EMSESP.writeDeviceValue({
@@ -515,11 +468,11 @@ const DashboardDevices: FC = () => {
{tableList.map((dv: DeviceValue) => (
<Row key={dv.id} item={dv} onClick={() => sendCommand(dv)}>
<Cell>{renderNameCell(dv)}</Cell>
<Cell>{formatValue(dv.v, dv.u)}</Cell>
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
<Cell stiff>
{dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<IconButton size="small" onClick={() => sendCommand(dv)}>
{isCmdOnly(dv) ? (
{dv.v === '' && dv.c ? (
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
) : (
<EditIcon color="primary" sx={{ fontSize: 16 }} />
@@ -542,7 +495,6 @@ const DashboardDevices: FC = () => {
{renderCoreData()}
{renderDeviceData()}
{renderDeviceDetails()}
{console.log('redndering device data')}
{selectedDeviceValue && (
<DashboarDevicesDialog
@@ -550,6 +502,7 @@ const DashboardDevices: FC = () => {
onClose={deviceValueDialogClose}
onSave={deviceValueDialogSave}
selectedItem={selectedDeviceValue}
validator={deviceValueItemValidation(isNumberUOM(selectedDeviceValue.u))}
/>
)}

View File

@@ -11,31 +11,48 @@ import {
MenuItem,
TextField,
FormHelperText,
Grid
Grid,
Box,
Typography
} from '@mui/material';
import { useState, useEffect } from 'react';
import { formatValueNoUOM } from './deviceValue';
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
import type { DeviceValue } from './types';
import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator';
import { ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { updateValue } from 'utils';
import { validate } from 'validators';
type DashboardDevicesDialogProps = {
open: boolean;
onClose: () => void;
onSave: (as: DeviceValue) => void;
selectedItem: DeviceValue;
validator: Schema;
};
const DashboarDevicesDialog = ({ open, onClose, onSave, selectedItem }: DashboardDevicesDialogProps) => {
const DashboarDevicesDialog = ({ open, onClose, onSave, selectedItem, validator }: DashboardDevicesDialogProps) => {
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValue(setEditItem);
useEffect(() => {
if (open) {
setFieldErrors(undefined);
setEditItem(selectedItem);
// format value and convert to string
setEditItem({
...selectedItem,
v: formatValueNoUOM(selectedItem.v, selectedItem.u)
});
}
}, [open, selectedItem]);
@@ -43,12 +60,16 @@ const DashboarDevicesDialog = ({ open, onClose, onSave, selectedItem }: Dashboar
onClose();
};
const save = () => {
const save = async () => {
try {
setFieldErrors(undefined);
await validate(validator, editItem);
onSave(editItem);
} catch (errors: any) {
setFieldErrors(errors);
}
};
const isCmdOnly = (dv: DeviceValue) => dv.v === '' && dv.c;
const setUom = (uom: number) => {
switch (uom) {
case DeviceValueUOM.HOURS:
@@ -64,16 +85,17 @@ const DashboarDevicesDialog = ({ open, onClose, onSave, selectedItem }: Dashboar
return (
<Dialog open={open} onClose={close}>
<DialogTitle>
<DialogTitle>{isCmdOnly(editItem) ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()}</DialogTitle>
</DialogTitle>
<DialogTitle>{selectedItem.v === '' && selectedItem.c ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()}</DialogTitle>
<DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
<Typography variant="body2">{editItem.id.slice(2)}</Typography>
</Box>
<Grid container spacing={1}>
<Grid item>
{editItem.l && (
<TextField
name="v"
label={editItem.id.slice(2)}
label={LL.VALUE(1)}
value={editItem.v}
autoFocus
sx={{ width: '30ch' }}
@@ -88,16 +110,15 @@ const DashboarDevicesDialog = ({ open, onClose, onSave, selectedItem }: Dashboar
</TextField>
)}
{!editItem.l && (
<TextField
<ValidatedTextField
fieldErrors={fieldErrors}
name="v"
label={editItem.id.slice(2)}
value={typeof editItem.v === 'number' ? Math.round(editItem.v * 10) / 10 : editItem.v}
label={LL.VALUE(1)}
value={editItem.v}
autoFocus
multiline={editItem.u ? false : true}
sx={{ width: '30ch' }}
type={editItem.u ? 'number' : 'text'}
multiline={editItem.u ? false : true}
onChange={updateFormValue}
inputProps={editItem.u ? { min: editItem.m, max: editItem.x, step: editItem.s } : {}}
InputProps={{
startAdornment: <InputAdornment position="start">{setUom(editItem.u)}</InputAdornment>
}}

View File

@@ -0,0 +1,85 @@
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
import type { TranslationFunctions } from 'i18n/i18n-types';
const formatDurationMin = (LL: TranslationFunctions, duration_min: number) => {
const days = Math.trunc((duration_min * 60000) / 86400000);
const hours = Math.trunc((duration_min * 60000) / 3600000) % 24;
const minutes = Math.trunc((duration_min * 60000) / 60000) % 60;
let formatted = '';
if (days) {
formatted += LL.NUM_DAYS({ num: days });
}
if (hours) {
if (formatted) formatted += ' ';
formatted += LL.NUM_HOURS({ num: hours });
}
if (minutes) {
if (formatted) formatted += ' ';
formatted += LL.NUM_MINUTES({ num: minutes });
}
return formatted;
};
export function formatValue(LL: TranslationFunctions, value: any, uom: number) {
if (value === undefined) {
return '';
}
switch (uom) {
case DeviceValueUOM.HOURS:
return value ? formatDurationMin(LL, value * 60) : LL.NUM_HOURS({ num: 0 });
case DeviceValueUOM.MINUTES:
return value ? formatDurationMin(LL, value) : LL.NUM_MINUTES({ num: 0 });
case DeviceValueUOM.SECONDS:
return LL.NUM_SECONDS({ num: value });
case DeviceValueUOM.NONE:
if (typeof value === 'number') {
return new Intl.NumberFormat().format(value);
}
return value;
case DeviceValueUOM.DEGREES:
case DeviceValueUOM.DEGREES_R:
case DeviceValueUOM.FAHRENHEIT:
return (
new Intl.NumberFormat(undefined, {
minimumFractionDigits: 1
}).format(value) +
' ' +
DeviceValueUOM_s[uom]
);
default:
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
}
}
export const formatValueNoUOM = (value: any, uom: number) => {
if (value === undefined) {
return '';
}
switch (uom) {
case DeviceValueUOM.NONE:
if (typeof value === 'number') {
return new Intl.NumberFormat().format(value);
}
return value;
case DeviceValueUOM.DEGREES:
case DeviceValueUOM.DEGREES_R:
case DeviceValueUOM.FAHRENHEIT:
return new Intl.NumberFormat(undefined, {
minimumFractionDigits: 1
}).format(Number(value));
default:
return value;
}
};
export function isNumberUOM(uom: number) {
if (uom === DeviceValueUOM.NONE) {
return false;
}
return true;
}

View File

@@ -161,3 +161,18 @@ export const analogSensorItemValidation = (sensors: AnalogSensor[], creating: bo
...(creating ? [isGPIOUniqueValidator(sensors)] : [])
]
});
export const deviceValueItemValidation = (isNumber: boolean) =>
new Schema({
v: [
{ required: true, message: 'Value is required' },
{
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
if (isNumber && isNaN(+value)) {
callback('Not a valid number');
}
callback();
}
}
]
});

View File

@@ -488,7 +488,8 @@ const emsesp_devicedata_1 = {
v: 'auto',
u: 0,
id: '00hc1 mode',
c: 'hc1/mode'
c: 'hc1/mode',
l: ['off', 'on', 'auto']
}
]
};
@@ -497,8 +498,8 @@ const emsesp_devicedata_2 = {
label: 'Boiler: Nefit GBx72/Trendline/Cerapur/Greenstar Si/27i',
data: [
{ v: '', u: 0, id: '08reset', c: 'reset', l: ['-', 'maintenance', 'error'] },
{ v: 'false', u: 0, id: '08heating active' },
{ v: 'false', u: 0, id: '04tapwater active' },
{ v: 'off', u: 0, id: '08heating active' },
{ v: 'off', u: 0, id: '04tapwater active' },
{ v: 5, u: 1, id: '04selected flow temperature', c: 'selflowtemp' },
{ v: 0, u: 3, id: '0Eburner selected max power', c: 'selburnpow' },
{ v: 0, u: 3, id: '00heating pump modulation' },
@@ -506,14 +507,14 @@ const emsesp_devicedata_2 = {
{ v: 52.7, u: 1, id: '00return temperature' },
{ v: 1.3, u: 10, id: '00system pressure' },
{ v: 54.9, u: 1, id: '00actual boiler temperature' },
{ v: 'false', u: 0, id: '00gas' },
{ v: 'false', u: 0, id: '00gas stage 2' },
{ v: 'off', u: 0, id: '00gas' },
{ v: 'off', u: 0, id: '00gas stage 2' },
{ v: 0, u: 9, id: '00flame current' },
{ v: 'false', u: 0, id: '00heating pump' },
{ v: 'false', u: 0, id: '00fan' },
{ v: 'false', u: 0, id: '00ignition' },
{ v: 'false', u: 0, id: '00oil preheating' },
{ v: 'true', u: 0, id: '00heating activated', c: 'heatingactivated', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00heating pump' },
{ v: 'off', u: 0, id: '00fan' },
{ v: 'off', u: 0, id: '00ignition' },
{ v: 'off', u: 0, id: '00oil preheating' },
{ v: 'on', u: 0, id: '00heating activated', c: 'heatingactivated', l: ['off', 'on'] },
{ v: 80, u: 1, id: '00heating temperature', c: 'heatingtemp' },
{ v: 70, u: 3, id: '00burner pump max power', c: 'pumpmodmax' },
{ v: 30, u: 3, id: '00burner pump min power', c: 'pumpmodmin' },
@@ -537,14 +538,14 @@ const emsesp_devicedata_2 = {
{ v: 'manual', u: 0, id: '00maintenance scheduled', c: 'maintenance', l: ['off', 'time', 'date', 'manual'] },
{ v: 6000, u: 7, id: '00time to next maintenance', c: 'maintenancetime' },
{ v: '01.01.2012', u: 0, id: '00next maintenance date', c: 'maintenancedate', o: 'Format: < dd.mm.yyyy >' },
{ v: 'true', u: 0, id: '00dhw turn on/off', c: 'wwtapactivated', l: ['off', 'on'] },
{ v: 'on', u: 0, id: '00dhw turn on/off', c: 'wwtapactivated', l: ['off', 'on'] },
{ v: 62, u: 1, id: '00dhw set temperature' },
{ v: 60, u: 1, id: '00dhw selected temperature', c: 'wwseltemp' },
{ v: 'flow', u: 0, id: '00dhw type' },
{ v: 'hot', u: 0, id: '00dhw comfort', c: 'wwcomfort', l: ['hot', 'eco', 'intelligent'] },
{ v: 40, u: 2, id: '00dhw flow temperature offset', c: 'wwflowtempoffset' },
{ v: 100, u: 3, id: '00dhw max power', c: 'wwmaxpower' },
{ v: 'false', u: 0, id: '00dhw circulation pump available', c: 'wwcircpump', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw circulation pump available', c: 'wwcircpump', l: ['off', 'on'] },
{ v: '3-way valve', u: 0, id: '00dhw charging type' },
{ v: -5, u: 2, id: '00dhw hysteresis on temperature', c: 'wwhyston' },
{ v: 0, u: 2, id: '00dhw hysteresis off temperature', c: 'wwhystoff' },
@@ -556,18 +557,18 @@ const emsesp_devicedata_2 = {
c: 'wwcircmode',
l: ['off', '1x3min', '2x3min', '3x3min', '4x3min', '5x3min', '6x3min', 'continuous']
},
{ v: 'false', u: 0, id: '00dhw circulation active', c: 'wwcirc', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw circulation active', c: 'wwcirc', l: ['off', 'on'] },
{ v: 47.3, u: 1, id: '00dhw current intern temperature' },
{ v: 0, u: 4, id: '00dhw current tap water flow' },
{ v: 47.3, u: 1, id: '00dhw storage intern temperature' },
{ v: 'true', u: 0, id: '00dhw activated', c: 'wwactivated', l: ['off', 'on'] },
{ v: 'false', u: 0, id: '00dhw one time charging', c: 'wwonetime', l: ['off', 'on'] },
{ v: 'false', u: 0, id: '00dhw disinfecting', c: 'wwdisinfecting', l: ['off', 'on'] },
{ v: 'false', u: 0, id: '00dhw charging' },
{ v: 'false', u: 0, id: '00dhw recharging' },
{ v: 'true', u: 0, id: '00dhw temperature ok' },
{ v: 'false', u: 0, id: '00dhw active' },
{ v: 'true', u: 0, id: '00dhw 3way valve active' },
{ v: 'on', u: 0, id: '00dhw activated', c: 'wwactivated', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw one time charging', c: 'wwonetime', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw disinfecting', c: 'wwdisinfecting', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw charging' },
{ v: 'off', u: 0, id: '00dhw recharging' },
{ v: 'on', u: 0, id: '00dhw temperature ok' },
{ v: 'off', u: 0, id: '00dhw active' },
{ v: 'on', u: 0, id: '00dhw 3way valve active' },
{ v: 0, u: 3, id: '00dhw set pump power' },
{ v: 288768, u: 0, id: '00dhw starts' },
{ v: 102151, u: 8, id: '00dhw active time' }
@@ -593,7 +594,8 @@ const emsesp_devicedata_4 = {
v: 'off',
u: 0,
id: '02hc2 mode',
c: 'hc2/mode'
c: 'hc2/mode',
l: ['off', 'on', 'auto']
}
]
};
@@ -963,7 +965,7 @@ rest_server.get(EMSESP_CORE_DATA_ENDPOINT, (req, res) => {
});
rest_server.get(EMSESP_SENSOR_DATA_ENDPOINT, (req, res) => {
console.log('send back sensor data...');
console.log(emsesp_sensordata);
// console.log(emsesp_sensordata);
res.json(emsesp_sensordata);
});
rest_server.get(EMSESP_DEVICES_ENDPOINT, (req, res) => {