Merge pull request #1151 from MichaelDvP/dev

Custom telegram handler #1079
This commit is contained in:
Proddy
2023-04-06 21:08:28 +02:00
committed by GitHub
37 changed files with 1274 additions and 6579 deletions

View File

@@ -11,16 +11,21 @@
- Detect old Tado thermostat, device-id 0x19, no entities - Detect old Tado thermostat, device-id 0x19, no entities
- Some more HM200 entities [#500](https://github.com/emsesp/EMS-ESP32/issues/500) - Some more HM200 entities [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
- Custom Scheduler [#701](https://github.com/emsesp/EMS-ESP32/issues/701) - Custom Scheduler [#701](https://github.com/emsesp/EMS-ESP32/issues/701)
- Custom Entities read from EMS bus
## Fixed ## Fixed
- HA-discovery for analog sensor commands [#1035](https://github.com/emsesp/EMS-ESP32/issues/1035) - HA-discovery for analog sensor commands [#1035](https://github.com/emsesp/EMS-ESP32/issues/1035)
- Enum order of RC3x nofrost mode
- Heartbeat interval
## Changed ## Changed
- Optional upgrade to platform-espressif32 6.0.0 (after 5.3.0) [#862](https://github.com/emsesp/EMS-ESP32/issues/862) - Optional upgrade to platform-espressif32 6.1.0 (after 5.3.0) [#862](https://github.com/emsesp/EMS-ESP32/issues/862)
- Use byte 0 for detection RC30 active heatingcircuit [#786](https://github.com/emsesp/EMS-ESP32/issues/786) - Use byte 3 for detection RC30 active heatingcircuit [#786](https://github.com/emsesp/EMS-ESP32/issues/786)
- Write repeated selflowtemp if tx-queue is empty without verify [#954](https://github.com/emsesp/EMS-ESP32/issues/954) - Write repeated selflowtemp if tx-queue is empty without verify [#954](https://github.com/emsesp/EMS-ESP32/issues/954)
- HA discovery recreate after disconnect by device [#1067](https://github.com/emsesp/EMS-ESP32/issues/1067) - HA discovery recreate after disconnect by device [#1067](https://github.com/emsesp/EMS-ESP32/issues/1067)
- File upload: check flash size (overflow) instead of filesize - File upload: check flash size (overflow) instead of filesize
- Improved HA Discovery so previous configs no longer need to be removed when starting [#1077](https://github.com/emsesp/EMS-ESP32/pull/1077) (thanks @pswid!) - Improved HA Discovery so previous configs no longer need to be removed when starting [#1077](https://github.com/emsesp/EMS-ESP32/pull/1077) (thanks @pswid!)
- Enlarge UART-Stack to 2,5k
- Retry timeout for Mqtt-QOS1/2 10seconds

View File

@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, , 0x2000,
app1, app, ota_1, , 0x140000,
app0, app, ota_0, , 0x2A0000,
spiffs, data, spiffs, , 64K,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0x2000
4 app1 app ota_1 0x140000
5 app0 app ota_0 0x2A0000
6 spiffs data spiffs 64K

View File

@@ -21,9 +21,9 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.6", "@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6", "@emotion/styled": "^11.10.6",
"@msgpack/msgpack": "^2.8.0", "@msgpack/msgpack": "^3.0.0-beta2",
"@mui/icons-material": "^5.11.16", "@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.16", "@mui/material": "^5.11.15",
"@remix-run/router": "^1.5.0", "@remix-run/router": "^1.5.0",
"@table-library/react-table-library": "4.1.0", "@table-library/react-table-library": "4.1.0",
"@types/lodash-es": "^4.17.7", "@types/lodash-es": "^4.17.7",

View File

@@ -67,6 +67,19 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
} }
}; };
const downloadEntities = async () => {
try {
const response = await EMSESP.getEntities();
if (response.status !== 200) {
toast.error(LL.PROBLEM_LOADING());
} else {
saveFile(response.data, 'entities');
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
};
const downloadSchedule = async () => { const downloadSchedule = async () => {
try { try {
const response = await EMSESP.getSchedule(); const response = await EMSESP.getSchedule();
@@ -125,6 +138,14 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
> >
{LL.CUSTOMIZATIONS()} {LL.CUSTOMIZATIONS()}
</Button> </Button>
<Button sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => downloadEntities()}
>
{LL.ENTITIES()}
</Button>
<Box color="warning.main"> <Box color="warning.main">
<Typography mt={2} mb={1} variant="body2"> <Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_SCHEDULE_TEXT()}{' '} {LL.DOWNLOAD_SCHEDULE_TEXT()}{' '}

View File

@@ -318,7 +318,9 @@ const de: Translation = {
SCHEDULE_SAVED: 'Plan gespeichert', SCHEDULE_SAVED: 'Plan gespeichert',
SCHEDULE_TIMER_1: 'beim Start', SCHEDULE_TIMER_1: 'beim Start',
SCHEDULE_TIMER_2: 'jede Minute', SCHEDULE_TIMER_2: 'jede Minute',
SCHEDULE_TIMER_3: 'jede Stunde' SCHEDULE_TIMER_3: 'jede Stunde',
CUSTOM_ENTITIES: 'Individuelle Entitäten',
ENTITIES_HELP_1: 'Abfrage von Werten auf dem EMS-Bus'
}; };
export default de; export default de;

View File

@@ -318,7 +318,9 @@ const en: Translation = {
SCHEDULE_SAVED: 'Schedule updated', SCHEDULE_SAVED: 'Schedule updated',
SCHEDULE_TIMER_1: 'on startup', SCHEDULE_TIMER_1: 'on startup',
SCHEDULE_TIMER_2: 'every minute', SCHEDULE_TIMER_2: 'every minute',
SCHEDULE_TIMER_3: 'every hour' SCHEDULE_TIMER_3: 'every hour',
CUSTOM_ENTITIES: 'Custom entities',
ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus'
}; };
export default en; export default en;

View File

@@ -318,7 +318,9 @@ const fr: Translation = {
SCHEDULE_SAVED: 'Schedule updated', // TODO translate SCHEDULE_SAVED: 'Schedule updated', // TODO translate
SCHEDULE_TIMER_1: 'on startup', // TODO translate SCHEDULE_TIMER_1: 'on startup', // TODO translate
SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate
SCHEDULE_TIMER_3: 'every hour' // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate
CUSTOM_ENTITIES: 'Custom entities',
ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus'
}; };
export default fr; export default fr;

View File

@@ -318,7 +318,9 @@ const nl: Translation = {
SCHEDULE_SAVED: 'Schedule updated', // TODO translate SCHEDULE_SAVED: 'Schedule updated', // TODO translate
SCHEDULE_TIMER_1: 'on startup', // TODO translate SCHEDULE_TIMER_1: 'on startup', // TODO translate
SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate
SCHEDULE_TIMER_3: 'every hour' // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate
CUSTOM_ENTITIES: 'Custom entities',
ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus'
}; };
export default nl; export default nl;

View File

@@ -318,7 +318,9 @@ const no: Translation = {
SCHEDULE_SAVED: 'Planlegger er oppdatert', SCHEDULE_SAVED: 'Planlegger er oppdatert',
SCHEDULE_TIMER_1: 'ved oppstart', SCHEDULE_TIMER_1: 'ved oppstart',
SCHEDULE_TIMER_2: 'hvert minutt', SCHEDULE_TIMER_2: 'hvert minutt',
SCHEDULE_TIMER_3: 'hver time' SCHEDULE_TIMER_3: 'hver time',
CUSTOM_ENTITIES: 'Custom entities',
ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus'
}; };
export default no; export default no;

View File

@@ -318,7 +318,9 @@ const pl: BaseTranslation = {
SCHEDULE_SAVED: 'Harmonogram został uaktualniony.', SCHEDULE_SAVED: 'Harmonogram został uaktualniony.',
SCHEDULE_TIMER_1: 'przy starcie', SCHEDULE_TIMER_1: 'przy starcie',
SCHEDULE_TIMER_2: 'co minutę', SCHEDULE_TIMER_2: 'co minutę',
SCHEDULE_TIMER_3: 'co godzinę' SCHEDULE_TIMER_3: 'co godzinę',
CUSTOM_ENTITIES: 'Custom entities',
ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus'
}; };
export default pl; export default pl;

View File

@@ -318,7 +318,9 @@ const sv: Translation = {
SCHEDULE_SAVED: 'Schedule updated', // TODO translate SCHEDULE_SAVED: 'Schedule updated', // TODO translate
SCHEDULE_TIMER_1: 'on startup', // TODO translate SCHEDULE_TIMER_1: 'on startup', // TODO translate
SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate
SCHEDULE_TIMER_3: 'every hour' // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate
CUSTOM_ENTITIES: 'Custom entities',
ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus'
}; };
export default sv; export default sv;

View File

@@ -318,7 +318,9 @@ const tr: Translation = {
SCHEDULE_SAVED: 'Schedule updated', // TODO translate SCHEDULE_SAVED: 'Schedule updated', // TODO translate
SCHEDULE_TIMER_1: 'on startup', // TODO translate SCHEDULE_TIMER_1: 'on startup', // TODO translate
SCHEDULE_TIMER_2: 'every minute', // TODO translate SCHEDULE_TIMER_2: 'every minute', // TODO translate
SCHEDULE_TIMER_3: 'every hour' // TODO translate SCHEDULE_TIMER_3: 'every hour', // TODO translate
CUSTOM_ENTITIES: 'Custom entities',
ENTITIES_HELP_1: 'Fetch custom entities from the EMS-bus'
}; };
export default tr; export default tr;

