Merge branch 'dev_' into dev_no_master_thermostat

This commit is contained in:
MichaelDvP
2022-04-14 07:27:07 +02:00
55 changed files with 3607 additions and 2542 deletions

View File

@@ -183,45 +183,49 @@ const MqttSettingsForm: FC = () => {
control={<Checkbox name="send_response" checked={data.send_response} onChange={updateFormValue} />}
label="Publish command output to a 'response' topic"
/>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item>
<BlockFormControlLabel
control={<Checkbox name="publish_single" checked={data.publish_single} onChange={updateFormValue} />}
label="Publish single value topics on change"
/>
</Grid>
{data.publish_single && (
{!data.ha_enabled && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item>
<BlockFormControlLabel
control={
<Checkbox name="publish_single2cmd" checked={data.publish_single2cmd} onChange={updateFormValue} />
}
label="Publish to command topics (ioBroker)"
control={<Checkbox name="publish_single" checked={data.publish_single} onChange={updateFormValue} />}
label="Publish single value topics on change"
/>
</Grid>
)}
</Grid>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item>
<BlockFormControlLabel
control={<Checkbox name="ha_enabled" checked={data.ha_enabled} onChange={updateFormValue} />}
label="Enable MQTT Discovery (Home Assistant, Domoticz)"
/>
{data.publish_single && (
<Grid item>
<BlockFormControlLabel
control={
<Checkbox name="publish_single2cmd" checked={data.publish_single2cmd} onChange={updateFormValue} />
}
label="Publish to command topics (ioBroker)"
/>
</Grid>
)}
</Grid>
{data.ha_enabled && (
<Grid item xs={6}>
<ValidatedTextField
name="discovery_prefix"
label="Prefix for the Discovery topics"
fullWidth
variant="outlined"
value={data.discovery_prefix}
onChange={updateFormValue}
margin="normal"
)}
{!data.publish_single && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item>
<BlockFormControlLabel
control={<Checkbox name="ha_enabled" checked={data.ha_enabled} onChange={updateFormValue} />}
label="Enable MQTT Discovery (Home Assistant, Domoticz)"
/>
</Grid>
)}
</Grid>
{data.ha_enabled && (
<Grid item xs={6}>
<ValidatedTextField
name="discovery_prefix"
label="Prefix for the Discovery topics"
fullWidth
variant="outlined"
value={data.discovery_prefix}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
)}
</Grid>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Publish Intervals (in seconds, 0=automatic)
</Typography>

View File

@@ -32,11 +32,14 @@ import { extractErrorMessage, formatDateTime, formatLocalDateTime, useRest } fro
import { AuthenticatedContext } from '../../contexts/authentication';
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
switch (status) {
case NTPSyncStatus.NTP_INACTIVE:
case NTPSyncStatus.NTP_DISABLED:
return theme.palette.info.main;
case NTPSyncStatus.NTP_INACTIVE:
return theme.palette.error.main;
case NTPSyncStatus.NTP_ACTIVE:
return theme.palette.success.main;
default:
@@ -46,6 +49,8 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
export const ntpStatus = ({ status }: NTPStatus) => {
switch (status) {
case NTPSyncStatus.NTP_DISABLED:
return 'Disabled';
case NTPSyncStatus.NTP_INACTIVE:
return 'Inactive';
case NTPSyncStatus.NTP_ACTIVE:
@@ -143,7 +148,7 @@ const NTPStatusForm: FC = () => {
<ListItemText primary="Status" secondary={ntpStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
{isNtpActive(data) && (
{isNtpEnabled(data) && (
<>
<ListItem>
<ListItemAvatar>

View File

@@ -46,7 +46,7 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
};
return (
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open={!!user} fullWidth maxWidth="sm">
<Dialog onClose={onCancelEditing} open={!!user} fullWidth maxWidth="sm">
{user && (
<>
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>

View File

@@ -13,13 +13,14 @@ const FirmwareFileUpload: FC<UploadFirmwareProps> = ({ uploadFirmware }) => {
return (
<>
<MessageBox
message="Upload a new firmware (.bin) file below to replace the existing firmware"
level="warning"
my={2}
/>
{!uploading && (
<MessageBox
message="Upload a new firmware (.bin) file below to replace the existing firmware"
level="warning"
my={2}
/>
)}
<SingleUpload
// accept="application/octet-stream"
accept=".bin"
onDrop={uploadFile}
onCancel={cancelUpload}

View File

@@ -37,6 +37,10 @@ import CancelIcon from '@mui/icons-material/Cancel';
import SendIcon from '@mui/icons-material/TrendingFlat';
import SaveIcon from '@mui/icons-material/Save';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import DeviceIcon from './DeviceIcon';
@@ -62,7 +66,8 @@ import {
AnalogType,
AnalogTypeNames,
Sensor,
Analog
Analog,
DeviceEntityMask
} from './types';
const StyledTableCell = styled(TableCell)(({ theme }) => ({
@@ -153,6 +158,8 @@ const DashboardData: FC = () => {
}
};
const isCmdOnly = (dv: DeviceValue) => dv.v === undefined && dv.c;
function formatValue(value: any, uom: number) {
if (value === undefined) {
return '';
@@ -213,12 +220,12 @@ const DashboardData: FC = () => {
if (deviceValue) {
return (
<Dialog open={deviceValue !== undefined} onClose={() => setDeviceValue(undefined)}>
<DialogTitle>Change Value</DialogTitle>
<DialogTitle>{isCmdOnly(deviceValue) ? 'Run Command' : 'Change Value'}</DialogTitle>
<DialogContent dividers>
{deviceValue.l && (
<ValidatedTextField
name="v"
label={deviceValue.n}
label={deviceValue.n.slice(2)}
value={deviceValue.v}
autoFocus
sx={{ width: '30ch' }}
@@ -233,13 +240,13 @@ const DashboardData: FC = () => {
{!deviceValue.l && (
<ValidatedTextField
name="v"
label={deviceValue.n}
label={deviceValue.n.slice(2)}
value={deviceValue.u ? numberValue(deviceValue.v) : deviceValue.v}
autoFocus
sx={{ width: '30ch' }}
type={deviceValue.u ? 'number' : 'text'}
onChange={updateValue(setDeviceValue)}
inputProps={{ step: deviceValue.s }}
inputProps={deviceValue.u ? { min: deviceValue.m, max: deviceValue.x, step: deviceValue.s } : {}}
InputProps={{
startAdornment: <InputAdornment position="start">{DeviceValueUOM_s[deviceValue.u]}</InputAdornment>
}}
@@ -485,26 +492,24 @@ const DashboardData: FC = () => {
return;
}
const hasMask = (entityName: string, mask: number) => (parseInt(entityName.slice(0, 2), 16) & mask) === mask;
const sendCommand = (dv: DeviceValue) => {
if (dv.c && me.admin) {
if (dv.c && me.admin && !hasMask(dv.n, DeviceEntityMask.DV_READONLY)) {
setDeviceValue(dv);
}
};
const renderNameCell = (dv: DeviceValue) => {
if (dv.v === undefined && dv.c) {
return (
<StyledTableCell component="th" scope="row" sx={{ color: 'yellow' }}>
command:&nbsp;{dv.n}
</StyledTableCell>
);
}
return (
<StyledTableCell component="th" scope="row">
{dv.n}
</StyledTableCell>
);
};
const renderNameCell = (dv: DeviceValue) => (
<>
{dv.n.slice(2)}&nbsp;
{hasMask(dv.n, DeviceEntityMask.DV_FAVORITE) && <FavoriteBorderIcon color="success" sx={{ fontSize: 12 }} />}
{hasMask(dv.n, DeviceEntityMask.DV_READONLY) && <EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />}
{hasMask(dv.n, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
</>
);
return (
<>
@@ -515,7 +520,7 @@ const DashboardData: FC = () => {
<TableHead>
<TableRow>
<StyledTableCell padding="checkbox" style={{ width: 18 }}></StyledTableCell>
<StyledTableCell align="left">ENTITY NAME/COMMAND</StyledTableCell>
<StyledTableCell align="left">ENTITY NAME</StyledTableCell>
<StyledTableCell align="right">VALUE</StyledTableCell>
</TableRow>
</TableHead>
@@ -523,14 +528,18 @@ const DashboardData: FC = () => {
{deviceData.data.map((dv, i) => (
<StyledTableRow key={i} onClick={() => sendCommand(dv)}>
<StyledTableCell padding="checkbox">
{dv.c && me.admin && (
<IconButton size="small" aria-label="Edit">
{dv.c && me.admin && !hasMask(dv.n, DeviceEntityMask.DV_READONLY) && (
<IconButton size="small">
<EditIcon color="primary" fontSize="small" />
</IconButton>
)}
</StyledTableCell>
{renderNameCell(dv)}
<StyledTableCell align="right">{formatValue(dv.v, dv.u)}</StyledTableCell>
<StyledTableCell component="th" scope="row">
{renderNameCell(dv)}
</StyledTableCell>
<StyledTableCell align="right">
{isCmdOnly(dv) ? <PlayArrowIcon color="primary" sx={{ fontSize: 14 }} /> : formatValue(dv.v, dv.u)}
</StyledTableCell>
</StyledTableRow>
))}
</TableBody>
@@ -569,7 +578,7 @@ const DashboardData: FC = () => {
<StyledTableRow key={sensor_data.n} onClick={() => updateSensor(sensor_data)}>
<StyledTableCell padding="checkbox">
{me.admin && (
<IconButton edge="start" size="small" aria-label="Edit">
<IconButton edge="start" size="small">
<EditIcon color="primary" fontSize="small" />
</IconButton>
)}
@@ -605,7 +614,7 @@ const DashboardData: FC = () => {
<StyledTableRow key={analog_data.i} onClick={() => updateAnalog(analog_data)}>
<StyledTableCell padding="checkbox">
{me.admin && (
<IconButton edge="start" size="small" aria-label="Edit">
<IconButton edge="start" size="small">
<EditIcon color="primary" fontSize="small" />
</IconButton>
)}

View File

@@ -12,7 +12,9 @@ import {
Dialog,
DialogActions,
DialogContent,
DialogTitle
DialogTitle,
ToggleButton,
ToggleButtonGroup
} from '@mui/material';
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
@@ -22,8 +24,11 @@ import { styled } from '@mui/material/styles';
import { useSnackbar } from 'notistack';
import SaveIcon from '@mui/icons-material/Save';
import CloseIcon from '@mui/icons-material/Close';
import CancelIcon from '@mui/icons-material/Cancel';
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
import FavoriteBorderOutlinedIcon from '@mui/icons-material/FavoriteBorderOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import { ButtonRow, FormLoader, ValidatedTextField, SectionContent } from '../components';
@@ -36,12 +41,7 @@ import { DeviceShort, Devices, DeviceEntity } from './types';
const StyledTableCell = styled(TableCell)(({ theme }) => ({
[`&.${tableCellClasses.head}`]: {
backgroundColor: '#607d8b',
color: theme.palette.common.white,
fontSize: 11
},
[`&.${tableCellClasses.body}`]: {
fontSize: 11
backgroundColor: '#607d8b'
}
}));
@@ -54,6 +54,9 @@ const SettingsCustomization: FC = () => {
const [selectedDevice, setSelectedDevice] = useState<number>(0);
const [confirmReset, setConfirmReset] = useState<boolean>(false);
// eslint-disable-next-line
const [masks, setMasks] = useState(() => ['']);
const fetchDevices = useCallback(async () => {
try {
setDevices((await EMSESP.readDevices()).data);
@@ -62,9 +65,14 @@ const SettingsCustomization: FC = () => {
}
}, []);
const setInitialMask = (data: DeviceEntity[]) => {
setDeviceEntities(data.map((de) => ({ ...de, om: de.m })));
};
const fetchDeviceEntities = async (unique_id: number) => {
try {
setDeviceEntities((await EMSESP.readDeviceEntities({ id: unique_id })).data);
const data = (await EMSESP.readDeviceEntities({ id: unique_id })).data;
setInitialMask(data);
} catch (error: any) {
setErrorMessage(extractErrorMessage(error, 'Problem fetching device entities'));
}
@@ -109,8 +117,22 @@ const SettingsCustomization: FC = () => {
return (
<>
<Box color="warning.main">
<Typography variant="body2">
Customize which entities to exclude from all all services (MQTT, API). This will have immediate effect.
<Typography variant="body2">Select a device and customize each of its entities using the options:</Typography>
<Typography mt={1} ml={2} display="block" variant="body2" sx={{ alignItems: 'center', display: 'flex' }}>
<FavoriteBorderOutlinedIcon color="success" sx={{ fontSize: 13 }} />
&nbsp;mark it as favorite to be listed at the top of the Dashboard
</Typography>
<Typography ml={2} display="block" variant="body2" sx={{ alignItems: 'center', display: 'flex' }}>
<EditOffOutlinedIcon color="secondary" sx={{ fontSize: 13 }} />
&nbsp;make it read-only, only if it has write operation available
</Typography>
<Typography ml={2} display="block" variant="body2" sx={{ alignItems: 'center', display: 'flex' }}>
<CommentsDisabledOutlinedIcon color="secondary" sx={{ fontSize: 13 }} />
&nbsp;excluded it from MQTT and API outputs
</Typography>
<Typography ml={2} mb={1} display="block" variant="body2" sx={{ alignItems: 'center', display: 'flex' }}>
<VisibilityOffOutlinedIcon color="secondary" sx={{ fontSize: 13 }} />
&nbsp;hide it from the Dashboard
</Typography>
</Box>
<ValidatedTextField
@@ -138,11 +160,22 @@ const SettingsCustomization: FC = () => {
const saveCustomization = async () => {
if (deviceEntities && selectedDevice) {
const exclude_entities = deviceEntities.filter((de) => de.x).map((new_de) => new_de.i);
const masked_entities = deviceEntities
.filter((de) => de.m !== de.om)
.map((new_de) => new_de.m.toString(16).padStart(2, '0') + new_de.s);
if (masked_entities.length > 50) {
enqueueSnackbar(
'Too many selected entities (' + masked_entities.length + '). Limit is 50. Please Save in batches',
{ variant: 'warning' }
);
return;
}
try {
const response = await EMSESP.writeExcludeEntities({
const response = await EMSESP.writeMaskedEntities({
id: selectedDevice,
entity_ids: exclude_entities
entity_ids: masked_entities
});
if (response.status === 200) {
enqueueSnackbar('Customization saved', { variant: 'success' });
@@ -152,6 +185,7 @@ const SettingsCustomization: FC = () => {
} catch (error: any) {
enqueueSnackbar(extractErrorMessage(error, 'Problem sending entity list'), { variant: 'error' });
}
setInitialMask(deviceEntities);
}
};
@@ -160,48 +194,76 @@ const SettingsCustomization: FC = () => {
return;
}
const toggleDeviceEntity = (id: number) => {
setDeviceEntities(
deviceEntities.map((o) => {
if (o.i === id) {
return { ...o, x: !o.x };
}
return o;
})
);
const setMask = (de: DeviceEntity, newMask: string[]) => {
var new_mask = 0;
for (let entry of newMask) {
new_mask |= Number(entry);
}
de.m = new_mask;
setMasks(newMask);
};
const getMask = (de: DeviceEntity) => {
var new_masks = [];
if ((de.m & 1) === 1 || de.n === '') {
new_masks.push('1');
}
if ((de.m & 2) === 2) {
new_masks.push('2');
}
if ((de.m & 4) === 4 && de.w) {
new_masks.push('4');
}
if ((de.m & 8) === 8) {
new_masks.push('8');
}
return new_masks;
};
return (
<>
<Table size="small">
<TableHead>
<TableRow>
<StyledTableCell>
({deviceEntities.reduce((a, v) => (v.x ? a + 1 : a), 0)}/{deviceEntities.length})
<Table size="small" padding="normal">
<TableHead>
<TableRow>
<StyledTableCell align="center">OPTIONS</StyledTableCell>
<StyledTableCell align="left">ENTITY NAME (CODE)</StyledTableCell>
<StyledTableCell align="right">VALUE</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{deviceEntities.map((de) => (
<TableRow key={de.s} hover>
<StyledTableCell padding="none">
<ToggleButtonGroup
size="small"
color="secondary"
value={getMask(de)}
onChange={(event, mask) => {
setMask(de, mask);
}}
>
<ToggleButton value="8" color="success" disabled={(de.m & 1) !== 0 || de.n === ''}>
<FavoriteBorderOutlinedIcon sx={{ fontSize: 14 }} />
</ToggleButton>
<ToggleButton value="4" disabled={!de.w}>
<EditOffOutlinedIcon sx={{ fontSize: 14 }} />
</ToggleButton>
<ToggleButton value="2">
<CommentsDisabledOutlinedIcon sx={{ fontSize: 14 }} />
</ToggleButton>
<ToggleButton value="1">
<VisibilityOffOutlinedIcon sx={{ fontSize: 14 }} />
</ToggleButton>
</ToggleButtonGroup>
</StyledTableCell>
<StyledTableCell align="left">ENTITY NAME</StyledTableCell>
<StyledTableCell>CODE</StyledTableCell>
<StyledTableCell align="right">VALUE</StyledTableCell>
<StyledTableCell>
{de.n}&nbsp;({de.s})
</StyledTableCell>
<StyledTableCell align="right">{formatValue(de.v)}</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{deviceEntities.map((de) => (
<TableRow
key={de.i}
onClick={() => toggleDeviceEntity(de.i)}
sx={de.x ? { backgroundColor: '#f8696b' } : { backgroundColor: 'black' }}
>
<StyledTableCell padding="checkbox">{de.x && <CloseIcon fontSize="small" />}</StyledTableCell>
<StyledTableCell component="th" scope="row">
{de.n}
</StyledTableCell>
<StyledTableCell>{de.s}</StyledTableCell>
<StyledTableCell align="right">{formatValue(de.v)}</StyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
))}
</TableBody>
</Table>
);
};

View File

@@ -12,7 +12,7 @@ import {
DeviceData,
DeviceEntity,
UniqueID,
ExcludeEntities,
MaskedEntities,
WriteValue,
WriteSensor,
WriteAnalog,
@@ -63,8 +63,8 @@ export function readDeviceEntities(unique_id: UniqueID): AxiosPromise<DeviceEnti
return AXIOS_BIN.post('/deviceEntities', unique_id);
}
export function writeExcludeEntities(excludeEntities: ExcludeEntities): AxiosPromise<void> {
return AXIOS.post('/excludeEntities', excludeEntities);
export function writeMaskedEntities(maskedEntities: MaskedEntities): AxiosPromise<void> {
return AXIOS.post('/maskedEntities', maskedEntities);
}
export function writeValue(writevalue: WriteValue): AxiosPromise<void> {
@@ -83,7 +83,6 @@ export function resetCustomizations(): AxiosPromise<void> {
return AXIOS.post('/resetCustomizations');
}
// EMS-ESP API calls
export function API(apiCall: APIcall): AxiosPromise<void> {
return AXIOS_API.post('/', apiCall);
}

View File

@@ -130,8 +130,10 @@ export interface DeviceValue {
n: string; // name
c: string; // command
l: string[]; // list
h?: string; // help text
s?: string; // steps for up/down
h?: string; // help text, optional
s?: string; // steps for up/down, optional
m?: string; // min, optional
x?: string; // max, optional
}
export interface DeviceData {
@@ -143,13 +145,14 @@ export interface DeviceEntity {
v?: any; // value, in any format
n: string; // name
s: string; // shortname
x: boolean; // excluded flag
i: number; // unique id
m: number; // mask
om?: number; // original mask before edits
w: boolean; // writeable
}
export interface ExcludeEntities {
export interface MaskedEntities {
id: number;
entity_ids: number[];
entity_ids: string[];
}
export interface UniqueID {
@@ -280,3 +283,11 @@ export interface WriteAnalog {
uom: number;
type: number;
}
export enum DeviceEntityMask {
DV_DEFAULT = 0,
DV_WEB_EXCLUDE = 1,
DV_API_MQTT_EXCLUDE = 2,
DV_READONLY = 4,
DV_FAVORITE = 8
}

View File

@@ -1,6 +1,7 @@
export enum NTPSyncStatus {
NTP_INACTIVE = 0,
NTP_ACTIVE = 1
NTP_DISABLED = 0,
NTP_INACTIVE = 1,
NTP_ACTIVE = 2
}
export interface NTPStatus {