translations and finish writeDevice

This commit is contained in:
proddy
2024-10-05 16:25:53 +02:00
parent 4fde9e7c54
commit e501ac31f5
33 changed files with 230 additions and 187 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -27,6 +27,7 @@ import {
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { useInterval } from 'utils';
import { readCustomEntities, writeCustomEntities } from '../../api/app';
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
@@ -52,17 +53,11 @@ const CustomEntities = () => {
initialData: []
});
useEffect(() => {
const timer = setInterval(async () => {
if (dialogOpen || numChanges > 0) {
return;
}
await fetchEntities();
}, 2000);
return () => {
clearInterval(timer);
};
}, []);
useInterval(() => {
if (!dialogOpen && !numChanges) {
void fetchEntities();
}
}, 3000);
const { send: writeEntities } = useRequest(
(data: Entities) => writeCustomEntities(data),
@@ -295,7 +290,7 @@ const CustomEntities = () => {
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
<Typography variant="body1">{LL.ENTITIES_HELP_1()}</Typography>
</Box>
{renderEntity()}

View File

@@ -427,7 +427,7 @@ const Customizations = () => {
const renderDeviceList = () => (
<>
<Box mb={1} color="warning.main">
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
<Typography variant="body1">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
</Box>
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
{rename ? (

View File

@@ -22,23 +22,29 @@ import { useRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import { useInterval } from 'utils';
import { readDashboard, writeDeviceValue } from '../../api/app';
import DeviceIcon from './DeviceIcon';
import DashboardDevicesDialog from './DevicesDialog';
import { formatValue } from './deviceValue';
import { type DashboardItem, type DeviceValue } from './types';
import { type DashboardItem, DeviceEntityMask, type DeviceValue } from './types';
import { deviceValueItemValidation } from './validators';
const Dashboard = () => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
useLayoutTitle('Dashboard'); // TODO translate
useLayoutTitle(LL.DASHBOARD());
const [firstLoad, setFirstLoad] = useState<boolean>(true);
const [showAll, setShowAll] = useState<boolean>(true);
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState<boolean>(false);
const [selectedDashboardItem, setSelectedDashboardItem] =
useState<DashboardItem>();
const {
data,
send: fetchDashboard,
@@ -55,17 +61,11 @@ const Dashboard = () => {
}
);
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false);
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
const deviceValueDialogClose = () => {
setDeviceValueDialogOpen(false);
void sendDeviceData(selectedDevice);
};
// TODO get this working next
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
const id = Number(device_select.state.id);
if (!selectedDashboardItem) {
return;
}
const id = selectedDashboardItem.parentNode.id; // this is the parent ID
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
.then(() => {
toast.success(LL.WRITE_CMD_SENT());
@@ -73,10 +73,9 @@ const Dashboard = () => {
.catch((error: Error) => {
toast.error(error.message);
})
.finally(async () => {
.finally(() => {
setDeviceValueDialogOpen(false);
await sendDeviceData(id);
setSelectedDeviceValue(undefined);
setSelectedDashboardItem(undefined);
});
};
@@ -105,7 +104,7 @@ const Dashboard = () => {
const tree = useTree(
{ nodes: data },
{
onChange: null
onChange: null // not used but needed
},
{
treeIcon: {
@@ -118,25 +117,19 @@ const Dashboard = () => {
}
);
useEffect(() => {
const timer = setInterval(() => {
if (deviceValueDialogOpen) {
return;
}
fetchDashboard();
}, 2000);
return () => {
clearInterval(timer);
};
}, []);
useInterval(() => {
if (!deviceValueDialogOpen) {
void fetchDashboard();
}
}, 3000);
// auto expand on first load
useEffect(() => {
if (firstLoad && data.length && !tree.state.ids.length) {
if (firstLoad && Array.isArray(data) && data.length && !tree.state.ids.length) {
tree.fns.onToggleAll({});
setFirstLoad(false);
}
}, [data]);
}, [loading]);
const showName = (di: DashboardItem) => {
if (di.id < 100) {
@@ -145,7 +138,7 @@ const Dashboard = () => {
return (
<>
<span style="font-size: 14px">
<DeviceIcon type_id={di.t} />
<DeviceIcon type_id={di.t ?? 0} />
&nbsp;&nbsp;{di.n}
</span>
<span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span>
@@ -153,20 +146,14 @@ const Dashboard = () => {
);
}
}
return <span style="color:lightgrey">{di.n}</span>;
return <span style="color:lightgrey">{di.dv ? di.dv.id.slice(2) : ''}</span>;
};
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);
const hasMask = (id: string, mask: number) =>
(parseInt(id.slice(0, 2), 16) & mask) === mask;
const editDashboardValue = (di: DashboardItem) => {
setSelectedDashboardItem(di);
setDeviceValueDialogOpen(true);
};
@@ -185,21 +172,10 @@ const Dashboard = () => {
return <FormLoader onRetry={fetchDashboard} errorMessage={error?.message} />;
}
// if (data.length === 0) {
// return (
// <Typography variant="body2" color="warning">
// {/* TODO translate */}
// No entities found.
// </Typography>
// );
// }
return (
<>
<Typography mb={2} variant="body2" color="warning">
{/* TODO translate */}
The dashboard shows all EMS entities that are marked as favorite, and the
sensors.
<Typography mb={2} variant="body1" color="warning">
{LL.DASHBOARD_1()}
</Typography>
<ToggleButtonGroup
@@ -235,9 +211,8 @@ const Dashboard = () => {
}}
>
{!loading && data.length === 0 ? (
<Typography variant="body2" color="warning">
{/* TODO translate */}
No entities found.
<Typography variant="subtitle2" color="warning">
{LL.NO_DATA()}
</Typography>
) : (
<Table
@@ -257,19 +232,21 @@ const Dashboard = () => {
)}
<Cell pinRight>
<span style={{ color: 'lightgrey' }}>
{formatValue(LL, di.v, di.u)}
{di.dv && formatValue(LL, di.dv.v, di.dv.u)}
</span>
</Cell>
<Cell stiff>
{me.admin && di.c && (
<IconButton
size="small"
onClick={() => showDeviceValue(di)}
>
<EditIcon color="primary" sx={{ fontSize: 16 }} />
</IconButton>
)}
{me.admin &&
di.dv?.c &&
!hasMask(di.dv.id, DeviceEntityMask.DV_READONLY) && (
<IconButton
size="small"
onClick={() => editDashboardValue(di)}
>
<EditIcon color="primary" sx={{ fontSize: 16 }} />
</IconButton>
)}
</Cell>
</Row>
))}
@@ -286,14 +263,14 @@ const Dashboard = () => {
return (
<SectionContent>
{renderContent()}
{selectedDeviceValue && (
{selectedDashboardItem && selectedDashboardItem.dv && (
<DashboardDevicesDialog
open={deviceValueDialogOpen}
onClose={deviceValueDialogClose}
onClose={() => setDeviceValueDialogOpen(false)}
onSave={deviceValueDialogSave}
selectedItem={selectedDeviceValue}
selectedItem={selectedDashboardItem.dv}
writeable={true}
validator={deviceValueItemValidation(selectedDeviceValue)}
validator={deviceValueItemValidation(selectedDashboardItem.dv)}
progress={submitting}
/>
)}

View File

@@ -60,6 +60,7 @@ import { useRequest } from 'alova/client';
import { MessageBox, SectionContent, useLayoutTitle } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import { useInterval } from 'utils';
import { readCoreData, readDeviceData, writeDeviceValue } from '../../api/app';
import DeviceIcon from './DeviceIcon';
@@ -77,7 +78,7 @@ const Devices = () => {
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
const [onlyFav, setOnlyFav] = useState(false);
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false);
const [showDeviceInfo, setShowDeviceInfo] = useState<boolean>(false);
const [showDeviceInfo, setShowDeviceInfo] = useState(false);
const [selectedDevice, setSelectedDevice] = useState<number>();
const navigate = useNavigate();
@@ -418,17 +419,11 @@ const Devices = () => {
downloadBlob(new Blob([csvData], { type: 'text/csv;charset:utf-8' }));
};
useEffect(() => {
const timer = setInterval(() => {
if (deviceValueDialogOpen) {
return;
}
useInterval(() => {
if (!deviceValueDialogOpen) {
selectedDevice ? void sendDeviceData(selectedDevice) : void sendCoreData();
}, 2000);
return () => {
clearInterval(timer);
};
}, []);
}
}, 3000);
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
const id = Number(device_select.state.id);

View File

@@ -24,7 +24,7 @@ import { useRequest } from 'alova/client';
import { SectionContent, useLayoutTitle } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import { saveFile } from 'utils/file';
import { saveFile } from 'utils';
import { API, callAction } from '../../api/app';
import type { APIcall } from './types';
@@ -147,7 +147,7 @@ const Help = () => {
)}
<Box p={2} color="warning.main">
<Typography mb={1} variant="body2">
<Typography mb={1} variant="body1">
{LL.HELP_INFORMATION_4()}
</Typography>
<Button

View File

@@ -175,7 +175,7 @@ const Modules = () => {
return (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.MODULES_DESCRIPTION()}</Typography>
<Typography variant="body1">{LL.MODULES_DESCRIPTION()}</Typography>
</Box>
<Table
data={{ nodes: modules }}

View File

@@ -318,7 +318,7 @@ const Scheduler = () => {
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
<Typography variant="body1">{LL.SCHEDULER_HELP_1()}</Typography>
</Box>
{renderSchedule()}

View File

@@ -68,7 +68,7 @@ const Sensors = () => {
analog_enabled: false,
platform: 'ESP32'
},
pollingTime: 2000
pollingTime: 3000
}
);

View File

@@ -116,14 +116,10 @@ export interface CoreData {
export interface DashboardItem {
id: number; // unique index
n: string; // name
v?: unknown; // value, optional
u: number; // uom
t: number; // type from DeviceType
c?: string; // command, optional
l?: string[]; // list, optional
h?: string; // help text, optional
nodes?: DashboardItem[]; // nodes, optional
t?: number; // type from DeviceType
n?: string; // name
dv?: DeviceValue;
nodes?: DashboardItem[]; // children nodes, optional
}
export interface DashboardData {

View File

@@ -32,7 +32,7 @@ import {
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { saveFile } from 'utils/file';
import { saveFile } from 'utils';
const DownloadUpload = () => {
const { LL } = useI18nContext();
@@ -221,7 +221,7 @@ const DownloadUpload = () => {
{LL.DOWNLOAD(0)}
</Typography>
<Typography mb={1} variant="body2" color="warning">
<Typography mb={1} variant="body1" color="warning">
{LL.DOWNLOAD_SETTINGS_TEXT()}
</Typography>
<Grid container spacing={1}>
@@ -269,7 +269,7 @@ const DownloadUpload = () => {
</Typography>
<Box color="warning.main" sx={{ pb: 2 }}>
<Typography variant="body2">{LL.UPLOAD_TEXT()}</Typography>
<Typography variant="body1">{LL.UPLOAD_TEXT()}</Typography>
</Box>
<SingleUpload doRestart={doRestart} />

View File

@@ -38,7 +38,7 @@ const APStatus = () => {
data,
send: loadData,
error
} = useAutoRequest(APApi.readAPStatus, { pollingTime: 5000 });
} = useAutoRequest(APApi.readAPStatus, { pollingTime: 3000 });
const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF(LL.ACCESS_POINT(0)));

View File

@@ -21,7 +21,7 @@ const SystemActivity = () => {
data,
send: loadData,
error
} = useAutoRequest(readActivity, { pollingTime: 2000 });
} = useAutoRequest(readActivity, { pollingTime: 3000 });
const { LL } = useI18nContext();

View File

@@ -36,7 +36,7 @@ const HardwareStatus = () => {
data,
send: loadData,
error
} = useAutoRequest(SystemApi.readSystemStatus, { pollingTime: 2000 });
} = useAutoRequest(SystemApi.readSystemStatus, { pollingTime: 3000 });
const content = () => {
if (!data) {

View File

@@ -58,7 +58,7 @@ const MqttStatus = () => {
data,
send: loadData,
error
} = useAutoRequest(MqttApi.readMqttStatus, { pollingTime: 5000 });
} = useAutoRequest(MqttApi.readMqttStatus, { pollingTime: 3000 });
const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF('MQTT'));

View File

@@ -40,7 +40,7 @@ const NTPStatus = () => {
data,
send: loadData,
error
} = useAutoRequest(NTPApi.readNTPStatus, { pollingTime: 5000 });
} = useAutoRequest(NTPApi.readNTPStatus, { pollingTime: 3000 });
const [localTime, setLocalTime] = useState<string>('');
const [settingTime, setSettingTime] = useState<boolean>(false);

View File

@@ -85,7 +85,7 @@ const NetworkStatus = () => {
data,
send: loadData,
error
} = useAutoRequest(NetworkApi.readNetworkStatus, { pollingTime: 5000 });
} = useAutoRequest(NetworkApi.readNetworkStatus, { pollingTime: 3000 });
const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF(LL.NETWORK(1)));

View File

@@ -28,7 +28,7 @@ const RestartMonitor = () => {
initialData: { status: 'Getting ready...' },
async middleware(_, next) {
if (count++ >= 1) {
// skip first request (1 seconds) to allow AsyncWS to send its response
// skip first request (1 second) to allow AsyncWS to send its response
await next();
}
}

View File

@@ -65,7 +65,7 @@ const SystemStatus = () => {
error
} = useAutoRequest(readSystemStatus, {
initialData: [],
pollingTime: 5000,
pollingTime: 3000,
async middleware(_, next) {
if (!restarting) {
await next();

View File

@@ -340,7 +340,10 @@ const de: Translation = {
PLEASE_WAIT: 'Bitte warten',
RESTARTING_PRE: 'Initialisierung',
RESTARTING_POST: 'Vorbereitung',
AUTO_SCROLL: 'Automatisches Scrollen'
AUTO_SCROLL: 'Automatisches Scrollen',
DASHBOARD: 'Dashboard', // TODO translate
NO_DATA: 'No data available', // TODO translate
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorites using the Customizations module.' // TODO translate
};
export default de;

View File

@@ -340,7 +340,10 @@ const en: Translation = {
PLEASE_WAIT: 'Please wait',
RESTARTING_PRE: 'Initializing',
RESTARTING_POST: 'Preparing',
AUTO_SCROLL: 'Auto Scroll'
AUTO_SCROLL: 'Auto Scroll',
DASHBOARD: 'Dashboard',
NO_DATA: 'No data available',
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorites using the Customizations module.',
};
export default en;

View File

@@ -340,7 +340,10 @@ const fr: Translation = {
PLEASE_WAIT: 'Please wait', // TODO translate
RESTARTING_PRE: 'Initializing', // TODO translate
RESTARTING_POST: 'Preparing', // TODO translate
AUTO_SCROLL: 'Auto Scroll' // TODO translate
AUTO_SCROLL: 'Auto Scroll', // TODO translate
DASHBOARD: 'Dashboard', // TODO translate
NO_DATA: 'No data available', // TODO translate
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorites using the Customizations module.' // TODO translate
};
export default fr;

View File

@@ -340,7 +340,10 @@ const it: Translation = {
PLEASE_WAIT: 'Please wait', // TODO translate
RESTARTING_PRE: 'Initializing', // TODO translate
RESTARTING_POST: 'Preparing', // TODO translate
AUTO_SCROLL: 'Auto Scroll' // TODO translate
AUTO_SCROLL: 'Auto Scroll', // TODO translate
DASHBOARD: 'Dashboard', // TODO translate
NO_DATA: 'No data available', // TODO translate
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorites using the Customizations module.' // TODO translate
};
export default it;

View File

@@ -340,7 +340,10 @@ const nl: Translation = {
PLEASE_WAIT: 'Please wait', // TODO translate
RESTARTING_PRE: 'Initializing', // TODO translate
RESTARTING_POST: 'Preparing', // TODO translate
AUTO_SCROLL: 'Auto Scroll' // TODO translate
AUTO_SCROLL: 'Auto Scroll', // TODO translate
DASHBOARD: 'Dashboard', // TODO translate
NO_DATA: 'No data available', // TODO translate
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorites using the Customizations module.' // TODO translate
};
export default nl;

View File

@@ -340,7 +340,10 @@ const no: Translation = {
PLEASE_WAIT: 'Please wait', // TODO translate
RESTARTING_PRE: 'Initializing', // TODO translate
RESTARTING_POST: 'Preparing', // TODO translate
AUTO_SCROLL: 'Auto Scroll' // TODO translate
AUTO_SCROLL: 'Auto Scroll', // TODO translate
DASHBOARD: 'Dashboard', // TODO translate
NO_DATA: 'No data available', // TODO translate
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorites using the Customizations module.' // TODO translate
};
export default no;

View File

@@ -340,7 +340,10 @@ const pl: BaseTranslation = {
PLEASE_WAIT: 'Please wait', // TODO translate
RESTARTING_PRE: 'Initializing', // TODO translate
RESTARTING_POST: 'Preparing', // TODO translate
AUTO_SCROLL: 'Auto Scroll' // TODO translate
AUTO_SCROLL: 'Auto Scroll', // TODO translate
DASHBOARD: 'Dashboard', // TODO translate
NO_DATA: 'No data available', // TODO translate
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorites using the Customizations module.' // TODO translate
};
export default pl;

View File

@@ -340,7 +340,10 @@ const sk: Translation = {
PLEASE_WAIT: 'Čakajte prosím',
RESTARTING_PRE: 'Prebieha inicializácia',
RESTARTING_POST: 'Príprava',
AUTO_SCROLL: 'Auto Scroll' // TODO translate
AUTO_SCROLL: 'Auto Scroll', // TODO translate
DASHBOARD: 'Dashboard', // TODO translate
NO_DATA: 'No data available', // TODO translate
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorites using the Customizations module.' // TODO translate
};
export default sk;

View File

@@ -340,7 +340,10 @@ const sv: Translation = {
PLEASE_WAIT: 'Please wait', // TODO translate
RESTARTING_PRE: 'Initializing', // TODO translate
RESTARTING_POST: 'Preparing', // TODO translate
AUTO_SCROLL: 'Auto Scroll' // TODO translate
AUTO_SCROLL: 'Auto Scroll', // TODO translate
DASHBOARD: 'Dashboard', // TODO translate
NO_DATA: 'No data available', // TODO translate
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorites using the Customizations module.' // TODO translate
};
export default sv;

View File

@@ -340,7 +340,10 @@ const tr: Translation = {
PLEASE_WAIT: 'Please wait', // TODO translate
RESTARTING_PRE: 'Initializing', // TODO translate
RESTARTING_POST: 'Preparing', // TODO translate
AUTO_SCROLL: 'Auto Scroll' // TODO translate
AUTO_SCROLL: 'Auto Scroll', // TODO translate
DASHBOARD: 'Dashboard', // TODO translate
NO_DATA: 'No data available', // TODO translate
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorites using the Customizations module.' // TODO translate
};
export default tr;

View File

@@ -3,4 +3,6 @@ export * from './route';
export * from './submit';
export * from './time';
export * from './useRest';
export * from './useInterval';
export * from './props';
export * from './file';

View File

@@ -0,0 +1,22 @@
import { useEffect, useRef } from 'react';
// adapted from https://www.joshwcomeau.com/snippets/react-hooks/use-interval/
export const useInterval = (callback: () => void, delay: number) => {
const intervalRef = useRef<number | null>(null);
const savedCallback = useRef<() => void>(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => savedCallback.current();
if (typeof delay === 'number') {
intervalRef.current = window.setInterval(tick, delay);
return () => {
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
}
};
}
}, [delay]);
return intervalRef;
};