mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
add own entities read from ems-bus with free factor
This commit is contained in:
@@ -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()}{' '}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -245,6 +245,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);
|
||||||
}
|
}
|
||||||
|
|||||||
491
interface/src/project/SettingsEntities.tsx
Normal file
491
interface/src/project/SettingsEntities.tsx
Normal 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()} {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;
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -101,12 +101,47 @@ export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem:
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
|
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
|
||||||
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
|
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
|
||||||
if (name && o_name && o_name !== name && schedule.find((si) => si.name === name)) {
|
if (name && o_name && o_name !== name && schedule.find((si) => si.name === name)) {
|
||||||
callback('Name already in use');
|
callback('Name already in use');
|
||||||
} else {
|
} else {
|
||||||
callback();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ class EMSdevice {
|
|||||||
PUMP,
|
PUMP,
|
||||||
GENERIC,
|
GENERIC,
|
||||||
HEATSOURCE,
|
HEATSOURCE,
|
||||||
|
CUSTOM,
|
||||||
UNKNOWN
|
UNKNOWN
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, "Entites", root);
|
||||||
|
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace emsesp
|
} // namespace emsesp
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
429
src/web/WebEntityService.cpp
Normal file
429
src/web/WebEntityService.cpp
Normal 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
|
||||||
74
src/web/WebEntityService.h
Normal file
74
src/web/WebEntityService.h
Normal 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
|
||||||
Reference in New Issue
Block a user