View File

@@ -2,7 +2,7 @@ import { FC } from 'react';
import { CgSmartHomeBoiler } from 'react-icons/cg'; import { CgSmartHomeBoiler } from 'react-icons/cg';
import { FaSolarPanel } from 'react-icons/fa'; import { FaSolarPanel } from 'react-icons/fa';
import { MdThermostatAuto, MdOutlineSensors } from 'react-icons/md'; import { MdThermostatAuto, MdOutlineSensors, MdOutlineExtension } from 'react-icons/md';
import { GiHeatHaze } from 'react-icons/gi'; import { GiHeatHaze } from 'react-icons/gi';
import { TiFlowSwitch } from 'react-icons/ti'; import { TiFlowSwitch } from 'react-icons/ti';
import { VscVmConnect } from 'react-icons/vsc'; import { VscVmConnect } from 'react-icons/vsc';
@@ -31,6 +31,7 @@ const enum DeviceType {
PUMP, PUMP,
GENERIC, GENERIC,
HEATSOURCE, HEATSOURCE,
CUSTOM,
UNKNOWN UNKNOWN
} }
@@ -61,6 +62,8 @@ const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
return <AiOutlineAlert />; return <AiOutlineAlert />;
case DeviceType.PUMP: case DeviceType.PUMP:
return <AiOutlineChrome />; return <AiOutlineChrome />;
case DeviceType.CUSTOM:
return <MdOutlineExtension />;
default: default:
return null; return null;
} }

View File

@@ -10,6 +10,7 @@ import { useI18nContext } from 'i18n/i18n-react';
import SettingsApplication from './SettingsApplication'; import SettingsApplication from './SettingsApplication';
import SettingsCustomization from './SettingsCustomization'; import SettingsCustomization from './SettingsCustomization';
import SettingsScheduler from './SettingsScheduler'; import SettingsScheduler from './SettingsScheduler';
import SettingsEntities from './SettingsEntities';
const Settings: FC = () => { const Settings: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -23,11 +24,13 @@ const Settings: FC = () => {
<Tab value="application" label={LL.APPLICATION_SETTINGS()} /> <Tab value="application" label={LL.APPLICATION_SETTINGS()} />
<Tab value="customization" label={LL.CUSTOMIZATIONS()} /> <Tab value="customization" label={LL.CUSTOMIZATIONS()} />
<Tab value="scheduler" label={LL.SCHEDULER()} /> <Tab value="scheduler" label={LL.SCHEDULER()} />
<Tab value="customentities" label={LL.CUSTOM_ENTITIES()} />
</RouterTabs> </RouterTabs>
<Routes> <Routes>
<Route path="application" element={<SettingsApplication />} /> <Route path="application" element={<SettingsApplication />} />
<Route path="customization" element={<SettingsCustomization />} /> <Route path="customization" element={<SettingsCustomization />} />
<Route path="scheduler" element={<SettingsScheduler />} /> <Route path="scheduler" element={<SettingsScheduler />} />
<Route path="customentities" element={<SettingsEntities />} />
<Route path="/*" element={<Navigate replace to="application" />} /> <Route path="/*" element={<Navigate replace to="application" />} />
</Routes> </Routes>
</> </>

View File

@@ -271,6 +271,7 @@ const SettingsCustomization: FC = () => {
if (devices) { if (devices) {
const selected_device = parseInt(event.target.value, 10); const selected_device = parseInt(event.target.value, 10);
setSelectedDevice(selected_device); setSelectedDevice(selected_device);
setNumChanges(0);
fetchDeviceEntities(devices?.devices[selected_device].i); fetchDeviceEntities(devices?.devices[selected_device].i);
setRestartNeeded(false); setRestartNeeded(false);
} }

View File

@@ -0,0 +1,491 @@
import { FC, useState, useEffect, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import {
Button,
Typography,
Box,
Grid,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
MenuItem,
InputAdornment
} from '@mui/material';
import { useTheme } from '@table-library/react-table-library/theme';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { toast } from 'react-toastify';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
import WarningIcon from '@mui/icons-material/Warning';
import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done';
import AddIcon from '@mui/icons-material/Add';
import { ValidatedTextField, ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { DeviceValueUOM_s, EntityItem } from './types';
import { extractErrorMessage, updateValue } from 'utils';
import { validate } from 'validators';
import { entityItemValidation } from './validators';
import { useI18nContext } from 'i18n/i18n-react';
import { ValidateFieldsError } from 'async-validator';
import * as EMSESP from './api';
const SettingsEntities: FC = () => {
const { LL, locale } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
const emptyEntity = {
device_id: 8,
type_id: 2,
offset: 0,
factor: 1,
uom: 0,
val_type: 2,
name: 'name',
deleted: false
};
const [entity, setEntity] = useState<EntityItem[]>([emptyEntity]);
const [entityItem, setEntityItem] = useState<EntityItem>();
const [errorMessage, setErrorMessage] = useState<string>();
const [creating, setCreating] = useState<boolean>(false);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
useEffect(() => {
setNumChanges(getNumChanges());
});
const entity_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) 80px 80px 80px 80px;
`,
BaseRow: `
font-size: 14px;
.td {
height: 32px;
}
`,
BaseCell: `
&:nth-of-type(2) {
text-align: center;
}
&:nth-of-type(3) {
text-align: center;
}
&:nth-of-type(4) {
text-align: center;
}
&:nth-of-type(5) {
text-align: right;
}
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
.th {
border-bottom: 1px solid #565656;
height: 36px;
}
`,
Row: `
background-color: #1e1e1e;
position: relative;
cursor: pointer;
.td {
border-top: 1px solid #565656;
border-bottom: 1px solid #565656;
}
&:hover .td {
border-top: 1px solid #177ac9;
border-bottom: 1px solid #177ac9;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
`
});
const fetchEntities = useCallback(async () => {
try {
const response = await EMSESP.readEntities();
setOriginalEntity(response.data.entity);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
useEffect(() => {
fetchEntities();
}, [fetchEntities]);
const setOriginalEntity = (data: EntityItem[]) => {
setEntity(
data.map((ei) => ({
...ei,
o_device_id: ei.device_id,
o_type_id: ei.type_id,
o_offset: ei.offset,
o_factor: ei.factor,
o_uom: ei.uom,
o_val_type: ei.val_type,
o_name: ei.name,
o_deleted: ei.deleted
}))
);
};
function hasEntityChanged(ei: EntityItem) {
return (
ei.device_id !== ei.o_device_id ||
ei.type_id !== ei.o_type_id ||
ei.name !== ei.o_name ||
ei.offset !== ei.o_offset ||
ei.uom !== ei.o_uom ||
ei.factor !== ei.o_factor ||
ei.val_type !== ei.o_val_type ||
ei.deleted !== ei.o_deleted
);
}
const getNumChanges = () => {
if (!entity) {
return 0;
}
return entity.filter((ei) => hasEntityChanged(ei)).length;
};
const saveEntity = async () => {
if (entity) {
try {
const response = await EMSESP.writeEntities({
entity: entity
.filter((ei) => !ei.deleted)
.map((condensed_ei) => {
return {
device_id: condensed_ei.device_id,
type_id: condensed_ei.type_id,
offset: condensed_ei.offset,
factor: condensed_ei.factor,
val_type: condensed_ei.val_type,
uom: condensed_ei.uom,
name: condensed_ei.name
};
})
});
if (response.status === 200) {
toast.success(LL.SUCCESS());
} else {
toast.error(LL.PROBLEM_UPDATING());
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
setOriginalEntity(entity);
}
};
const editEntityItem = (ei: EntityItem) => {
setCreating(false);
setEntityItem(ei);
};
const addEntityItem = () => {
setCreating(true);
setEntityItem({
device_id: 8,
type_id: 2,
offset: 0,
factor: 1,
val_type: 2,
uom: 0,
name: 'name',
deleted: false
});
};
const updateEntityItem = () => {
if (entityItem) {
setEntity([...entity.filter((ei) => creating || ei.o_name !== entityItem.o_name), entityItem]);
}
setEntityItem(undefined);
};
function formatValue(value: any, uom: number) {
if (value === undefined) {
return '';
}
if (uom === 0) {
return new Intl.NumberFormat().format(value);
}
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
}
function showHex(value: string, digit: number) {
if (digit === 4) {
return '0x' + ('000' + value).slice(-4);
}
return '0x' + ('0' + value).slice(-2);
}
const renderEntity = () => {
if (!entity) {
return <FormLoader errorMessage={errorMessage} />;
}
return (
<Table
data={{ nodes: entity.filter((ei) => !ei.deleted).sort((a, b) => a.name.localeCompare(b.time)) }}
theme={entity_theme}
layout={{ custom: true }}
>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell>{LL.NAME(0)}</HeaderCell>
<HeaderCell stiff>Device ID</HeaderCell>
<HeaderCell stiff>Type ID</HeaderCell>
<HeaderCell stiff>Offset</HeaderCell>
<HeaderCell stiff>{LL.VALUE()}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((ei: EntityItem) => (
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
<Cell>{ei.name}</Cell>
<Cell>{showHex(ei.device_id, 2)}</Cell>
<Cell>{showHex(ei.type_id, 4)}</Cell>
<Cell>{ei.offset}</Cell>
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
);
};
const removeEntityItem = (ei: EntityItem) => {
ei.deleted = true;
setEntityItem(ei);
updateEntityItem();
};
const validateEntityItem = async () => {
if (entityItem) {
try {
setFieldErrors(undefined);
await validate(entityItemValidation(entity, entityItem), entityItem);
updateEntityItem();
} catch (errors: any) {
setFieldErrors(errors);
}
}
};
const closeDialog = () => {
setEntityItem(undefined);
setFieldErrors(undefined);
};
const renderEditEntity = () => {
if (entityItem) {
return (
<Dialog open={!!entityItem} onClose={() => closeDialog()}>
<DialogTitle>
{creating ? LL.ADD(1) + ' ' + LL.NEW() : LL.EDIT()}&nbsp;{LL.CUSTOM_ENTITIES()}
</DialogTitle>
<DialogContent dividers>
<Box display="flex" flexWrap="wrap" mb={1}>
<Box flexGrow={1}></Box>
<Box flexWrap="nowrap" whiteSpace="nowrap"></Box>
</Box>
<Grid container spacing={2}>
<Grid item xs={12}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="name"
label={LL.NAME(0)}
value={entityItem.name}
margin="normal"
fullWidth
onChange={updateValue(setEntityItem)}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
name="device_id"
label="Device ID"
margin="normal"
fullWidth
value={entityItem.device_id}
onChange={updateValue(setEntityItem)}
InputProps={{
startAdornment: <InputAdornment position="start">0x</InputAdornment>
}}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
name="type_id"
label="Type ID"
margin="normal"
fullWidth
value={entityItem.type_id}
onChange={updateValue(setEntityItem)}
InputProps={{
startAdornment: <InputAdornment position="start">0x</InputAdornment>
}}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
name="offset"
label="Offset"
margin="normal"
fullWidth
type="number"
value={entityItem.offset}
onChange={updateValue(setEntityItem)}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
name="val_type"
label="Value Type"
value={entityItem.val_type}
variant="outlined"
onChange={updateValue(setEntityItem)}
margin="normal"
fullWidth
select
>
<MenuItem value={0}>BOOL</MenuItem>
<MenuItem value={1}>INT</MenuItem>
<MenuItem value={2}>UINT</MenuItem>
<MenuItem value={3}>SHORT</MenuItem>
<MenuItem value={4}>USHORT</MenuItem>
<MenuItem value={5}>ULONG</MenuItem>
<MenuItem value={6}>TIME</MenuItem>
</ValidatedTextField>
</Grid>
{entityItem.val_type !== 0 && (
<>
<Grid item xs={4}>
<ValidatedTextField
name="factor"
label={LL.FACTOR()}
value={entityItem.factor}
variant="outlined"
onChange={updateValue(setEntityItem)}
fullWidth
margin="normal"
type="number"
inputProps={{ step: '0.001' }}
/>
</Grid>
<Grid item xs={4}>
<ValidatedTextField
name="uom"
label={LL.UNIT()}
value={entityItem.uom}
margin="normal"
fullWidth
onChange={updateValue(setEntityItem)}
select
>
{DeviceValueUOM_s.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
</MenuItem>
))}
</ValidatedTextField>
</Grid>
</>
)}
</Grid>
</DialogContent>
<DialogActions>
{!creating && (
<Box flexGrow={1}>
<Button
startIcon={<RemoveIcon />}
variant="outlined"
color="error"
onClick={() => removeEntityItem(entityItem)}
>
{LL.REMOVE()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => closeDialog()} color="secondary">
{LL.CANCEL()}
</Button>
<Button
startIcon={creating ? <AddIcon /> : <DoneIcon />}
variant="outlined"
type="submit"
onClick={() => validateEntityItem()}
color="primary"
>
{creating ? LL.ADD(0) : LL.UPDATE()}
</Button>
</DialogActions>
</Dialog>
);
}
};
return (
<SectionContent title={LL.CUSTOM_ENTITIES()} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
</Box>
{renderEntity()}
{renderEditEntity()}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1}>
{numChanges !== 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => fetchEntities()} color="secondary">
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
color="info"
onClick={() => saveEntity()}
>
{LL.APPLY_CHANGES(numChanges)}
</Button>
</ButtonRow>
)}
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button startIcon={<AddIcon />} variant="outlined" color="secondary" onClick={() => addEntityItem()}>
{LL.ADD(0)}
</Button>
</ButtonRow>
</Box>
</Box>
</SectionContent>
);
};
export default SettingsEntities;

View File

@@ -17,7 +17,8 @@ import {
WriteSensor, WriteSensor,
WriteAnalog, WriteAnalog,
SensorData, SensorData,
Schedule Schedule,
Entities
} from './types'; } from './types';
export function restart(): AxiosPromise<void> { export function restart(): AxiosPromise<void> {
@@ -96,6 +97,18 @@ export function getCustomizations(): AxiosPromise<void> {
return AXIOS.get('/getCustomizations'); return AXIOS.get('/getCustomizations');
} }
export function getEntities(): AxiosPromise<void> {
return AXIOS.get('/getEntities');
}
export function readEntities(): AxiosPromise<void> {
return AXIOS.get('/entity');
}
export function writeEntities(entities: Entities): AxiosPromise<void> {
return AXIOS.post('/entity', entities);
}
export function getSchedule(): AxiosPromise<Schedule> { export function getSchedule(): AxiosPromise<Schedule> {
return AXIOS.get('/getSchedule'); return AXIOS.get('/getSchedule');
} }

View File

@@ -337,3 +337,27 @@ export enum ScheduleFlag {
SCHEDULE_SAT = 64, SCHEDULE_SAT = 64,
SCHEDULE_TIMER = 128 SCHEDULE_TIMER = 128
} }
export interface EntityItem {
name: string;
device_id: string;
type_id: string;
offset: number;
factor: number;
uom: number;
val_type: number;
value?: number;
o_name?: string;
o_device_id?: string;
o_type_id?: string;
o_offset?: number;
o_factor?: number;
o_uom?: number;
o_val_type?: number;
deleted?: boolean; // optional
o_deleted?: boolean;
}
export interface Entities {
entity: EntityItem[];
}

View File

@@ -1,6 +1,6 @@
import Schema, { InternalRuleItem } from 'async-validator'; import Schema, { InternalRuleItem } from 'async-validator';
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared'; import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
import { Settings, ScheduleItem } from './types'; import { Settings, ScheduleItem, EntityItem } from './types';
export const GPIO_VALIDATOR = { export const GPIO_VALIDATOR = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
@@ -85,16 +85,6 @@ export const createSettingsValidator = (settings: Settings) =>
}) })
}); });
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
if (name && o_name && o_name !== name && schedule.find((si) => si.name === name)) {
callback('Name already in use');
} else {
callback();
}
}
});
export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) => export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) =>
new Schema({ new Schema({
name: [ name: [
@@ -110,3 +100,48 @@ export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem:
{ type: 'string', min: 1, max: 64, message: 'Command must be 1-64 characters' } { type: 'string', min: 1, max: 64, message: 'Command must be 1-64 characters' }
] ]
}); });
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
if (name && o_name && o_name !== name && schedule.find((si) => si.name === name)) {
callback('Name already in use');
} else {
callback();
}
}
});
export const entityItemValidation = (entity: EntityItem[], entityItem: EntityItem) =>
new Schema({
name: [
{ required: true, message: 'Name is required' },
{
type: 'string',
pattern: /^[a-zA-Z0-9_\\.]{1,15}$/,
message: "Must be <15 characters: alpha numeric, '_' or '.'"
},
...[uniqueEntityNameValidator(entity, entityItem.o_name)]
],
device_id: [
{ required: true, message: 'Device_id is required' },
{ type: 'string', pattern: /^[A-F0-9]{1,2}$/, message: 'Must be a hex number' }
],
type_id: [
{ required: true, message: 'Type_id is required' },
{ type: 'string', pattern: /^[A-F0-9]{1,4}$/, message: 'Must be a hex number' }
],
offset: [
{ required: true, message: 'Offset is required' },
{ type: 'number', min: 0, max: 255, message: 'Must be between 0 and 255' }
]
});
export const uniqueEntityNameValidator = (entity: EntityItem[], o_name?: string) => ({
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
if (name && o_name && o_name !== name && entity.find((ei) => ei.name === name)) {
callback('Name already in use');
} else {
callback();
}
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -40,9 +40,9 @@ unbuild_flags =
${common.core_unbuild_flags} ${common.core_unbuild_flags}
[espressi32_base] [espressi32_base]
; platform = espressif32 platform = espressif32
; platform = espressif32@5.3.0 ; platform = espressif32@5.3.0
platform = espressif32@5.2.0 ; platform = espressif32@5.2.0
framework = arduino framework = arduino
[env] [env]
@@ -62,7 +62,8 @@ check_flags =
; build for GitHub Actions CI ; build for GitHub Actions CI
; the Web interface is built seperately ; the Web interface is built seperately
[env:ci] [env:ci]
extends = espressi32_base platform = espressif32@5.2.0
framework = arduino
extra_scripts = scripts/rename_fw.py extra_scripts = scripts/rename_fw.py
board = esp32dev board = esp32dev
board_build.partitions = esp32_partition_4M.csv board_build.partitions = esp32_partition_4M.csv
@@ -71,7 +72,8 @@ build_flags = ${common.build_flags}
build_unflags = ${common.unbuild_flags} build_unflags = ${common.unbuild_flags}
[env:esp32_4M] [env:esp32_4M]
extends = espressi32_base platform = espressif32@5.2.0
framework = arduino
extra_scripts = extra_scripts =
pre:scripts/build_interface.py pre:scripts/build_interface.py
scripts/rename_fw.py scripts/rename_fw.py
@@ -81,6 +83,17 @@ board_build.partitions = esp32_partition_4M.csv
build_flags = ${common.build_flags} -Os build_flags = ${common.build_flags} -Os
build_unflags = ${common.unbuild_flags} build_unflags = ${common.unbuild_flags}
[env:esp32_4M+]
extends = espressi32_base
extra_scripts =
pre:scripts/build_interface.py
scripts/rename_fw.py
board = esp32dev
board_upload.flash_size = 4MB
board_build.partitions = esp32_asym_partition_4M.csv
build_flags = ${common.build_flags}
build_unflags = ${common.unbuild_flags}
[env:esp32_16M] [env:esp32_16M]
extends = espressi32_base extends = espressi32_base
extra_scripts = extra_scripts =
@@ -99,7 +112,7 @@ extra_scripts =
scripts/rename_fw.py scripts/rename_fw.py
board = lolin_c3_mini board = lolin_c3_mini
board_upload.flash_size = 4MB board_upload.flash_size = 4MB
board_build.partitions = esp32_partition_4M.csv board_build.partitions = esp32_asym_partition_4M.csv
build_flags = ${common.build_flags} build_flags = ${common.build_flags}
build_unflags = ${common.unbuild_flags} build_unflags = ${common.unbuild_flags}
@@ -112,7 +125,7 @@ extra_scripts =
scripts/rename_fw.py scripts/rename_fw.py
board = lolin_c3_mini board = lolin_c3_mini
board_upload.flash_size = 4MB board_upload.flash_size = 4MB
board_build.partitions = esp32_partition_4M.csv board_build.partitions = esp32_asym_partition_4M.csv
build_flags = ${common.build_flags} -DBOARD_C3_MINI_V1 build_flags = ${common.build_flags} -DBOARD_C3_MINI_V1
build_unflags = ${common.unbuild_flags} build_unflags = ${common.unbuild_flags}
@@ -123,7 +136,7 @@ extra_scripts =
scripts/rename_fw.py scripts/rename_fw.py
board = lolin_s2_mini board = lolin_s2_mini
board_upload.flash_size = 4MB board_upload.flash_size = 4MB
board_build.partitions = esp32_partition_4M.csv board_build.partitions = esp32_asym_partition_4M.csv
build_flags = ${common.build_flags} build_flags = ${common.build_flags}
build_unflags = ${common.unbuild_flags} build_unflags = ${common.unbuild_flags}

View File

@@ -39,6 +39,7 @@ def buildWeb():
text = r.read().replace("Locales = 'pl'", "Locales = 'en'") text = r.read().replace("Locales = 'pl'", "Locales = 'en'")
with open("./src/i18n/i18n-util.ts", "w") as w: with open("./src/i18n/i18n-util.ts", "w") as w:
w.write(text) w.write(text)
print("Setting locale to 'en'")
env.Execute("yarn run build") env.Execute("yarn run build")
buildPath = Path("build") buildPath = Path("build")

View File

@@ -539,6 +539,10 @@ bool Command::device_has_commands(const uint8_t device_type) {
return EMSESP::webSchedulerService.has_commands(); return EMSESP::webSchedulerService.has_commands();
} }
if (device_type == EMSdevice::DeviceType::CUSTOM) {
return (EMSESP::webEntityService.count_entities() != 0);
}
if (device_type == EMSdevice::DeviceType::DALLASSENSOR) { if (device_type == EMSdevice::DeviceType::DALLASSENSOR) {
return (EMSESP::dallassensor_.have_sensors()); return (EMSESP::dallassensor_.have_sensors());
} }

View File

@@ -135,6 +135,8 @@ const char * EMSdevice::device_type_2_device_name(const uint8_t device_type) {
return F_(pump); return F_(pump);
case DeviceType::HEATSOURCE: case DeviceType::HEATSOURCE:
return F_(heatsource); return F_(heatsource);
case DeviceType::CUSTOM:
return F_(custom);
default: default:
return Helpers::translated_word(FL_(unknown), true); return Helpers::translated_word(FL_(unknown), true);
} }
@@ -229,6 +231,9 @@ uint8_t EMSdevice::device_name_2_device_type(const char * topic) {
if (!strcmp(lowtopic, F_(heatsource))) { if (!strcmp(lowtopic, F_(heatsource))) {
return DeviceType::HEATSOURCE; return DeviceType::HEATSOURCE;
} }
if (!strcmp(lowtopic, F_(custom))) {
return DeviceType::CUSTOM;
}
return DeviceType::UNKNOWN; return DeviceType::UNKNOWN;
} }

View File

@@ -336,6 +336,7 @@ class EMSdevice {
PUMP, PUMP,
GENERIC, GENERIC,
HEATSOURCE, HEATSOURCE,
CUSTOM,
UNKNOWN UNKNOWN
}; };

View File

@@ -32,11 +32,13 @@ ESP8266React EMSESP::esp8266React(&webServer, &dummyFS);
WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, &dummyFS, EMSESP::esp8266React.getSecurityManager()); WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, &dummyFS, EMSESP::esp8266React.getSecurityManager());
WebCustomizationService EMSESP::webCustomizationService = WebCustomizationService(&webServer, &dummyFS, EMSESP::esp8266React.getSecurityManager()); WebCustomizationService EMSESP::webCustomizationService = WebCustomizationService(&webServer, &dummyFS, EMSESP::esp8266React.getSecurityManager());
WebSchedulerService EMSESP::webSchedulerService = WebSchedulerService(&webServer, &dummyFS, EMSESP::esp8266React.getSecurityManager()); WebSchedulerService EMSESP::webSchedulerService = WebSchedulerService(&webServer, &dummyFS, EMSESP::esp8266React.getSecurityManager());
WebEntityService EMSESP::webEntityService = WebEntityService(&webServer, &dummyFS, EMSESP::esp8266React.getSecurityManager());
#else #else
ESP8266React EMSESP::esp8266React(&webServer, &LittleFS); ESP8266React EMSESP::esp8266React(&webServer, &LittleFS);
WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, &LittleFS, EMSESP::esp8266React.getSecurityManager()); WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, &LittleFS, EMSESP::esp8266React.getSecurityManager());
WebCustomizationService EMSESP::webCustomizationService = WebCustomizationService(&webServer, &LittleFS, EMSESP::esp8266React.getSecurityManager()); WebCustomizationService EMSESP::webCustomizationService = WebCustomizationService(&webServer, &LittleFS, EMSESP::esp8266React.getSecurityManager());
WebSchedulerService EMSESP::webSchedulerService = WebSchedulerService(&webServer, &LittleFS, EMSESP::esp8266React.getSecurityManager()); WebSchedulerService EMSESP::webSchedulerService = WebSchedulerService(&webServer, &LittleFS, EMSESP::esp8266React.getSecurityManager());
WebEntityService EMSESP::webEntityService = WebEntityService(&webServer, &LittleFS, EMSESP::esp8266React.getSecurityManager());
#endif #endif
WebStatusService EMSESP::webStatusService = WebStatusService(&webServer, EMSESP::esp8266React.getSecurityManager()); WebStatusService EMSESP::webStatusService = WebStatusService(&webServer, EMSESP::esp8266React.getSecurityManager());
@@ -480,6 +482,7 @@ void EMSESP::publish_all(bool force) {
publish_device_values(EMSdevice::DeviceType::MIXER); publish_device_values(EMSdevice::DeviceType::MIXER);
publish_other_values(); // switch and heat pump, ... publish_other_values(); // switch and heat pump, ...
webSchedulerService.publish(); webSchedulerService.publish();
webEntityService.publish();
publish_sensor_values(true); // includes dallas and analog sensors publish_sensor_values(true); // includes dallas and analog sensors
system_.send_heartbeat(); system_.send_heartbeat();
} }
@@ -512,6 +515,7 @@ void EMSESP::publish_all_loop() {
case 5: case 5:
publish_other_values(); // switch and heat pump publish_other_values(); // switch and heat pump
webSchedulerService.publish(true); webSchedulerService.publish(true);
webEntityService.publish(true);
break; break;
case 6: case 6:
publish_sensor_values(true, true); publish_sensor_values(true, true);
@@ -601,6 +605,7 @@ void EMSESP::publish_other_values() {
// publish_device_values(EMSdevice::DeviceType::ALERT); // publish_device_values(EMSdevice::DeviceType::ALERT);
// publish_device_values(EMSdevice::DeviceType::PUMP); // publish_device_values(EMSdevice::DeviceType::PUMP);
// publish_device_values(EMSdevice::DeviceType::GENERIC); // publish_device_values(EMSdevice::DeviceType::GENERIC);
webEntityService.publish();
} }
// publish both the dallas and analog sensor values // publish both the dallas and analog sensor values
@@ -666,6 +671,11 @@ bool EMSESP::get_device_value_info(JsonObject & root, const char * cmd, const in
return EMSESP::webSchedulerService.get_value_info(root, cmd); return EMSESP::webSchedulerService.get_value_info(root, cmd);
} }
// own entities
if (devicetype == DeviceType::CUSTOM) {
return EMSESP::webEntityService.get_value_info(root, cmd);
}
char error[100]; char error[100];
snprintf(error, sizeof(error), "cannot find values for entity '%s'", cmd); snprintf(error, sizeof(error), "cannot find values for entity '%s'", cmd);
root["message"] = error; root["message"] = error;
@@ -866,6 +876,9 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
return false; return false;
} }
// Check for custom entities reding this telegram
webEntityService.get_value(telegram);
// check for common types, like the Version(0x02) // check for common types, like the Version(0x02)
if (telegram->type_id == EMSdevice::EMS_TYPE_VERSION) { if (telegram->type_id == EMSdevice::EMS_TYPE_VERSION) {
process_version(telegram); process_version(telegram);
@@ -1063,6 +1076,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const
name = "RF room temperature sensor"; name = "RF room temperature sensor";
device_type = DeviceType::THERMOSTAT; device_type = DeviceType::THERMOSTAT;
} else if (device_id == EMSdevice::EMS_DEVICE_ID_ROOMTHERMOSTAT || device_id == EMSdevice::EMS_DEVICE_ID_TADO_OLD) { } else if (device_id == EMSdevice::EMS_DEVICE_ID_ROOMTHERMOSTAT || device_id == EMSdevice::EMS_DEVICE_ID_TADO_OLD) {
// see https://github.com/emsesp/EMS-ESP32/issues/174
name = "Generic thermostat"; name = "Generic thermostat";
device_type = DeviceType::THERMOSTAT; device_type = DeviceType::THERMOSTAT;
flags = DeviceFlags::EMS_DEVICE_FLAG_RC10 | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE; flags = DeviceFlags::EMS_DEVICE_FLAG_RC10 | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE;
@@ -1078,7 +1092,8 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const
} else if (device_id == EMSdevice::EMS_DEVICE_ID_CASCADE) { } else if (device_id == EMSdevice::EMS_DEVICE_ID_CASCADE) {
name = "Cascade"; name = "Cascade";
device_type = DeviceType::CONNECT; device_type = DeviceType::CONNECT;
} else if (device_id == EMSdevice::EMS_DEVICE_ID_EASYCOM) { } else if (device_id == EMSdevice::EMS_DEVICE_ID_EASYCOM
|| (device_id >= EMSdevice::EMS_DEVICE_ID_MODEM && device_id <= EMSdevice::EMS_DEVICE_ID_MODEM + 5)) {
// see https://github.com/emsesp/EMS-ESP/issues/460#issuecomment-709553012 // see https://github.com/emsesp/EMS-ESP/issues/460#issuecomment-709553012
name = "Modem"; name = "Modem";
device_type = DeviceType::CONNECT; device_type = DeviceType::CONNECT;
@@ -1372,6 +1387,7 @@ void EMSESP::scheduled_fetch_values() {
return; return;
} }
} }
webEntityService.fetch();
no = 0; no = 0;
} }
} }
@@ -1453,6 +1469,7 @@ void EMSESP::start() {
webCustomizationService.begin(); // load the customizations webCustomizationService.begin(); // load the customizations
webSchedulerService.begin(); // load the scheduler events webSchedulerService.begin(); // load the scheduler events
webEntityService.begin(); // load the custom telegram reads
// start telnet service if it's enabled // start telnet service if it's enabled
// default idle is 10 minutes, default write timeout is 0 (automatic) // default idle is 10 minutes, default write timeout is 0 (automatic)

View File

@@ -46,6 +46,7 @@
#include "web/WebSchedulerService.h" #include "web/WebSchedulerService.h"
#include "web/WebAPIService.h" #include "web/WebAPIService.h"
#include "web/WebLogService.h" #include "web/WebLogService.h"
#include "web/WebEntityService.h"
#include "emsdevicevalue.h" #include "emsdevicevalue.h"
#include "emsdevice.h" #include "emsdevice.h"
@@ -230,6 +231,7 @@ class EMSESP {
static WebLogService webLogService; static WebLogService webLogService;
static WebCustomizationService webCustomizationService; static WebCustomizationService webCustomizationService;
static WebSchedulerService webSchedulerService; static WebSchedulerService webSchedulerService;
static WebEntityService webEntityService;
private: private:
static std::string device_tostring(const uint8_t device_id); static std::string device_tostring(const uint8_t device_id);

View File

@@ -98,6 +98,7 @@ MAKE_WORD(alert)
MAKE_WORD(pump) MAKE_WORD(pump)
MAKE_WORD(heatsource) MAKE_WORD(heatsource)
MAKE_WORD(scheduler) MAKE_WORD(scheduler)
MAKE_WORD(custom)
// brands // brands
MAKE_WORD_CUSTOM(bosch, "Bosch") MAKE_WORD_CUSTOM(bosch, "Bosch")

View File

@@ -49,6 +49,7 @@ MAKE_WORD_TRANSLATION(pump_device, "Pump Module", "Pumpenmodul", "Pump Module",
MAKE_WORD_TRANSLATION(heatsource_device, "Heatsource", "Heizquelle", "Heatsource", "Värmekälla", "Źródło ciepła", "Varmekilde", "", "Isı Kaynağı") // TODO translate MAKE_WORD_TRANSLATION(heatsource_device, "Heatsource", "Heizquelle", "Heatsource", "Värmekälla", "Źródło ciepła", "Varmekilde", "", "Isı Kaynağı") // TODO translate
MAKE_WORD_TRANSLATION(sensors_device, "Sensors", "Sensoren", "Sensoren", "Sensorer", "Czujniki", "Sensorer", "Capteurs", "Sensör Cihazı") MAKE_WORD_TRANSLATION(sensors_device, "Sensors", "Sensoren", "Sensoren", "Sensorer", "Czujniki", "Sensorer", "Capteurs", "Sensör Cihazı")
MAKE_WORD_TRANSLATION(unknown_device, "Unknown", "Unbekannt", "Onbekend", "Okänt", "Nieznane urządzenie", "Ukjent", "Inconnu", "") // TODO translate MAKE_WORD_TRANSLATION(unknown_device, "Unknown", "Unbekannt", "Onbekend", "Okänt", "Nieznane urządzenie", "Ukjent", "Inconnu", "") // TODO translate
MAKE_WORD_TRANSLATION(custom_device, "User defined entities", "Nutzer deklarierte Entitäten", "", "", "", "", "", "") // TODO translate
// commands // commands
// TODO translate // TODO translate
@@ -64,6 +65,7 @@ MAKE_WORD_TRANSLATION(watch_cmd, "watch incoming telegrams", "Watch auf eingehen
MAKE_WORD_TRANSLATION(publish_cmd, "publish all to MQTT", "Publiziere MQTT", "", "", "opublikuj wszystko na MQTT", "Publiser alt til MQTT", "", "Hepsini MQTTye gönder") // TODO translate MAKE_WORD_TRANSLATION(publish_cmd, "publish all to MQTT", "Publiziere MQTT", "", "", "opublikuj wszystko na MQTT", "Publiser alt til MQTT", "", "Hepsini MQTTye gönder") // TODO translate
MAKE_WORD_TRANSLATION(system_info_cmd, "show system status", "Zeige System-Status", "", "", "pokaż status systemu", "vis system status", "", "Sistem Durumunu Göster") // TODO translate MAKE_WORD_TRANSLATION(system_info_cmd, "show system status", "Zeige System-Status", "", "", "pokaż status systemu", "vis system status", "", "Sistem Durumunu Göster") // TODO translate
MAKE_WORD_TRANSLATION(schedule_cmd, "enable schedule item", "Aktiviere Zeitplan", "", "", "aktywuj wybrany harmonogram", "", "", "") // TODO translate MAKE_WORD_TRANSLATION(schedule_cmd, "enable schedule item", "Aktiviere Zeitplan", "", "", "aktywuj wybrany harmonogram", "", "", "") // TODO translate
MAKE_WORD_TRANSLATION(entity_cmd, "set custom value on ems", "Sende eigene Entitäten zu EMS", "", "", "", "", "", "") // TODO translate
// tags // tags
MAKE_WORD_TRANSLATION(tag_boiler_data_ww, "dhw", "WW", "dhw", "VV", "CWU", "dhw", "ecs", "SKS") MAKE_WORD_TRANSLATION(tag_boiler_data_ww, "dhw", "WW", "dhw", "VV", "CWU", "dhw", "ecs", "SKS")

View File

@@ -1005,6 +1005,9 @@ bool System::check_restore() {
} else if (settings_type == "schedule") { } else if (settings_type == "schedule") {
// it's a schedule file, just replace it and there's no need to reboot // it's a schedule file, just replace it and there's no need to reboot
saveSettings(EMSESP_SCHEDULER_FILE, "Schedule", input); saveSettings(EMSESP_SCHEDULER_FILE, "Schedule", input);
} else if (settings_type == "entities") {
// it's a entity file, just replace it and there's no need to reboot
saveSettings(EMSESP_ENTITY_FILE, "Entities", input);
} else { } else {
LOG_ERROR("Unrecognized file uploaded"); LOG_ERROR("Unrecognized file uploaded");
} }

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.6.0-dev.8" #define EMSESP_APP_VERSION "3.6.0-dev.9"

View File

@@ -37,6 +37,7 @@ WebAPIService::WebAPIService(AsyncWebServer * server, SecurityManager * security
HTTP_GET, HTTP_GET,
securityManager->wrapRequest(std::bind(&WebAPIService::getCustomizations, this, _1), AuthenticationPredicates::IS_ADMIN)); securityManager->wrapRequest(std::bind(&WebAPIService::getCustomizations, this, _1), AuthenticationPredicates::IS_ADMIN));
server->on(GET_SCHEDULE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WebAPIService::getSchedule, this, _1), AuthenticationPredicates::IS_ADMIN)); server->on(GET_SCHEDULE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WebAPIService::getSchedule, this, _1), AuthenticationPredicates::IS_ADMIN));
server->on(GET_ENTITIES_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WebAPIService::getEntities, this, _1), AuthenticationPredicates::IS_ADMIN));
} }
// HTTP GET // HTTP GET
@@ -209,4 +210,16 @@ void WebAPIService::getSchedule(AsyncWebServerRequest * request) {
request->send(response); request->send(response);
} }
void WebAPIService::getEntities(AsyncWebServerRequest * request) {
auto * response = new AsyncJsonResponse(false, FS_BUFFER_SIZE);
JsonObject root = response->getRoot();
root["type"] = "entities";
System::extractSettings(EMSESP_ENTITY_FILE, "Entities", root);
response->setLength();
request->send(response);
}
} // namespace emsesp } // namespace emsesp

View File

@@ -23,6 +23,7 @@
#define GET_SETTINGS_PATH "/rest/getSettings" #define GET_SETTINGS_PATH "/rest/getSettings"
#define GET_CUSTOMIZATIONS_PATH "/rest/getCustomizations" #define GET_CUSTOMIZATIONS_PATH "/rest/getCustomizations"
#define GET_SCHEDULE_PATH "/rest/getSchedule" #define GET_SCHEDULE_PATH "/rest/getSchedule"
#define GET_ENTITIES_PATH "/rest/getEntities"
namespace emsesp { namespace emsesp {
@@ -53,6 +54,7 @@ class WebAPIService {
void getSettings(AsyncWebServerRequest * request); void getSettings(AsyncWebServerRequest * request);
void getCustomizations(AsyncWebServerRequest * request); void getCustomizations(AsyncWebServerRequest * request);
void getSchedule(AsyncWebServerRequest * request); void getSchedule(AsyncWebServerRequest * request);
void getEntities(AsyncWebServerRequest * request);
}; };
} // namespace emsesp } // namespace emsesp

View File

@@ -91,6 +91,18 @@ void WebDataService::core_data(AsyncWebServerRequest * request) {
obj["e"] = emsdevice->count_entities(); // number of entities (device values) obj["e"] = emsdevice->count_entities(); // number of entities (device values)
} }
} }
if (EMSESP::webEntityService.count_entities()) {
JsonObject obj = devices.createNestedObject();
obj["id"] = "99"; // the last unique id as a string
obj["tn"] = "Custom"; // translated device type name
obj["t"] = EMSdevice::DeviceType::CUSTOM; // device type number
obj["b"] = 0; // brand
obj["n"] = Helpers::translated_word(FL_(custom_device)); // name
obj["d"] = 0; // deviceid
obj["p"] = 0; // productid
obj["v"] = 0; // version
obj["e"] = EMSESP::webEntityService.count_entities(); // number of entities (device values)
}
// sensors stuff // sensors stuff
root["s_n"] = Helpers::translated_word(FL_(sensors_device)); root["s_n"] = Helpers::translated_word(FL_(sensors_device));
@@ -196,6 +208,15 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant &
return; return;
} }
} }
#ifndef EMSESP_STANDALONE
if (json["id"] == 99) {
JsonObject output = response->getRoot();
EMSESP::webEntityService.generate_value_web(output);
response->setLength();
request->send(response);
return;
}
#endif
} }
// invalid but send ok // invalid but send ok
@@ -256,6 +277,37 @@ void WebDataService::write_value(AsyncWebServerRequest * request, JsonVariant &
return; return;
} }
} }
if (unique_id == 99) {
// parse the command as it could have a hc or wwc prefixed, e.g. hc2/seltemp
const char * cmd = dv["c"];
int8_t id = -1;
cmd = Command::parse_command_string(cmd, id);
auto * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL);
JsonObject output = response->getRoot();
JsonVariant data = dv["v"]; // the value in any format
uint8_t return_code = CommandRet::OK;
uint8_t device_type = EMSdevice::DeviceType::CUSTOM;
if (data.is<int>()) {
char s[10];
return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as<int16_t>(), 0), true, id, output);
} else if (data.is<float>()) {
char s[10];
return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as<float>(), 1), true, id, output);
} else if (data.is<bool>()) {
return_code = Command::call(device_type, cmd, data.as<bool>() ? "true" : "false", true, id, output);
}
if (return_code != CommandRet::OK) {
EMSESP::logger().err("Write command failed %s (%s)", (const char *)output["message"], Command::return_code_string(return_code).c_str());
} else {
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug("Write command successful");
#endif
}
response->setCode((return_code == CommandRet::OK) ? 200 : 204);
response->setLength();
request->send(response);
return;
}
} }
AsyncWebServerResponse * response = request->beginResponse(204); // Write command failed AsyncWebServerResponse * response = request->beginResponse(204); // Write command failed

View File

@@ -0,0 +1,429 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2023 Paul Derbyshire
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "emsesp.h"
namespace emsesp {
using namespace std::placeholders; // for `_1` etc
WebEntityService::WebEntityService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(WebEntity::read, WebEntity::update, this, server, EMSESP_ENTITY_SERVICE_PATH, securityManager, AuthenticationPredicates::IS_AUTHENTICATED)
, _fsPersistence(WebEntity::read, WebEntity::update, this, fs, EMSESP_ENTITY_FILE, FS_BUFFER_SIZE) {
}
// load the settings when the service starts
void WebEntityService::begin() {
_fsPersistence.readFromFS();
EMSESP::logger().info("Starting custom entity service");
}
// this creates the scheduler file, saving it to the FS
// and also calls when the Scheduler web page is refreshed
void WebEntity::read(WebEntity & webEntity, JsonObject & root) {
JsonArray entity = root.createNestedArray("entity");
for (const EntityItem & entityItem : webEntity.entityItems) {
JsonObject ei = entity.createNestedObject();
ei["device_id"] = Helpers::hextoa(entityItem.device_id, false);
ei["type_id"] = Helpers::hextoa(entityItem.type_id, false);
ei["offset"] = entityItem.offset;
ei["factor"] = entityItem.factor;
ei["name"] = entityItem.name;
ei["uom"] = entityItem.uom;
ei["val_type"] = entityItem.valuetype;
EMSESP::webEntityService.render_value(ei, entityItem, true);
}
}
// call on initialization and also when the Schedule web page is updated
// this loads the data into the internal class
StateUpdateResult WebEntity::update(JsonObject & root, WebEntity & webEntity) {
for (EntityItem & entityItem : webEntity.entityItems) {
Command::erase_command(EMSdevice::DeviceType::CUSTOM, entityItem.name.c_str());
}
webEntity.entityItems.clear();
if (root["entity"].is<JsonArray>()) {
for (const JsonObject ei : root["entity"].as<JsonArray>()) {
auto entityItem = EntityItem();
entityItem.device_id = Helpers::hextoint(ei["device_id"]);
entityItem.type_id = Helpers::hextoint(ei["type_id"]);
entityItem.offset = ei["offset"];
entityItem.factor = ei["factor"];
entityItem.name = ei["name"].as<std::string>();
entityItem.uom = ei["uom"];
entityItem.valuetype = ei["val_type"];
if (entityItem.valuetype == DeviceValueType::BOOL) {
entityItem.val = EMS_VALUE_DEFAULT_BOOL;
} else if (entityItem.valuetype == DeviceValueType::INT) {
entityItem.val = EMS_VALUE_DEFAULT_INT;
} else if (entityItem.valuetype == DeviceValueType::UINT) {
entityItem.val = EMS_VALUE_DEFAULT_UINT;
} else if (entityItem.valuetype == DeviceValueType::SHORT) {
entityItem.val = EMS_VALUE_DEFAULT_SHORT;
} else if (entityItem.valuetype == DeviceValueType::USHORT) {
entityItem.val = EMS_VALUE_DEFAULT_USHORT;
} else { // if (entityItem.valuetype == DeviceValueType::ULONG || entityItem.valuetype == DeviceValueType::TIME) {
entityItem.val = EMS_VALUE_DEFAULT_ULONG;
}
webEntity.entityItems.push_back(entityItem); // add to list
Command::add(
EMSdevice::DeviceType::CUSTOM,
webEntity.entityItems.back().name.c_str(),
[webEntity](const char * value, const int8_t id) { return EMSESP::webEntityService.command_setvalue(value, webEntity.entityItems.back().name); },
FL_(entity_cmd),
CommandFlag::ADMIN_ONLY);
}
}
return StateUpdateResult::CHANGED;
}
// set value by api command
bool WebEntityService::command_setvalue(const char * value, const std::string name) {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
for (EntityItem & entityItem : *entityItems) {
if (entityItem.name == name) {
if (entityItem.valuetype == DeviceValueType::BOOL) {
bool v;
if (!Helpers::value2bool(value, v)) {
return false;
}
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v ? 0xFF : 0, 0);
} else {
float f;
if (!Helpers::value2float(value, f)) {
return false;
}
int v = f / entityItem.factor;
if (entityItem.valuetype == DeviceValueType::UINT || entityItem.valuetype == DeviceValueType::INT) {
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v, 0);
} else if (entityItem.valuetype == DeviceValueType::USHORT || entityItem.valuetype == DeviceValueType::SHORT) {
uint8_t v1[2] = {(uint8_t)(v >> 8), (uint8_t)(v & 0xFF)};
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v1, 2, 0);
} else {
uint8_t v1[3] = {(uint8_t)(v >> 16), (uint8_t)((v & 0xFF00) >> 8), (uint8_t)(v & 0xFF)};
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v1, 3, 0);
}
}
publish_single(entityItem);
if (EMSESP::mqtt_.get_publish_onchange(0)) {
publish();
}
return true;
}
}
return false;
}
// output of a single value
void WebEntityService::render_value(JsonObject & output, EntityItem entity, const bool useVal) {
char payload[12];
std::string name = useVal ? "value" : entity.name;
switch (entity.valuetype) {
case DeviceValueType::BOOL:
if ((uint8_t)entity.val != EMS_VALUE_BOOL_NOTSET) {
if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) {
output[name] = (uint8_t)entity.val ? true : false;
} else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) {
output[name] = (uint8_t)entity.val ? 1 : 0;
} else {
output[name] = Helpers::render_boolean(payload, (uint8_t)entity.val);
}
}
break;
case DeviceValueType::INT:
if ((int8_t)entity.val != EMS_VALUE_INT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (int8_t)entity.val, 2));
}
break;
case DeviceValueType::UINT:
if ((uint8_t)entity.val != EMS_VALUE_UINT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint8_t)entity.val, 2));
}
break;
case DeviceValueType::SHORT:
if ((int16_t)entity.val != EMS_VALUE_SHORT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (int16_t)entity.val, 2));
}
break;
case DeviceValueType::USHORT:
if ((uint16_t)entity.val != EMS_VALUE_USHORT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint16_t)entity.val, 2));
}
break;
case DeviceValueType::ULONG:
case DeviceValueType::TIME:
if (entity.val != EMS_VALUE_ULONG_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * entity.val, 2));
}
break;
default:
// EMSESP::logger().warning("unknown value type");
break;
}
}
// process json output for info/commands and value_info
bool WebEntityService::get_value_info(JsonObject & output, const char * cmd) {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
if (entityItems->size() == 0) {
return false;
}
if (Helpers::toLower(cmd) == "commands") {
output["info"] = "lists all values";
output["commands"] = "lists all commands";
for (const auto & entity : *entityItems) {
output[entity.name] = "custom entitiy";
}
return true;
}
if (strlen(cmd) == 0 || Helpers::toLower(cmd) == "values" || Helpers::toLower(cmd) == "info") {
// list all names
for (const EntityItem & entity : *entityItems) {
render_value(output, entity);
}
return (output.size() != 0);
}
char command_s[30];
strlcpy(command_s, cmd, sizeof(command_s));
char * attribute_s = nullptr;
// check specific attribute to fetch instead of the complete record
char * breakp = strchr(command_s, '/');
if (breakp) {
*breakp = '\0';
attribute_s = breakp + 1;
}
for (const auto & entity : *entityItems) {
if (Helpers::toLower(entity.name) == Helpers::toLower(command_s)) {
output["name"] = entity.name;
output["uom"] = EMSdevice::uom_to_string(entity.uom);
output["readable"] = true;
output["writeable"] = true;
output["visible"] = true;
render_value(output, entity, true);
if (attribute_s) {
if (output.containsKey(attribute_s)) {
JsonVariant data = output[attribute_s];
output.clear();
output["api_data"] = data;
} else {
char error[100];
snprintf(error, sizeof(error), "cannot find attribute %s in entity %s", attribute_s, command_s);
output.clear();
output["message"] = error;
}
}
}
if (output.size()) {
return true;
}
}
output["message"] = "unknown command";
return false;
}
// publish single value
void WebEntityService::publish_single(const EntityItem & entity) {
if (!Mqtt::enabled() || !Mqtt::publish_single()) {
return;
}
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
if (Mqtt::publish_single2cmd()) {
snprintf(topic, sizeof(topic), "%s/%s", "custom", entity.name.c_str());
} else {
snprintf(topic, sizeof(topic), "%s/%s", "custom_data", entity.name.c_str());
}
StaticJsonDocument<256> doc;
JsonObject output = doc.to<JsonObject>();
render_value(output, entity, true);
Mqtt::queue_publish(topic, output["value"].as<std::string>());
}
// publish to Mqtt
void WebEntityService::publish(const bool force) {
if (force) {
ha_registered_ = false;
}
if (!Mqtt::enabled()) {
return;
}
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
if (entityItems->size() == 0) {
return;
}
if (Mqtt::publish_single() && force) {
for (const EntityItem & entityItem : *entityItems) {
publish_single(entityItem);
}
}
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE);
JsonObject output = doc.to<JsonObject>();
for (const EntityItem & entityItem : *entityItems) {
render_value(output, entityItem);
// create HA config
if (Mqtt::ha_enabled() && !ha_registered_) {
StaticJsonDocument<EMSESP_JSON_SIZE_MEDIUM> config;
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/custom_data", Mqtt::base().c_str());
config["stat_t"] = stat_t;
char val_obj[50];
char val_cond[65];
snprintf(val_obj, sizeof(val_obj), "value_json['%s']", entityItem.name.c_str());
snprintf(val_cond, sizeof(val_cond), "%s is defined", val_obj);
config["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + "}}";
char uniq_s[70];
snprintf(uniq_s, sizeof(uniq_s), "custom_%s", entityItem.name.c_str());
config["obj_id"] = uniq_s;
config["uniq_id"] = uniq_s; // same as object_id
config["name"] = entityItem.name.c_str();
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "sensor/%s/custom_%s/config", Mqtt::basename().c_str(), entityItem.name.c_str());
//char command_topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
// snprintf(command_topic, sizeof(command_topic), "%s/custom/%s", Mqtt::basename().c_str(), entityItem.name.c_str());
// config["cmd_t"] = command_topic;
JsonObject dev = config.createNestedObject("dev");
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
// add "availability" section
Mqtt::add_avty_to_doc(stat_t, config.as<JsonObject>(), val_cond);
Mqtt::queue_ha(topic, config.as<JsonObject>());
ha_registered_ = true;
}
}
if (output.size() > 0) {
Mqtt::queue_publish("custom_data", output);
}
// EMSESP::logger().debug("publish %d custom entities", output.size());
}
// count only entities with valid value
uint8_t WebEntityService::count_entities() {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
if (entityItems->size() == 0) {
return 0;
}
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE);
JsonObject output = doc.to<JsonObject>();
for (const EntityItem & entity : *entityItems) {
render_value(output, entity);
}
return output.size();
}
// send to dashboard, msgpack don't like serialized, use number
void WebEntityService::generate_value_web(JsonObject & output) {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
output["label"] = (std::string) "Custom Entities";
JsonArray data = output.createNestedArray("data");
for (const EntityItem & entity : *entityItems) {
JsonObject obj = data.createNestedObject(); // create the object, we know there is a value
obj["id"] = "00" + entity.name;
obj["u"] = entity.uom;
obj["c"] = entity.name;
switch (entity.valuetype) {
case DeviceValueType::BOOL: {
char s[12];
obj["v"] = Helpers::render_boolean(s, (uint8_t)entity.val);
JsonArray l = obj.createNestedArray("l");
l.add(Helpers::render_boolean(s, false, true));
l.add(Helpers::render_boolean(s, true, true));
break;
}
case DeviceValueType::INT:
if ((int8_t)entity.val != EMS_VALUE_INT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (int8_t)entity.val, 0);
}
break;
case DeviceValueType::UINT:
if ((uint8_t)entity.val != EMS_VALUE_UINT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (uint8_t)entity.val, 0);
}
break;
case DeviceValueType::SHORT:
if ((int16_t)entity.val != EMS_VALUE_SHORT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (int16_t)entity.val, 0);
}
break;
case DeviceValueType::USHORT:
if ((uint16_t)entity.val != EMS_VALUE_USHORT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (uint16_t)entity.val, 0);
}
break;
case DeviceValueType::ULONG:
case DeviceValueType::TIME:
if (entity.val != EMS_VALUE_ULONG_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * entity.val, 0);
}
break;
default:
break;
}
}
}
// fetch telegram, called from emsesp::fetch
void WebEntityService::fetch() {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
for (auto & entity : *entityItems) {
EMSESP::send_read_request(entity.type_id, entity.device_id, entity.offset);
}
// EMSESP::logger().debug("fetch custom entities");
}
// called on process telegram, read from telegram
bool WebEntityService::get_value(std::shared_ptr<const Telegram> telegram) {
bool has_change = false;
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
// read-length of BOOL, INT, UINT, SHORT, USHORT, ULONG, TIME
const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3};
for (auto & entity : *entityItems) {
if (telegram->type_id == entity.type_id && telegram->src == entity.device_id && telegram->offset <= entity.offset
&& (telegram->offset + telegram->message_length) >= (entity.offset + len[entity.valuetype])) {
uint32_t val = 0;
for (uint8_t i = 0; i < len[entity.valuetype]; i++) {
val = (val << 8) + telegram->message_data[i + entity.offset - telegram->offset];
}
if (val != entity.val) {
entity.val = val;
if (Mqtt::publish_single()) {
publish_single(entity);
} else if (EMSESP::mqtt_.get_publish_onchange(0)) {
has_change = true;
}
}
// EMSESP::logger().debug("custom entity %s received with value %d", entity.name.c_str(), (int)entity.val);
break;
}
}
if (has_change) {
publish();
return true;
}
return false;
}
} // namespace emsesp

View File

@@ -0,0 +1,74 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2023 Paul Derbyshire
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "../telegram.h"
#ifndef WebEntityService_h
#define WebEntityService_h
#define EMSESP_ENTITY_FILE "/config/emsespEntity.json"
#define EMSESP_ENTITY_SERVICE_PATH "/rest/entity" // GET and POST
namespace emsesp {
class EntityItem {
public:
uint8_t device_id;
uint16_t type_id;
uint8_t offset;
int8_t valuetype;
uint8_t uom;
std::string name;
double factor;
uint32_t val;
};
class WebEntity {
public:
std::list<EntityItem> entityItems;
static void read(WebEntity & webEntity, JsonObject & root);
static StateUpdateResult update(JsonObject & root, WebEntity & webEntity);
};
class WebEntityService : public StatefulService<WebEntity> {
public:
WebEntityService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
void begin();
void publish_single(const EntityItem & entity);
void publish(const bool force = false);
bool command_setvalue(const char * value, const std::string name);
bool get_value_info(JsonObject & output, const char * cmd);
bool get_value(std::shared_ptr<const Telegram> telegram);
void fetch();
void render_value(JsonObject & output, EntityItem entity, const bool useVal = false);
uint8_t count_entities();
void generate_value_web(JsonObject & output);
private:
HttpEndpoint<WebEntity> _httpEndpoint;
FSPersistence<WebEntity> _fsPersistence;
std::list<EntityItem> * entityItems; // pointer to the list of schedule events
bool ha_registered_ = false;
};
} // namespace emsesp
#endif

View File

@@ -166,7 +166,7 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings)
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
String old_syslog_host = settings.syslog_host; String old_syslog_host = settings.syslog_host;
settings.syslog_host = root["syslog_host"] | EMSESP_DEFAULT_SYSLOG_HOST; settings.syslog_host = root["syslog_host"] | EMSESP_DEFAULT_SYSLOG_HOST;
if (!old_syslog_host.equals(settings.syslog_host)) { if (old_syslog_host != settings.syslog_host) {
add_flags(ChangeFlags::SYSLOG); add_flags(ChangeFlags::SYSLOG);
} }
#endif #endif
@@ -260,7 +260,7 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings)
String old_locale = settings.locale; String old_locale = settings.locale;
settings.locale = root["locale"] | EMSESP_DEFAULT_LOCALE; settings.locale = root["locale"] | EMSESP_DEFAULT_LOCALE;
EMSESP::system_.locale(settings.locale); EMSESP::system_.locale(settings.locale);
if (Mqtt::ha_enabled() && !old_locale.equals(settings.locale)) { if (Mqtt::ha_enabled() && old_locale != settings.locale) {
add_flags(ChangeFlags::MQTT); add_flags(ChangeFlags::MQTT);
} }