mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
Merge branch 'emsesp:dev' into dev
This commit is contained in:
1
.github/workflows/tagged_release.yml
vendored
1
.github/workflows/tagged_release.yml
vendored
@@ -31,6 +31,7 @@ jobs:
|
||||
cd interface
|
||||
npm ci
|
||||
npx typesafe-i18n --no-watch
|
||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||
npm run build
|
||||
|
||||
- name: Build firmware
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
|
||||
# [3.5.0]
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES**
|
||||
|
||||
- When upgrading to v3.5 for the first time from v3.4 on a BBQKees Gateway board you will need to use the [EMS-EPS Flasher](https://github.com/emsesp/EMS-ESP-Flasher/releases) to correctly re-partition the flash. Make sure you backup the settings and customizations from the WebUI (System->Upload/Download) and restore after the upgrade.
|
||||
- Since 3.5.0b11 we have added support for multiple EMS-ESPs [#759] and also renamed the HA Entity IDs. For example what was previously `sensor.boiler_actual_boiler_temperature` is now using the shortname form `sensor.boiler_boiltemp` as opposed to the English description. Unfortunately this does means any HA dashboards, automation scripts and integrations (e.g. Grafana) need to be adjusted accordingly.
|
||||
|
||||
## Added
|
||||
|
||||
- Translations in Web UI and all device entity names (DE, NL, SE, PL, NO, ...) [#22](https://github.com/emsesp/EMS-ESP32/issues/22)
|
||||
- Translations in Web UI and all device entity names (DE, NL, SE, PL, NO) [#22](https://github.com/emsesp/EMS-ESP32/issues/22)
|
||||
- Add support for Lolin C3 mini [#620](https://github.com/emsesp/EMS-ESP32/pull/620)
|
||||
- Add support for ESP32-S2 [#667](https://github.com/emsesp/EMS-ESP32/pull/667)
|
||||
- Add devices: Greenstar 30Ri boiler, Junkers FW500 thermostat, Buderus BC30 controller
|
||||
@@ -17,8 +22,9 @@
|
||||
- Add min/max to customization table [#686](https://github.com/emsesp/EMS-ESP32/issues/686)
|
||||
- Add MD5 check [#637](https://github.com/emsesp/EMS-ESP32/issues/637)
|
||||
- Add more bus-ids [#673](https://github.com/emsesp/EMS-ESP32/issues/673)
|
||||
- Use HA connectivity device class for Status [#751](https://github.com/emsesp/EMS-ESP32/issues/751)
|
||||
- Use HA connectivity device class for Status, added boot time [#751](https://github.com/emsesp/EMS-ESP32/issues/751)
|
||||
- Add commands for analog sensors outputs
|
||||
- Support for multiple EMS-ESPs with MQTT and HA [[#759](https://github.com/emsesp/EMS-ESP32/issues/759)]
|
||||
|
||||
## Fixed
|
||||
|
||||
@@ -29,11 +35,8 @@
|
||||
- Discovery in HomeAssistant don't work with custom base topic. [#596](https://github.com/emsesp/EMS-ESP32/issues/596) Base topic containing `/` are changed to `_`
|
||||
- RF room temperature sensor are shown as thermostat
|
||||
- render mqtt float json values with trailing zero
|
||||
- removed flash strings
|
||||
- removed flash strings, to increase available heap memory
|
||||
- reload page after restart button is pressed
|
||||
- analog/dallas values command as list like ems-devices
|
||||
- analog/dallas HA-entities based on id
|
||||
|
||||
## **BREAKING CHANGES:**
|
||||
|
||||
- When upgrading from 3.4.x you may need to erase the flash on the ESP32 before uploading the firmware. Make sure you make a backup of the settings and customizations via the WebUI (System->Upload/Download)
|
||||
- MQTT Base is a mandatory field. Removed MQTT topic length from settings.
|
||||
|
||||
856
interface/package-lock.json
generated
856
interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,15 +8,15 @@
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@msgpack/msgpack": "^2.8.0",
|
||||
"@mui/icons-material": "^5.10.16",
|
||||
"@mui/material": "^5.10.16",
|
||||
"@mui/material": "^5.10.17",
|
||||
"@table-library/react-table-library": "4.0.23",
|
||||
"@types/lodash": "^4.14.190",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.12",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"async-validator": "^4.2.5",
|
||||
"axios": "^1.2.0",
|
||||
"axios": "^1.2.1",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -26,12 +26,12 @@
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-icons": "^4.6.0",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-router-dom": "^6.4.5",
|
||||
"react-scripts": "5.0.1",
|
||||
"sockette": "^2.0.6",
|
||||
"typesafe-i18n": "^5.17.1",
|
||||
"typescript": "^4.9.3"
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-app-rewired start",
|
||||
|
||||
@@ -220,6 +220,19 @@ const MqttSettingsForm: FC = () => {
|
||||
/>
|
||||
</Grid>
|
||||
{data.ha_enabled && (
|
||||
<>
|
||||
<Grid item>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="multiple_instances"
|
||||
checked={data.multiple_instances}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_MULTIPLE_INSTANCES()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item sx={{ pb: 3 }}>
|
||||
<ValidatedTextField
|
||||
name="discovery_prefix"
|
||||
@@ -231,6 +244,7 @@ const MqttSettingsForm: FC = () => {
|
||||
size="small"
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
@@ -238,6 +252,22 @@ const MqttSettingsForm: FC = () => {
|
||||
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
||||
</Typography>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="publish_time_heartbeat"
|
||||
label={LL.MQTT_INT_HEARTBEAT()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_heartbeat)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC, useContext, useEffect, useState } from 'react';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@@ -18,6 +19,7 @@ import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
@@ -25,11 +27,13 @@ import {
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
ValidatedTextField,
|
||||
MessageBox
|
||||
} from '../../components';
|
||||
import { NetworkSettings } from '../../types';
|
||||
import * as NetworkApi from '../../api/network';
|
||||
import { numberValue, updateValue, useRest } from '../../utils';
|
||||
import * as EMSESP from '../../project/api';
|
||||
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
||||
@@ -38,14 +42,17 @@ import { validate } from '../../validators';
|
||||
import { createNetworkSettingsValidator } from '../../validators/network';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
import RestartMonitor from '../system/RestartMonitor';
|
||||
|
||||
const WiFiSettingsForm: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
|
||||
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<NetworkSettings>({
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
const { loadData, saving, data, setData, saveData, errorMessage, restartNeeded } = useRest<NetworkSettings>({
|
||||
read: NetworkApi.readNetworkSettings,
|
||||
update: NetworkApi.updateNetworkSettings
|
||||
});
|
||||
@@ -62,7 +69,9 @@ const WiFiSettingsForm: FC = () => {
|
||||
bandwidth20: false,
|
||||
tx_power: 20,
|
||||
nosleep: false,
|
||||
enableMDNS: true
|
||||
enableMDNS: true,
|
||||
enableCORS: false,
|
||||
CORSOrigin: '*'
|
||||
});
|
||||
}
|
||||
setInitialized(true);
|
||||
@@ -90,6 +99,15 @@ const WiFiSettingsForm: FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const restart = async () => {
|
||||
try {
|
||||
await EMSESP.restart();
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
@@ -162,11 +180,6 @@ const WiFiSettingsForm: FC = () => {
|
||||
label={LL.NETWORK_LOW_BAND()}
|
||||
/>
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
|
||||
label={LL.NETWORK_USE_DNS()}
|
||||
/>
|
||||
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.GENERAL_OPTIONS()}
|
||||
</Typography>
|
||||
@@ -182,6 +195,28 @@ const WiFiSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
|
||||
label={LL.NETWORK_USE_DNS()}
|
||||
/>
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableCORS" checked={data.enableCORS} onChange={updateFormValue} />}
|
||||
label={LL.NETWORK_ENABLE_CORS()}
|
||||
/>
|
||||
{data.enableCORS && (
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="CORSOrigin"
|
||||
label={LL.NETWORK_CORS_ORIGIN()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.CORSOrigin}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
)}
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableIPv6" checked={data.enableIPv6} onChange={updateFormValue} />}
|
||||
label={LL.NETWORK_ENABLE_IPV6()}
|
||||
@@ -245,6 +280,14 @@ const WiFiSettingsForm: FC = () => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{restartNeeded && (
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
|
||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</MessageBox>
|
||||
)}
|
||||
{!restartNeeded && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
@@ -257,13 +300,14 @@ const WiFiSettingsForm: FC = () => {
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
||||
{content()}
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Translation } from '../i18n-types';
|
||||
/* eslint-disable */
|
||||
|
||||
const de: Translation = {
|
||||
// ...en as Translation,
|
||||
LANGUAGE: 'Sprache',
|
||||
RETRY: 'Neuer Versuch',
|
||||
LOADING: 'Laden',
|
||||
@@ -37,7 +38,7 @@ const de: Translation = {
|
||||
PRODUCT: 'Produkt',
|
||||
VERSION: 'Version',
|
||||
ENTITY_NAME: 'Entitätsname',
|
||||
VALUE: 'Wert',
|
||||
VALUE: '{{Wert|wert}}',
|
||||
SHOW_FAV: 'nur Favoriten anzeigen',
|
||||
DEVICE_SENSOR_DATA: 'Geräte- und Sensordaten',
|
||||
DEVICES_SENSORS: 'Geräte & Sensoren',
|
||||
@@ -190,8 +191,8 @@ const de: Translation = {
|
||||
SYSTEM_FACTORY_TEXT_DIALOG: 'Sind Sie sicher alle Einstellungen auf Werkseinstellung zu setzen?',
|
||||
VERSION_CHECK: 'Versionsprüfung',
|
||||
THE_LATEST: 'Die neueste',
|
||||
OFFICIAL: 'official',
|
||||
DEVELOPMENT: 'development',
|
||||
OFFICIAL: 'offizielle',
|
||||
DEVELOPMENT: 'Entwicklungs',
|
||||
VERSION_IS: 'Version ist',
|
||||
RELEASE_NOTES: 'Versionshinweise',
|
||||
EMS_ESP_VER: 'EMS-ESP Version',
|
||||
@@ -245,8 +246,10 @@ const de: Translation = {
|
||||
MQTT_INT_THERMOSTATS: 'Thermostate',
|
||||
MQTT_INT_SOLAR: 'Solarmodule',
|
||||
MQTT_INT_MIXER: 'Mischermodule',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'MQTT Queue',
|
||||
DEFAULT: 'Standard',
|
||||
MQTT_MULTIPLE_INSTANCES: 'Erlaube EMS-ESP Mehrfachinstanzen',
|
||||
MQTT_CLEAN_SESSION: 'Setze `Clean Session`',
|
||||
MQTT_RETAIN_FLAG: 'Setze `Retain flag` immer',
|
||||
INACTIVE: 'Inaktiv',
|
||||
@@ -282,6 +285,8 @@ const de: Translation = {
|
||||
NETWORK_DISABLE_SLEEP: 'Deaktiviere WiFi Schlafmodus',
|
||||
NETWORK_LOW_BAND: 'Verwende niedrige WiFi Bandbreite',
|
||||
NETWORK_USE_DNS: 'Aktiviere mDNS Service',
|
||||
NETWORK_ENABLE_CORS: 'Aktiviere CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Aktiviere IPv6 Unterstützung',
|
||||
NETWORK_FIXED_IP: 'Feste IP Adresse',
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
|
||||
@@ -37,7 +37,7 @@ const en: Translation = {
|
||||
VERSION: 'Version',
|
||||
BRAND: 'Brand',
|
||||
ENTITY_NAME: 'Entity Name',
|
||||
VALUE: 'Value',
|
||||
VALUE: '{{Value|value}}',
|
||||
SHOW_FAV: 'only show favorites',
|
||||
DEVICE_SENSOR_DATA: 'Device and Sensor Data',
|
||||
DEVICES_SENSORS: 'Devices & Sensors',
|
||||
@@ -245,8 +245,10 @@ const en: Translation = {
|
||||
MQTT_INT_THERMOSTATS: 'Thermostats',
|
||||
MQTT_INT_SOLAR: 'Solar Modules',
|
||||
MQTT_INT_MIXER: 'Mixer Modules',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'MQTT Queue',
|
||||
DEFAULT: 'Default',
|
||||
MQTT_MULTIPLE_INSTANCES: 'Enable Multiple Instances of EMS-ESP',
|
||||
MQTT_CLEAN_SESSION: 'Set Clean Session',
|
||||
MQTT_RETAIN_FLAG: 'Always set Retain flag',
|
||||
INACTIVE: 'Inactive',
|
||||
@@ -282,6 +284,8 @@ const en: Translation = {
|
||||
NETWORK_DISABLE_SLEEP: 'Disable WiFi Sleep Mode',
|
||||
NETWORK_LOW_BAND: 'Use Lower WiFi Bandwidth',
|
||||
NETWORK_USE_DNS: 'Enable mDNS Service',
|
||||
NETWORK_ENABLE_CORS: 'Enable CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Enable IPv6 support',
|
||||
NETWORK_FIXED_IP: 'Use Fixed IP address',
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
|
||||
@@ -100,8 +100,8 @@ const nl: Translation = {
|
||||
NUM_ANALOG_SENSORS: '{num} Analoge sensor{{en}}',
|
||||
NUM_DAYS: '{num} dag{{en}}',
|
||||
NUM_SECONDS: '{num} second{{en}}',
|
||||
NUM_HOURS: '{num} uur{{en}}',
|
||||
NUM_MINUTES: '{num} minuut{{en}}',
|
||||
NUM_HOURS: '{num} {{uur|uren}}',
|
||||
NUM_MINUTES: '{num} {{minuut|minuten}}',
|
||||
APPLICATION_SETTINGS: 'Applicatieinstellingen',
|
||||
CUSTOMIZATION: 'Custom aanpassingen',
|
||||
APPLICATION_RESTARTING: 'EMS-ESP herstarten',
|
||||
@@ -245,8 +245,10 @@ const nl: Translation = {
|
||||
MQTT_INT_THERMOSTATS: 'Thermostaten',
|
||||
MQTT_INT_SOLAR: 'Solar Modules',
|
||||
MQTT_INT_MIXER: 'Mixer Modules',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'MQTT Queue',
|
||||
DEFAULT: 'Default',
|
||||
MQTT_MULTIPLE_INSTANCES: 'Enable Multiple Instances of EMS-ESP',
|
||||
MQTT_CLEAN_SESSION: 'Clean Session aan',
|
||||
MQTT_RETAIN_FLAG: 'Retain flag aan',
|
||||
INACTIVE: 'Inactief',
|
||||
@@ -282,6 +284,8 @@ const nl: Translation = {
|
||||
NETWORK_DISABLE_SLEEP: 'WiFi Sleep Mode uitzetten',
|
||||
NETWORK_LOW_BAND: 'Lagere WiFi bandbreedte gebruiken',
|
||||
NETWORK_USE_DNS: 'Activeer mDNS Service',
|
||||
NETWORK_ENABLE_CORS: 'Activeer CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Activeer IPv6 support',
|
||||
NETWORK_FIXED_IP: 'Gebruik vast IP addres',
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
|
||||
@@ -245,8 +245,10 @@ const no: Translation = {
|
||||
MQTT_INT_THERMOSTATS: 'Termostat',
|
||||
MQTT_INT_SOLAR: 'Solpaneler',
|
||||
MQTT_INT_MIXER: 'Blandeventil',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'MQTT Queue',
|
||||
DEFAULT: 'Standard',
|
||||
MQTT_MULTIPLE_INSTANCES: 'Enable Multiple Instances of EMS-ESP',
|
||||
MQTT_CLEAN_SESSION: 'Benytt Clean Session',
|
||||
MQTT_RETAIN_FLAG: 'Alltid sett Retain flag',
|
||||
INACTIVE: 'Innaktiv',
|
||||
@@ -282,6 +284,8 @@ const no: Translation = {
|
||||
NETWORK_DISABLE_SLEEP: 'Hindre at trådløst nettverk går i Sleep Mode',
|
||||
NETWORK_LOW_BAND: 'Benytt smalere båndbredde på trådløst nettverk',
|
||||
NETWORK_USE_DNS: 'Aktiviser mDNS Service',
|
||||
NETWORK_ENABLE_CORS: 'Aktiviser CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Aktiviser IPv6 støtte',
|
||||
NETWORK_FIXED_IP: 'Benytt statisk IP adresse',
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
|
||||
@@ -245,8 +245,10 @@ const pl: BaseTranslation = {
|
||||
MQTT_INT_THERMOSTATS: 'Termostaty',
|
||||
MQTT_INT_SOLAR: 'Panele solarne',
|
||||
MQTT_INT_MIXER: 'Mieszacze',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'Kolejka MQTT',
|
||||
DEFAULT: '{{Pozostałe|Domyślna|}}',
|
||||
MQTT_MULTIPLE_INSTANCES: 'Enable Multiple Instances of EMS-ESP',
|
||||
MQTT_CLEAN_SESSION: 'Ustawiaj flagę "Clean session"',
|
||||
MQTT_RETAIN_FLAG: 'Ustawiaj flagę "Retain"',
|
||||
INACTIVE: 'nieaktywny',
|
||||
@@ -282,6 +284,8 @@ const pl: BaseTranslation = {
|
||||
NETWORK_DISABLE_SLEEP: 'Wyłącz tryb usypiania WiFi',
|
||||
NETWORK_LOW_BAND: 'Używaj zmniejszonej przepustowości WiFi',
|
||||
NETWORK_USE_DNS: 'Włącz wsparcie dla mDNS',
|
||||
NETWORK_ENABLE_CORS: 'Włącz wsparcie dla CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Włącz wsparcie dla IPv6',
|
||||
NETWORK_FIXED_IP: 'Użyj stałego adresu IP',
|
||||
NETWORK_GATEWAY: 'Brama',
|
||||
|
||||
@@ -245,8 +245,10 @@ const se: Translation = {
|
||||
MQTT_INT_THERMOSTATS: 'Termostater',
|
||||
MQTT_INT_SOLAR: 'Solpaneler',
|
||||
MQTT_INT_MIXER: 'Blandarventiler',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'MQTT Queue',
|
||||
DEFAULT: 'Standard',
|
||||
MQTT_MULTIPLE_INSTANCES: 'Enable Multiple Instances of EMS-ESP',
|
||||
MQTT_CLEAN_SESSION: 'Använd "Clean Session"-flaggan',
|
||||
MQTT_RETAIN_FLAG: 'Använd "Always Retain"-flaggan',
|
||||
INACTIVE: 'Inaktiv',
|
||||
@@ -282,6 +284,8 @@ const se: Translation = {
|
||||
NETWORK_DISABLE_SLEEP: 'Inaktivera sömnläge',
|
||||
NETWORK_LOW_BAND: 'Använd lägre bandbredd',
|
||||
NETWORK_USE_DNS: 'Aktivera mDNS-tjänsten',
|
||||
NETWORK_ENABLE_CORS: 'Aktivera CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Aktivera IPv6-support',
|
||||
NETWORK_FIXED_IP: 'Använd statiskt IP',
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
|
||||
@@ -72,11 +72,11 @@ export const createSettingsValidator = (settings: Settings) =>
|
||||
syslog_host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
|
||||
syslog_port: [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Port must be between 0 and 65535' }
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
|
||||
],
|
||||
syslog_mark_interval: [
|
||||
{ required: true, message: 'Mark interval is required' },
|
||||
{ type: 'number', min: 0, max: 10, message: 'Port must be between 0 and 10' }
|
||||
{ type: 'number', min: 0, max: 10, message: ' must be between 0 and 10' }
|
||||
]
|
||||
}),
|
||||
...(settings.shower_alert && {
|
||||
|
||||
@@ -29,13 +29,14 @@ export interface MqttSettings {
|
||||
client_id: string;
|
||||
keep_alive: number;
|
||||
clean_session: boolean;
|
||||
max_topic_length: number;
|
||||
multiple_instances: boolean;
|
||||
publish_time_boiler: number;
|
||||
publish_time_thermostat: number;
|
||||
publish_time_solar: number;
|
||||
publish_time_mixer: number;
|
||||
publish_time_other: number;
|
||||
publish_time_sensor: number;
|
||||
publish_time_heartbeat: number;
|
||||
mqtt_qos: number;
|
||||
mqtt_retain: boolean;
|
||||
ha_enabled: boolean;
|
||||
|
||||
@@ -48,6 +48,8 @@ export interface NetworkSettings {
|
||||
dns_ip_1?: string;
|
||||
dns_ip_2?: string;
|
||||
enableMDNS: boolean;
|
||||
enableCORS: boolean;
|
||||
CORSOrigin: string;
|
||||
}
|
||||
|
||||
export interface WiFiNetworkList {
|
||||
|
||||
@@ -3,16 +3,17 @@ import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
|
||||
|
||||
export const MQTT_SETTINGS_VALIDATOR = new Schema({
|
||||
host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
|
||||
base: { required: true, message: 'Base is required' },
|
||||
port: [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Port must be between 0 and 65535' }
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
|
||||
],
|
||||
keep_alive: [
|
||||
{ required: true, message: 'Keep alive is required' },
|
||||
{ type: 'number', min: 1, max: 86400, message: 'Keep alive must be between 1 and 86400' }
|
||||
],
|
||||
max_topic_length: [
|
||||
{ required: true, message: 'Max topic length is required' },
|
||||
{ type: 'number', min: 16, max: 1024, message: 'Max topic length must be between 16 and 1024' }
|
||||
publish_time_heartbeat: [
|
||||
{ required: true, message: 'Heartbeat is required' },
|
||||
{ type: 'number', min: 10, max: 86400, message: 'Heartbeat must be between 10 and 86400' }
|
||||
]
|
||||
});
|
||||
|
||||
@@ -46,6 +46,13 @@ ESP8266React::ESP8266React(AsyncWebServer * server, FS * fs)
|
||||
|
||||
void ESP8266React::begin() {
|
||||
_networkSettingsService.begin();
|
||||
_networkSettingsService.read([&](NetworkSettings & networkSettings) {
|
||||
if (networkSettings.enableCORS) {
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", networkSettings.CORSOrigin);
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
});
|
||||
_apSettingsService.begin();
|
||||
_ntpSettingsService.begin();
|
||||
_otaSettingsService.begin();
|
||||
|
||||
@@ -137,7 +137,7 @@ void MqttSettingsService::configureMqtt() {
|
||||
_mqttClient.setClientId(retainCstr(_state.clientId.c_str(), &_retainedClientId));
|
||||
_mqttClient.setKeepAlive(_state.keepAlive);
|
||||
_mqttClient.setCleanSession(_state.cleanSession);
|
||||
_mqttClient.setMaxTopicLength(_state.maxTopicLength);
|
||||
_mqttClient.setMaxTopicLength(FACTORY_MQTT_MAX_TOPIC_LENGTH); // hardcode. We don't take this from the settings anymore.
|
||||
_mqttClient.connect();
|
||||
// } else {
|
||||
// emsesp::EMSESP::logger().info("Error configuring Mqtt client");
|
||||
@@ -154,7 +154,7 @@ void MqttSettings::read(MqttSettings & settings, JsonObject & root) {
|
||||
root["client_id"] = settings.clientId;
|
||||
root["keep_alive"] = settings.keepAlive;
|
||||
root["clean_session"] = settings.cleanSession;
|
||||
root["max_topic_length"] = settings.maxTopicLength;
|
||||
root["multiple_instances"] = settings.multiple_instances;
|
||||
|
||||
// added by proddy for EMS-ESP
|
||||
root["publish_time_boiler"] = settings.publish_time_boiler;
|
||||
@@ -163,6 +163,7 @@ void MqttSettings::read(MqttSettings & settings, JsonObject & root) {
|
||||
root["publish_time_mixer"] = settings.publish_time_mixer;
|
||||
root["publish_time_other"] = settings.publish_time_other;
|
||||
root["publish_time_sensor"] = settings.publish_time_sensor;
|
||||
root["publish_time_heartbeat"] = settings.publish_time_heartbeat;
|
||||
root["mqtt_qos"] = settings.mqtt_qos;
|
||||
root["mqtt_retain"] = settings.mqtt_retain;
|
||||
root["ha_enabled"] = settings.ha_enabled;
|
||||
@@ -186,7 +187,8 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting
|
||||
newSettings.clientId = root["client_id"] | FACTORY_MQTT_CLIENT_ID;
|
||||
newSettings.keepAlive = root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE;
|
||||
newSettings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION;
|
||||
newSettings.maxTopicLength = root["max_topic_length"] | FACTORY_MQTT_MAX_TOPIC_LENGTH;
|
||||
newSettings.multiple_instances = root["multiple_instances"] | FACTORY_MQTT_MULTIPLE_INSTANCES;
|
||||
|
||||
newSettings.mqtt_qos = root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS;
|
||||
newSettings.mqtt_retain = root["mqtt_retain"] | EMSESP_DEFAULT_MQTT_RETAIN;
|
||||
|
||||
@@ -196,6 +198,7 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting
|
||||
newSettings.publish_time_mixer = root["publish_time_mixer"] | EMSESP_DEFAULT_PUBLISH_TIME;
|
||||
newSettings.publish_time_other = root["publish_time_other"] | EMSESP_DEFAULT_PUBLISH_TIME;
|
||||
newSettings.publish_time_sensor = root["publish_time_sensor"] | EMSESP_DEFAULT_PUBLISH_TIME;
|
||||
newSettings.publish_time_heartbeat = root["publish_time_heartbeat"] | EMSESP_DEFAULT_PUBLISH_HEARTBEAT;
|
||||
|
||||
newSettings.ha_enabled = root["ha_enabled"] | EMSESP_DEFAULT_HA_ENABLED;
|
||||
newSettings.nested_format = root["nested_format"] | EMSESP_DEFAULT_NESTED_FORMAT;
|
||||
@@ -284,6 +287,11 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (newSettings.publish_time_heartbeat != settings.publish_time_heartbeat) {
|
||||
emsesp::EMSESP::mqtt_.set_publish_time_heartbeat(newSettings.publish_time_heartbeat);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
emsesp::EMSESP::mqtt_.reset_mqtt();
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ static String generateClientId() {
|
||||
#define FACTORY_MQTT_MAX_TOPIC_LENGTH 128
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_MQTT_MULTIPLE_INSTANCES
|
||||
#define FACTORY_MQTT_MULTIPLE_INSTANCES false
|
||||
#endif
|
||||
|
||||
class MqttSettings {
|
||||
public:
|
||||
// host and port - if enabled
|
||||
@@ -74,7 +78,9 @@ class MqttSettings {
|
||||
// connection settings
|
||||
uint16_t keepAlive;
|
||||
bool cleanSession;
|
||||
uint16_t maxTopicLength;
|
||||
|
||||
// multiple instances
|
||||
bool multiple_instances;
|
||||
|
||||
// proddy EMS-ESP specific
|
||||
String base;
|
||||
@@ -84,6 +90,7 @@ class MqttSettings {
|
||||
uint16_t publish_time_mixer;
|
||||
uint16_t publish_time_other;
|
||||
uint16_t publish_time_sensor;
|
||||
uint16_t publish_time_heartbeat;
|
||||
uint8_t mqtt_qos;
|
||||
bool mqtt_retain;
|
||||
bool ha_enabled;
|
||||
|
||||
@@ -83,5 +83,5 @@ void NTPSettingsService::configureTime(AsyncWebServerRequest * request, JsonVari
|
||||
void NTPSettingsService::ntp_received(struct timeval * tv) {
|
||||
// emsesp::EMSESP::logger().info("NTP sync to %d sec", tv->tv_sec);
|
||||
emsesp::EMSESP::system_.ntp_connected(true);
|
||||
emsesp::EMSESP::system_.send_info_mqtt("connected");
|
||||
emsesp::EMSESP::system_.send_info_mqtt("connected", true); // send info topic with NTP time
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ class NetworkSettings {
|
||||
int8_t tx_power;
|
||||
bool nosleep;
|
||||
bool enableMDNS;
|
||||
bool enableCORS;
|
||||
String CORSOrigin;
|
||||
|
||||
// optional configuration for static IP address
|
||||
IPAddress localIP;
|
||||
@@ -58,6 +60,8 @@ class NetworkSettings {
|
||||
root["tx_power"] = settings.tx_power;
|
||||
root["nosleep"] = settings.nosleep;
|
||||
root["enableMDNS"] = settings.enableMDNS;
|
||||
root["enableCORS"] = settings.enableCORS;
|
||||
root["CORSOrigin"] = settings.CORSOrigin;
|
||||
|
||||
// extended settings
|
||||
JsonUtils::writeIP(root, "local_ip", settings.localIP);
|
||||
@@ -68,6 +72,8 @@ class NetworkSettings {
|
||||
}
|
||||
|
||||
static StateUpdateResult update(JsonObject & root, NetworkSettings & settings) {
|
||||
auto enableCORS = settings.enableCORS;
|
||||
auto CORSOrigin = settings.CORSOrigin;
|
||||
settings.ssid = root["ssid"] | FACTORY_WIFI_SSID;
|
||||
settings.password = root["password"] | FACTORY_WIFI_PASSWORD;
|
||||
settings.hostname = root["hostname"] | FACTORY_WIFI_HOSTNAME;
|
||||
@@ -77,6 +83,8 @@ class NetworkSettings {
|
||||
settings.tx_power = root["tx_power"] | 20;
|
||||
settings.nosleep = root["nosleep"] | false;
|
||||
settings.enableMDNS = root["enableMDNS"] | true;
|
||||
settings.enableCORS = root["enableCORS"] | false;
|
||||
settings.CORSOrigin = root["CORSOrigin"] | "*";
|
||||
|
||||
// extended settings
|
||||
JsonUtils::readIP(root, "local_ip", settings.localIP);
|
||||
@@ -97,6 +105,9 @@ class NetworkSettings {
|
||||
if (settings.staticIPConfig && (IPUtils::isNotSet(settings.localIP) || IPUtils::isNotSet(settings.gatewayIP) || IPUtils::isNotSet(settings.subnetMask))) {
|
||||
settings.staticIPConfig = false;
|
||||
}
|
||||
if (enableCORS != settings.enableCORS || CORSOrigin != settings.CORSOrigin) {
|
||||
return StateUpdateResult::CHANGED_RESTART; // tell WebUI that a restart is needed
|
||||
}
|
||||
|
||||
return StateUpdateResult::CHANGED;
|
||||
}
|
||||
|
||||
@@ -29,9 +29,12 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
|
||||
std::string extension = fname.substr(position + 1);
|
||||
size_t fsize = request->contentLength();
|
||||
|
||||
#ifdef EMSESP_DEBUG
|
||||
#if defined(EMSESP_USE_SERIAL)
|
||||
Serial.println();
|
||||
Serial.printf("Received filename: %s, len: %d, index: %d, ext: %s, fsize: %d", filename.c_str(), len, index, extension.c_str(), fsize);
|
||||
Serial.println();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
is_firmware = false;
|
||||
|
||||
@@ -74,13 +74,14 @@ class DummySettings {
|
||||
String base = "ems-esp";
|
||||
bool publish_single = false;
|
||||
bool publish_single2cmd = false;
|
||||
bool send_response = true;
|
||||
bool send_response = false; // don't send response
|
||||
String host = "192.168.1.4";
|
||||
uint16_t port = 1883;
|
||||
String clientId = "ems-esp";
|
||||
String username = "";
|
||||
uint16_t keepAlive = 60;
|
||||
bool cleanSession = false;
|
||||
bool multiple_instances = false;
|
||||
|
||||
uint16_t publish_time_boiler = 10;
|
||||
uint16_t publish_time_thermostat = 10;
|
||||
@@ -88,6 +89,7 @@ class DummySettings {
|
||||
uint16_t publish_time_mixer = 10;
|
||||
uint16_t publish_time_other = 10;
|
||||
uint16_t publish_time_sensor = 10;
|
||||
uint16_t publish_time_heartbeat = 60;
|
||||
|
||||
String hostname = "ems-esp";
|
||||
String jwtSecret = "ems-esp";
|
||||
@@ -101,6 +103,8 @@ class DummySettings {
|
||||
String dnsIP2 = "";
|
||||
bool enableIPv6 = false;
|
||||
bool enableMDNS = true;
|
||||
bool enableCORS = false;
|
||||
String CORSOrigin = "*";
|
||||
|
||||
uint8_t phy_type = 0;
|
||||
uint8_t eth_power = 0; // 0 means -1
|
||||
|
||||
@@ -122,7 +122,11 @@ network_settings = {
|
||||
hostname: 'ems-esp',
|
||||
nosleep: true,
|
||||
tx_power: 20,
|
||||
bandwidth20: false,
|
||||
static_ip_config: false,
|
||||
enableMDNS: true,
|
||||
enableCORS: false,
|
||||
CORSOrigin: '*',
|
||||
}
|
||||
const network_status = {
|
||||
status: 3,
|
||||
@@ -219,13 +223,14 @@ mqtt_settings = {
|
||||
client_id: 'ems-esp',
|
||||
keep_alive: 60,
|
||||
clean_session: true,
|
||||
max_topic_length: 128,
|
||||
multiple_instances: false,
|
||||
publish_time_boiler: 10,
|
||||
publish_time_thermostat: 10,
|
||||
publish_time_solar: 10,
|
||||
publish_time_mixer: 10,
|
||||
publish_time_other: 10,
|
||||
publish_time_sensor: 10,
|
||||
publish_time_heartbeat: 60,
|
||||
mqtt_qos: 0,
|
||||
mqtt_retain: false,
|
||||
ha_enabled: true,
|
||||
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "EMS-ESP32",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -296,7 +296,7 @@ void AnalogSensor::loop() {
|
||||
}
|
||||
|
||||
// update analog information name and offset
|
||||
bool AnalogSensor::update(uint8_t gpio, const std::string & name, float offset, float factor, uint8_t uom, int8_t type) {
|
||||
bool AnalogSensor::update(uint8_t gpio, const std::string & name, double offset, double factor, uint8_t uom, int8_t type) {
|
||||
boolean found_sensor = false; // see if we can find the sensor in our customization list
|
||||
|
||||
EMSESP::webCustomizationService.update(
|
||||
@@ -375,7 +375,7 @@ void AnalogSensor::publish_sensor(const Sensor & sensor) const {
|
||||
snprintf(topic, sizeof(topic), "%s%s/%s", F_(analogsensor), "_data", sensor.name().c_str());
|
||||
}
|
||||
char payload[10];
|
||||
Mqtt::publish(topic, Helpers::render_value(payload, sensor.value(), 2)); // always publish as floats
|
||||
Mqtt::publish(topic, Helpers::render_value(payload, sensor.value(), 2)); // always publish as doubles
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ void AnalogSensor::publish_values(const bool force) {
|
||||
case AnalogType::PWM_0:
|
||||
case AnalogType::PWM_1:
|
||||
case AnalogType::PWM_2:
|
||||
dataSensor["value"] = serialized(Helpers::render_value(s, sensor.value(), 2)); // float
|
||||
dataSensor["value"] = serialized(Helpers::render_value(s, sensor.value(), 2)); // double
|
||||
break;
|
||||
default:
|
||||
dataSensor["value"] = (uint8_t)sensor.value(); // convert to char for 1 or 0
|
||||
@@ -440,7 +440,7 @@ void AnalogSensor::publish_values(const bool force) {
|
||||
StaticJsonDocument<EMSESP_JSON_SIZE_MEDIUM> config;
|
||||
|
||||
char stat_t[50];
|
||||
snprintf(stat_t, sizeof(stat_t), "%s/analogsensor_data", Mqtt::base().c_str());
|
||||
snprintf(stat_t, sizeof(stat_t), "%s/analogsensor_data", Mqtt::base().c_str()); // use base path
|
||||
config["stat_t"] = stat_t;
|
||||
|
||||
char str[50];
|
||||
@@ -451,16 +451,18 @@ void AnalogSensor::publish_values(const bool force) {
|
||||
}
|
||||
config["val_tpl"] = str;
|
||||
|
||||
// snprintf(str, sizeof(str), "%s_analog_sensor_%s", Mqtt::basename().c_str(), sensor.name().c_str());
|
||||
snprintf(str, sizeof(str), "analog_sensor_%d", sensor.gpio());
|
||||
if (Mqtt::multiple_instances()) {
|
||||
snprintf(str, sizeof(str), "%s_analogsensor_%d", Mqtt::basename().c_str(), sensor.gpio());
|
||||
} else {
|
||||
snprintf(str, sizeof(str), "analogsensor_%d", sensor.gpio());
|
||||
}
|
||||
|
||||
config["object_id"] = str;
|
||||
config["uniq_id"] = str; // same as object_id
|
||||
|
||||
snprintf(str, sizeof(str), "%s", sensor.name().c_str());
|
||||
config["name"] = str;
|
||||
|
||||
snprintf(str, sizeof(str), "analogsensor_%d", sensor.gpio());
|
||||
config["uniq_id"] = str;
|
||||
|
||||
if (sensor.uom() != DeviceValueUOM::NONE) {
|
||||
config["unit_of_meas"] = EMSdevice::uom_to_string(sensor.uom());
|
||||
}
|
||||
@@ -584,7 +586,7 @@ bool AnalogSensor::command_info(const char * value, const int8_t id, JsonObject
|
||||
}
|
||||
|
||||
// this creates the sensor, initializing everything
|
||||
AnalogSensor::Sensor::Sensor(const uint8_t gpio, const std::string & name, const float offset, const float factor, const uint8_t uom, const int8_t type)
|
||||
AnalogSensor::Sensor::Sensor(const uint8_t gpio, const std::string & name, const double offset, const double factor, const uint8_t uom, const int8_t type)
|
||||
: gpio_(gpio)
|
||||
, name_(name)
|
||||
, offset_(offset)
|
||||
|
||||
@@ -35,10 +35,10 @@ class AnalogSensor {
|
||||
public:
|
||||
class Sensor {
|
||||
public:
|
||||
Sensor(const uint8_t gpio, const std::string & name, const float offset, const float factor, const uint8_t uom, const int8_t type);
|
||||
Sensor(const uint8_t gpio, const std::string & name, const double offset, const double factor, const uint8_t uom, const int8_t type);
|
||||
~Sensor() = default;
|
||||
|
||||
void set_offset(const float offset) {
|
||||
void set_offset(const double offset) {
|
||||
offset_ = offset;
|
||||
}
|
||||
|
||||
@@ -52,23 +52,23 @@ class AnalogSensor {
|
||||
return gpio_;
|
||||
}
|
||||
|
||||
float value() const {
|
||||
double value() const {
|
||||
return value_;
|
||||
}
|
||||
|
||||
void set_value(float value) {
|
||||
void set_value(const double value) {
|
||||
value_ = value;
|
||||
}
|
||||
|
||||
float factor() const {
|
||||
double factor() const {
|
||||
return factor_;
|
||||
}
|
||||
|
||||
void set_factor(float factor) {
|
||||
void set_factor(const double factor) {
|
||||
factor_ = factor;
|
||||
}
|
||||
|
||||
float offset() const {
|
||||
double offset() const {
|
||||
return offset_;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class AnalogSensor {
|
||||
return type_;
|
||||
}
|
||||
|
||||
void set_type(int8_t type) {
|
||||
void set_type(const int8_t type) {
|
||||
type_ = type;
|
||||
}
|
||||
|
||||
@@ -101,10 +101,10 @@ class AnalogSensor {
|
||||
private:
|
||||
uint8_t gpio_;
|
||||
std::string name_;
|
||||
float offset_;
|
||||
float factor_;
|
||||
double offset_;
|
||||
double factor_;
|
||||
uint8_t uom_;
|
||||
float value_; // float because of the factor is a float
|
||||
double value_; // double because of the factor is a double
|
||||
int8_t type_;
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@ class AnalogSensor {
|
||||
return sensors_.size();
|
||||
}
|
||||
|
||||
bool update(uint8_t gpio, const std::string & name, float offset, float factor, uint8_t uom, int8_t type);
|
||||
bool update(uint8_t gpio, const std::string & name, double offset, double factor, uint8_t uom, int8_t type);
|
||||
bool get_value_info(JsonObject & output, const char * cmd, const int8_t id) const;
|
||||
|
||||
#ifdef EMSESP_DEBUG
|
||||
|
||||
@@ -52,8 +52,10 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef EMSESP_DEBUG
|
||||
#if defined(EMSESP_USE_SERIAL)
|
||||
// Serial.println(p.path().c_str()); // dump paths, for debugging
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// re-calculate new path
|
||||
@@ -179,6 +181,8 @@ std::string Command::return_code_string(const uint8_t return_code) {
|
||||
return "Not Authorized";
|
||||
case CommandRet::FAIL:
|
||||
return "Failed";
|
||||
case CommandRet::INVALID:
|
||||
return "Invalid";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -270,18 +274,22 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char *
|
||||
}
|
||||
|
||||
auto description = Helpers::translated_word(cf->description_);
|
||||
char info_s[100];
|
||||
if (strlen(description)) {
|
||||
snprintf(info_s, sizeof(info_s), "'%s/%s' (%s)", dname, cmd, description);
|
||||
} else {
|
||||
snprintf(info_s, sizeof(info_s), "'%s/%s'", dname, cmd);
|
||||
}
|
||||
|
||||
std::string ro = EMSESP::system_.readonly_mode() ? "[readonly] " : "";
|
||||
|
||||
if ((value == nullptr) || (strlen(value) == 0)) {
|
||||
if (EMSESP::system_.readonly_mode()) {
|
||||
LOG_INFO(("[readonly] Calling command '%s/%s' (%s)"), dname, cmd, description);
|
||||
LOG_DEBUG(("%sCalling command %s"), ro.c_str(), info_s);
|
||||
} else {
|
||||
LOG_DEBUG(("Calling command '%s/%s' (%s)"), dname, cmd, description);
|
||||
}
|
||||
if (id > 0) {
|
||||
LOG_DEBUG(("%sCalling command %s with value %s and id %d"), ro.c_str(), info_s, value, id);
|
||||
} else {
|
||||
if (EMSESP::system_.readonly_mode()) {
|
||||
LOG_INFO(("[readonly] Calling command '%s/%s' (%s) with value %s"), dname, cmd, description, value);
|
||||
} else {
|
||||
LOG_DEBUG(("Calling command '%s/%s' (%s) with value %s"), dname, cmd, description, value);
|
||||
LOG_DEBUG(("%sCalling command %s with value %s"), ro.c_str(), info_s, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,9 +298,14 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char *
|
||||
return_code = ((cf->cmdfunction_json_)(value, id, output)) ? CommandRet::OK : CommandRet::ERROR;
|
||||
}
|
||||
|
||||
if (cf->cmdfunction_ && !EMSESP::cmd_is_readonly(device_type, cmd, id)) {
|
||||
// check if read-only. This also checks for valid tags (e.g. heating circuits)
|
||||
if (cf->cmdfunction_) {
|
||||
if (EMSESP::cmd_is_readonly(device_type, cmd, id)) {
|
||||
return_code = CommandRet::INVALID; // readonly or invalid hc
|
||||
} else {
|
||||
return_code = ((cf->cmdfunction_)(value, id)) ? CommandRet::OK : CommandRet::ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// report back
|
||||
if (return_code != CommandRet::OK) {
|
||||
|
||||
@@ -44,8 +44,8 @@ enum CommandRet : uint8_t {
|
||||
OK, // 1 or TRUE
|
||||
NOT_FOUND, // 2
|
||||
ERROR, // 3
|
||||
NOT_ALLOWED // needs authentication
|
||||
|
||||
NOT_ALLOWED, // 4 - needs authentication
|
||||
INVALID // 5 - invalid (tag)
|
||||
};
|
||||
|
||||
using cmd_function_p = std::function<bool(const char * data, const int8_t id)>;
|
||||
|
||||
@@ -505,7 +505,7 @@ void DallasSensor::publish_values(const bool force) {
|
||||
config["dev_cla"] = "temperature";
|
||||
|
||||
char stat_t[50];
|
||||
snprintf(stat_t, sizeof(stat_t), "%s/dallassensor_data", Mqtt::base().c_str());
|
||||
snprintf(stat_t, sizeof(stat_t), "%s/dallassensor_data", Mqtt::base().c_str()); // use base path
|
||||
config["stat_t"] = stat_t;
|
||||
|
||||
config["unit_of_meas"] = EMSdevice::uom_to_string(DeviceValueUOM::DEGREES);
|
||||
@@ -518,16 +518,18 @@ void DallasSensor::publish_values(const bool force) {
|
||||
}
|
||||
config["val_tpl"] = str;
|
||||
|
||||
// snprintf(str, sizeof(str), "%s_temperature_sensor_%s", Mqtt::basename().c_str(), sensor.id().c_str());
|
||||
snprintf(str, sizeof(str), "temperature_sensor_%s", sensor.id().c_str());
|
||||
if (Mqtt::multiple_instances()) {
|
||||
snprintf(str, sizeof(str), "%s_dallassensor_%s", Mqtt::basename().c_str(), sensor.id().c_str());
|
||||
} else {
|
||||
snprintf(str, sizeof(str), "dallassensor_%s", sensor.id().c_str());
|
||||
}
|
||||
|
||||
config["object_id"] = str;
|
||||
config["uniq_id"] = str; // same as object_id
|
||||
|
||||
snprintf(str, sizeof(str), "%s", sensor.name().c_str());
|
||||
config["name"] = str;
|
||||
|
||||
snprintf(str, sizeof(str), "dallasensor_%s", sensor.id().c_str());
|
||||
config["uniq_id"] = str;
|
||||
|
||||
JsonObject dev = config.createNestedObject("dev");
|
||||
JsonArray ids = dev.createNestedArray("ids");
|
||||
ids.add("ems-esp");
|
||||
|
||||
@@ -157,6 +157,10 @@
|
||||
#define EMSESP_DEFAULT_PUBLISH_TIME 10
|
||||
#endif
|
||||
|
||||
#ifndef EMSESP_DEFAULT_PUBLISH_HEARTBEAT
|
||||
#define EMSESP_DEFAULT_PUBLISH_HEARTBEAT 60
|
||||
#endif
|
||||
|
||||
#ifndef EMSESP_DEFAULT_NESTED_FORMAT
|
||||
#define EMSESP_DEFAULT_NESTED_FORMAT 1
|
||||
#endif
|
||||
|
||||
@@ -350,14 +350,8 @@ void EMSdevice::show_telegram_handlers(uuid::console::Shell & shell) const {
|
||||
|
||||
// list all the telegram type IDs for this device, outputting to a string (max size 200)
|
||||
char * EMSdevice::show_telegram_handlers(char * result, const size_t len, const uint8_t handlers) {
|
||||
uint8_t size = telegram_functions_.size();
|
||||
|
||||
strlcpy(result, "", len);
|
||||
|
||||
if (!size) {
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t i = 0;
|
||||
for (const auto & tf : telegram_functions_) {
|
||||
if (handlers == Handlers::ALL || (handlers == Handlers::RECEIVED && tf.received_ && !tf.fetch_)
|
||||
@@ -623,6 +617,7 @@ bool EMSdevice::is_readable(const void * value_p) const {
|
||||
}
|
||||
|
||||
// check if value/command is readonly
|
||||
// matches valid tags too
|
||||
bool EMSdevice::is_readonly(const std::string & cmd, const int8_t id) const {
|
||||
uint8_t tag = id > 0 ? DeviceValueTAG::TAG_HC1 + id - 1 : DeviceValueTAG::TAG_NONE;
|
||||
for (const auto & dv : devicevalues_) {
|
||||
|
||||
@@ -589,7 +589,7 @@ void EMSESP::publish_response(std::shared_ptr<const Telegram> telegram) {
|
||||
doc["value"] = value;
|
||||
}
|
||||
|
||||
Mqtt::publish(F_(response), doc.as<JsonObject>());
|
||||
Mqtt::publish("response", doc.as<JsonObject>());
|
||||
}
|
||||
|
||||
// builds json with the detail of each value, for a specific EMS device type or the dallas sensor
|
||||
@@ -1255,7 +1255,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
|
||||
}
|
||||
#endif
|
||||
// check for poll to us, if so send top message from Tx queue immediately and quit
|
||||
if (poll_id == txservice_.ems_bus_id()) {
|
||||
if (poll_id == txservice_.get_send_id()) {
|
||||
txservice_.send();
|
||||
}
|
||||
// send remote room temperature if active
|
||||
|
||||
@@ -244,7 +244,7 @@ char * Helpers::render_value(char * result, uint8_t value, int8_t format, const
|
||||
|
||||
// float: convert float to char
|
||||
// format is the precision, 0 to 8
|
||||
char * Helpers::render_value(char * result, const float value, const int8_t format) {
|
||||
char * Helpers::render_value(char * result, const double value, const int8_t format) {
|
||||
if (format > 8) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace emsesp {
|
||||
|
||||
class Helpers {
|
||||
public:
|
||||
static char * render_value(char * result, const float value, const int8_t format); // format is the precision
|
||||
static char * render_value(char * result, const double value, const int8_t format); // format is the precision
|
||||
static char * render_value(char * result, const uint8_t value, const int8_t format, const uint8_t fahrenheit = 0);
|
||||
static char * render_value(char * result, const int8_t value, const int8_t format, const uint8_t fahrenheit = 0);
|
||||
static char * render_value(char * result, const uint16_t value, const int8_t format, const uint8_t fahrenheit = 0);
|
||||
|
||||
161
src/mqtt.cpp
161
src/mqtt.cpp
@@ -36,7 +36,9 @@ uint32_t Mqtt::publish_time_solar_;
|
||||
uint32_t Mqtt::publish_time_mixer_;
|
||||
uint32_t Mqtt::publish_time_sensor_;
|
||||
uint32_t Mqtt::publish_time_other_;
|
||||
uint32_t Mqtt::publish_time_heartbeat_;
|
||||
bool Mqtt::mqtt_enabled_;
|
||||
bool Mqtt::multiple_instances_;
|
||||
bool Mqtt::ha_enabled_;
|
||||
uint8_t Mqtt::nested_format_;
|
||||
std::string Mqtt::discovery_prefix_;
|
||||
@@ -146,6 +148,12 @@ void Mqtt::loop() {
|
||||
process_queue();
|
||||
}
|
||||
|
||||
// send heartbeat
|
||||
if ((currentMillis - last_publish_heartbeat_ > publish_time_heartbeat_)) {
|
||||
last_publish_heartbeat_ = currentMillis;
|
||||
EMSESP::system_.send_heartbeat(); // send heartbeat
|
||||
}
|
||||
|
||||
// dallas publish on change
|
||||
if (!publish_time_sensor_) {
|
||||
EMSESP::publish_sensor_values(false);
|
||||
@@ -280,6 +288,7 @@ void Mqtt::on_message(const char * topic, const char * payload, size_t len) cons
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// for misconfigured mqtt servers and publish2command ignore echos
|
||||
if (publish_single_ && publish_single2cmd_ && lasttopic_ == topic && lastpayload_ == message) {
|
||||
LOG_DEBUG("Received echo message %s: %s", topic, message);
|
||||
@@ -291,11 +300,12 @@ void Mqtt::on_message(const char * topic, const char * payload, size_t len) cons
|
||||
// add the base back
|
||||
char full_topic[MQTT_TOPIC_MAX_SIZE];
|
||||
snprintf(full_topic, sizeof(full_topic), "%s/%s", mqtt_base_.c_str(), mf.topic_.c_str());
|
||||
|
||||
if ((!strcmp(topic, full_topic)) && (mf.mqtt_subfunction_)) {
|
||||
if (!(mf.mqtt_subfunction_)(message)) {
|
||||
LOG_ERROR("error: invalid payload %s for this topic %s", message, topic);
|
||||
if (send_response_) {
|
||||
Mqtt::publish(F_(response), "error: invalid data");
|
||||
Mqtt::publish("response", "error: invalid data");
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -330,12 +340,12 @@ void Mqtt::on_message(const char * topic, const char * payload, size_t len) cons
|
||||
}
|
||||
LOG_ERROR(error);
|
||||
if (send_response_) {
|
||||
Mqtt::publish(F_(response), error);
|
||||
Mqtt::publish("response", error);
|
||||
}
|
||||
} else {
|
||||
// all good, send back json output from call
|
||||
if (send_response_) {
|
||||
Mqtt::publish(F_(response), output);
|
||||
Mqtt::publish("response", output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,6 +430,7 @@ void Mqtt::load_settings() {
|
||||
publish_single2cmd_ = mqttSettings.publish_single2cmd;
|
||||
send_response_ = mqttSettings.send_response;
|
||||
discovery_prefix_ = mqttSettings.discovery_prefix.c_str();
|
||||
multiple_instances_ = mqttSettings.multiple_instances;
|
||||
|
||||
// convert to milliseconds
|
||||
publish_time_boiler_ = mqttSettings.publish_time_boiler * 1000;
|
||||
@@ -428,9 +439,11 @@ void Mqtt::load_settings() {
|
||||
publish_time_mixer_ = mqttSettings.publish_time_mixer * 1000;
|
||||
publish_time_other_ = mqttSettings.publish_time_other * 1000;
|
||||
publish_time_sensor_ = mqttSettings.publish_time_sensor * 1000;
|
||||
publish_time_heartbeat_ = mqttSettings.publish_time_heartbeat * 1000;
|
||||
});
|
||||
|
||||
// create basename from base
|
||||
// by taking the MQTT base path and replacing all / with underscores
|
||||
mqtt_basename_ = mqtt_base_;
|
||||
std::replace(mqtt_basename_.begin(), mqtt_basename_.end(), '/', '_');
|
||||
}
|
||||
@@ -510,6 +523,10 @@ void Mqtt::set_publish_time_sensor(uint16_t publish_time) {
|
||||
publish_time_sensor_ = publish_time * 1000; // convert to milliseconds
|
||||
}
|
||||
|
||||
void Mqtt::set_publish_time_heartbeat(uint16_t publish_time) {
|
||||
publish_time_heartbeat_ = publish_time * 1000; // convert to milliseconds
|
||||
}
|
||||
|
||||
bool Mqtt::get_publish_onchange(uint8_t device_type) {
|
||||
if (publish_single_ && !ha_enabled_) {
|
||||
return false;
|
||||
@@ -591,18 +608,26 @@ void Mqtt::on_connect() {
|
||||
void Mqtt::ha_status() {
|
||||
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
|
||||
|
||||
doc["uniq_id"] = "ems-esp-system";
|
||||
doc["~"] = mqtt_base_; // default ems-esp
|
||||
// doc["avty_t"] = "~/status"; // commented out, as it causes errors in HA sometimes
|
||||
// doc["json_attr_t"] = "~/heartbeat"; // store also as HA attributes
|
||||
doc["stat_t"] = "~/status";
|
||||
doc["object_id"] = "ems_esp_status";
|
||||
char uniq[70];
|
||||
if (Mqtt::multiple_instances()) {
|
||||
snprintf(uniq, sizeof(uniq), "%s_system_status", mqtt_basename_.c_str());
|
||||
} else {
|
||||
strcpy(uniq, "system_status");
|
||||
}
|
||||
|
||||
doc["uniq_id"] = uniq;
|
||||
doc["object_id"] = uniq;
|
||||
|
||||
doc["stat_t"] = mqtt_base_ + "/status";
|
||||
doc["name"] = "EMS-ESP status";
|
||||
doc["payload_on"] = "online";
|
||||
doc["payload_off"] = "offline";
|
||||
doc["state_class"] = "measurement";
|
||||
doc["device_class"] = "connectivity";
|
||||
|
||||
// doc["avty_t"] = "~/status"; // commented out, as it causes errors in HA sometimes
|
||||
// doc["json_attr_t"] = "~/heartbeat"; // store also as HA attributes
|
||||
|
||||
JsonObject dev = doc.createNestedObject("dev");
|
||||
dev["name"] = "EMS-ESP";
|
||||
dev["sw"] = "v" + std::string(EMSESP_APP_VERSION);
|
||||
@@ -612,7 +637,7 @@ void Mqtt::ha_status() {
|
||||
ids.add("ems-esp");
|
||||
|
||||
char topic[MQTT_TOPIC_MAX_SIZE];
|
||||
snprintf(topic, sizeof(topic), "binary_sensor/%s/system/config", mqtt_basename_.c_str());
|
||||
snprintf(topic, sizeof(topic), "binary_sensor/%s/system_status/config", mqtt_basename_.c_str());
|
||||
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
|
||||
|
||||
// create the sensors - must match the MQTT payload keys
|
||||
@@ -647,7 +672,7 @@ std::shared_ptr<const MqttMessage> Mqtt::queue_message(const uint8_t operation,
|
||||
std::shared_ptr<MqttMessage> message;
|
||||
message = std::make_shared<MqttMessage>(operation, topic, payload, retain);
|
||||
|
||||
#ifdef EMSESP_DEBUG
|
||||
#if defined(EMSESP_DEBUG)
|
||||
if (operation == Operation::PUBLISH) {
|
||||
if (message->payload.empty()) {
|
||||
LOG_INFO("[DEBUG] Adding to queue: (Publish) topic='%s' empty payload", message->topic.c_str());
|
||||
@@ -780,6 +805,7 @@ void Mqtt::process_queue() {
|
||||
if (message->topic.find(discovery_prefix_) == 0) {
|
||||
strlcpy(topic, message->topic.c_str(), sizeof(topic)); // leave topic as it is
|
||||
} else {
|
||||
// it's a discovery topic, added the mqtt base to the topic path
|
||||
snprintf(topic, MQTT_TOPIC_MAX_SIZE, "%s/%s", mqtt_base_.c_str(), message->topic.c_str()); // uses base
|
||||
}
|
||||
|
||||
@@ -841,7 +867,9 @@ void Mqtt::process_queue() {
|
||||
mqtt_message.retry_count_ + 1,
|
||||
message->payload.size(),
|
||||
packet_id);
|
||||
|
||||
#if defined(EMSESP_DEBUG)
|
||||
LOG_DEBUG("Payload:%s", message->payload.c_str()); // extra message for #784
|
||||
#endif
|
||||
if (packet_id == 0) {
|
||||
// it failed. if we retried n times, give up. remove from queue
|
||||
if (mqtt_message.retry_count_ == (MQTT_PUBLISH_MAX_RETRY - 1)) {
|
||||
@@ -904,7 +932,6 @@ void Mqtt::publish_ha_sensor_config(DeviceValue & dv, const std::string & model,
|
||||
publish_ha_sensor_config(dv.type,
|
||||
dv.tag,
|
||||
dv.get_fullname().c_str(),
|
||||
(dv.fullname ? dv.fullname[0] : nullptr), // EN name
|
||||
dv.device_type,
|
||||
dv.short_name,
|
||||
dv.uom,
|
||||
@@ -925,7 +952,7 @@ void Mqtt::publish_system_ha_sensor_config(uint8_t type, const char * name, cons
|
||||
JsonArray ids = dev_json.createNestedArray("ids");
|
||||
ids.add("ems-esp");
|
||||
|
||||
publish_ha_sensor_config(type, DeviceValueTAG::TAG_HEARTBEAT, name, name, EMSdevice::DeviceType::SYSTEM, entity, uom, false, false, nullptr, 0, 0, 0, dev_json);
|
||||
publish_ha_sensor_config(type, DeviceValueTAG::TAG_HEARTBEAT, name, EMSdevice::DeviceType::SYSTEM, entity, uom, false, false, nullptr, 0, 0, 0, dev_json);
|
||||
}
|
||||
|
||||
// MQTT discovery configs
|
||||
@@ -934,7 +961,6 @@ void Mqtt::publish_system_ha_sensor_config(uint8_t type, const char * name, cons
|
||||
void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdevice::DeviceValueType
|
||||
uint8_t tag, // EMSdevice::DeviceValueTAG
|
||||
const char * const fullname, // fullname, already translated
|
||||
const char * const en_name, // original name
|
||||
const uint8_t device_type, // EMSdevice::DeviceType
|
||||
const char * const entity, // same as shortname
|
||||
const uint8_t uom, // EMSdevice::DeviceValueUOM (0=NONE)
|
||||
@@ -946,25 +972,34 @@ void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
|
||||
const int16_t dv_set_max,
|
||||
const JsonObject & dev_json) {
|
||||
// ignore if name (fullname) is empty
|
||||
if (fullname == nullptr || en_name == nullptr) {
|
||||
if (!fullname) {
|
||||
return;
|
||||
}
|
||||
|
||||
// create the device name
|
||||
auto device_name = EMSdevice::device_type_2_device_name(device_type);
|
||||
|
||||
// create entity by add the hc/wwc tag if present, separating with a .
|
||||
char new_entity[50];
|
||||
// create entity by add the hc/wwc tag if present, separating with a _
|
||||
char entity_with_tag[50];
|
||||
if (tag >= DeviceValueTAG::TAG_HC1) {
|
||||
snprintf(new_entity, sizeof(new_entity), "%s.%s", EMSdevice::tag_to_mqtt(tag).c_str(), entity);
|
||||
snprintf(entity_with_tag, sizeof(entity_with_tag), "%s_%s", EMSdevice::tag_to_mqtt(tag).c_str(), entity);
|
||||
} else {
|
||||
snprintf(new_entity, sizeof(new_entity), "%s", entity);
|
||||
snprintf(entity_with_tag, sizeof(entity_with_tag), "%s", entity);
|
||||
}
|
||||
|
||||
// build unique identifier which will be used in the topic, replacing all . with _ as not to break HA
|
||||
char uniq[101];
|
||||
snprintf(uniq, sizeof(uniq), "%s_%s", device_name, new_entity);
|
||||
Helpers::replace_char(uniq, '.', '_');
|
||||
// build unique identifier which will be used in the topic, also used as object_id
|
||||
char uniq_id[70];
|
||||
if (Mqtt::multiple_instances()) {
|
||||
// prefix base name to each uniq_id
|
||||
snprintf(uniq_id, sizeof(uniq_id), "%s_%s_%s", mqtt_basename_.c_str(), device_name, entity_with_tag);
|
||||
} else {
|
||||
snprintf(uniq_id, sizeof(uniq_id), "%s_%s", device_name, entity_with_tag);
|
||||
}
|
||||
|
||||
// build a config topic that will be prefix onto a HA type (e.g. number, switch)
|
||||
// e.g. homeassistant/number/ems-esp/thermostat_hc1_manualtemp
|
||||
char config_topic[70];
|
||||
snprintf(config_topic, sizeof(config_topic), "%s/%s_%s/config", mqtt_basename_.c_str(), device_name, entity_with_tag);
|
||||
|
||||
bool set_ha_classes = false; // set to true if we want to set the state class and device class
|
||||
|
||||
@@ -981,46 +1016,43 @@ void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
|
||||
case DeviceValueType::ULONG:
|
||||
// number - https://www.home-assistant.io/integrations/number.mqtt
|
||||
// https://developers.home-assistant.io/docs/core/entity/number
|
||||
snprintf(topic, sizeof(topic), "number/%s/%s/config", mqtt_basename_.c_str(), uniq);
|
||||
snprintf(topic, sizeof(topic), "number/%s", config_topic);
|
||||
break;
|
||||
case DeviceValueType::BOOL:
|
||||
// switch - https://www.home-assistant.io/integrations/switch.mqtt
|
||||
snprintf(topic, sizeof(topic), "switch/%s/%s/config", mqtt_basename_.c_str(), uniq);
|
||||
snprintf(topic, sizeof(topic), "switch/%s", config_topic);
|
||||
break;
|
||||
case DeviceValueType::ENUM:
|
||||
// select - https://www.home-assistant.io/integrations/select.mqtt
|
||||
snprintf(topic, sizeof(topic), "select/%s/%s/config", mqtt_basename_.c_str(), uniq);
|
||||
snprintf(topic, sizeof(topic), "select/%s", config_topic);
|
||||
break;
|
||||
default:
|
||||
// plain old sensor
|
||||
snprintf(topic, sizeof(topic), "sensor/%s/%s/config", mqtt_basename_.c_str(), uniq);
|
||||
snprintf(topic, sizeof(topic), "sensor/%s", config_topic);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// these are Read only sensors. We can set the device class and state class
|
||||
set_ha_classes = true;
|
||||
set_ha_classes = true; // these are Read only sensors. We can set the device class and state class
|
||||
// plain old read only device entity
|
||||
if (type == DeviceValueType::BOOL) {
|
||||
snprintf(topic, sizeof(topic), "binary_sensor/%s/%s/config", mqtt_basename_.c_str(), uniq); // binary sensor (for booleans)
|
||||
snprintf(topic, sizeof(topic), "binary_sensor/%s", config_topic); // binary sensor (for booleans)
|
||||
} else {
|
||||
snprintf(topic, sizeof(topic), "sensor/%s/%s/config", mqtt_basename_.c_str(), uniq); // normal HA sensor
|
||||
snprintf(topic, sizeof(topic), "sensor/%s", config_topic); // normal HA sensor
|
||||
}
|
||||
}
|
||||
|
||||
// if we're asking to remove this topic, send an empty payload and exit
|
||||
// https://github.com/emsesp/EMS-ESP32/issues/196
|
||||
if (remove) {
|
||||
LOG_DEBUG("Removing HA config for %s", uniq);
|
||||
LOG_DEBUG("Removing HA config for %s", uniq_id);
|
||||
publish_ha(topic);
|
||||
return;
|
||||
}
|
||||
|
||||
bool have_tag = !EMSdevice::tag_to_string(tag).empty();
|
||||
|
||||
// build the payload
|
||||
DynamicJsonDocument doc(EMSESP_JSON_SIZE_HA_CONFIG);
|
||||
doc["~"] = mqtt_base_;
|
||||
doc["uniq_id"] = uniq;
|
||||
doc["uniq_id"] = uniq_id;
|
||||
doc["object_id"] = uniq_id; // same as unique_id
|
||||
|
||||
const char * ic_ha = "ic"; // icon - only set this if there is no device class
|
||||
const char * sc_ha = "state_class"; // state class
|
||||
@@ -1031,9 +1063,12 @@ void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
|
||||
// note: there is no way to handle strings in HA so datetimes (e.g. set_datetime, set_holiday, set_wwswitchtime etc) are excluded
|
||||
if (has_cmd) {
|
||||
// command topic back to EMS-ESP
|
||||
char command_topic[105];
|
||||
snprintf(command_topic, sizeof(command_topic), "~/%s", uniq);
|
||||
Helpers::replace_char(command_topic, '_', '/');
|
||||
char command_topic[MQTT_TOPIC_MAX_SIZE];
|
||||
if (tag >= DeviceValueTAG::TAG_HC1) {
|
||||
snprintf(command_topic, sizeof(command_topic), "%s/%s/%s/%s", mqtt_basename_.c_str(), device_name, EMSdevice::tag_to_mqtt(tag).c_str(), entity);
|
||||
} else {
|
||||
snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", mqtt_basename_.c_str(), device_name, entity);
|
||||
}
|
||||
doc["command_topic"] = command_topic;
|
||||
|
||||
// for enums, add options
|
||||
@@ -1075,38 +1110,30 @@ void Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
|
||||
|
||||
// state topic
|
||||
char stat_t[MQTT_TOPIC_MAX_SIZE];
|
||||
snprintf(stat_t, sizeof(stat_t), "~/%s", tag_to_topic(device_type, tag).c_str());
|
||||
snprintf(stat_t, sizeof(stat_t), "%s/%s", mqtt_basename_.c_str(), tag_to_topic(device_type, tag).c_str());
|
||||
doc["stat_t"] = stat_t;
|
||||
|
||||
// friendly name = <tag> <name>
|
||||
char ha_name[70];
|
||||
char * F_name = strdup(fullname);
|
||||
F_name[0] = toupper(F_name[0]); // capitalize first letter
|
||||
if (have_tag) {
|
||||
snprintf(ha_name, sizeof(ha_name), "%s %s", EMSdevice::tag_to_string(tag).c_str(), F_name);
|
||||
if (EMSdevice::tag_to_string(tag).empty()) {
|
||||
snprintf(ha_name, sizeof(ha_name), "%s", F_name); // no tag
|
||||
} else {
|
||||
snprintf(ha_name, sizeof(ha_name), "%s", F_name);
|
||||
snprintf(ha_name, sizeof(ha_name), "%s %s", EMSdevice::tag_to_string(tag).c_str(), F_name);
|
||||
}
|
||||
free(F_name);
|
||||
free(F_name); // very important!
|
||||
doc["name"] = ha_name;
|
||||
|
||||
// entity id is generated from the name, see https://www.home-assistant.io/docs/mqtt/discovery/#use-object_id-to-influence-the-entity-id
|
||||
// so we override it to make it unique using entity_id
|
||||
// See https://github.com/emsesp/EMS-ESP32/issues/596
|
||||
// keep it compatible to v3.4, use english fullname, no prefix (basename prefix commented out)
|
||||
char object_id[130];
|
||||
if (have_tag) {
|
||||
snprintf(object_id, sizeof(object_id), "%s_%s_%s", device_name, EMSdevice::tag_to_string(tag, false).c_str(), en_name);
|
||||
} else {
|
||||
snprintf(object_id, sizeof(object_id), "%s_%s", device_name, en_name);
|
||||
}
|
||||
doc["object_id"] = object_id;
|
||||
|
||||
// value template
|
||||
// if its nested mqtt format then use the appended entity name, otherwise take the original
|
||||
// if its nested mqtt format then use the appended entity name, otherwise take the original name
|
||||
char val_tpl[75];
|
||||
if (is_nested()) {
|
||||
snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", new_entity);
|
||||
if (tag >= DeviceValueTAG::TAG_HC1) {
|
||||
snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s.%s}}", EMSdevice::tag_to_mqtt(tag).c_str(), entity);
|
||||
} else {
|
||||
snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", entity);
|
||||
}
|
||||
} else {
|
||||
snprintf(val_tpl, sizeof(val_tpl), "{{value_json.%s}}", entity);
|
||||
}
|
||||
@@ -1230,8 +1257,7 @@ void Mqtt::publish_ha_climate_config(const uint8_t tag, const bool has_roomtemp,
|
||||
char currtemp_s[30];
|
||||
char mode_str_tpl[400];
|
||||
char name_s[10];
|
||||
char id_s[20];
|
||||
char uniq_id_s[30];
|
||||
char uniq_id_s[60];
|
||||
char temp_cmd_s[30];
|
||||
char mode_cmd_s[30];
|
||||
char min_s[10];
|
||||
@@ -1269,18 +1295,23 @@ void Mqtt::publish_ha_climate_config(const uint8_t tag, const bool has_roomtemp,
|
||||
hc_mode_s,
|
||||
hc_mode_s);
|
||||
|
||||
snprintf(id_s, sizeof(id_s), "thermostat_hc%d", hc_num);
|
||||
snprintf(name_s, sizeof(name_s), "Hc%d", hc_num);
|
||||
snprintf(uniq_id_s, sizeof(uniq_id_s), "thermostat_hc%d", hc_num);
|
||||
|
||||
if (Mqtt::multiple_instances()) {
|
||||
snprintf(uniq_id_s, sizeof(uniq_id_s), "%s_thermostat_hc%d", mqtt_basename_.c_str(), hc_num); // add basename
|
||||
} else {
|
||||
snprintf(uniq_id_s, sizeof(uniq_id_s), "thermostat_hc%d", hc_num); // backward compatible with v3.4
|
||||
}
|
||||
|
||||
snprintf(temp_cmd_s, sizeof(temp_cmd_s), "~/thermostat/hc%d/seltemp", hc_num);
|
||||
snprintf(mode_cmd_s, sizeof(temp_cmd_s), "~/thermostat/hc%d/mode", hc_num);
|
||||
|
||||
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
|
||||
|
||||
doc["~"] = mqtt_base_;
|
||||
doc["object_id"] = id_s;
|
||||
doc["name"] = name_s;
|
||||
doc["uniq_id"] = uniq_id_s;
|
||||
doc["object_id"] = uniq_id_s; // same as uniq_id
|
||||
doc["name"] = name_s;
|
||||
doc["mode_stat_t"] = topic_t;
|
||||
doc["mode_stat_tpl"] = mode_str_tpl;
|
||||
doc["temp_cmd_t"] = temp_cmd_s;
|
||||
|
||||
@@ -65,6 +65,7 @@ class Mqtt {
|
||||
void set_publish_time_mixer(uint16_t publish_time);
|
||||
void set_publish_time_other(uint16_t publish_time);
|
||||
void set_publish_time_sensor(uint16_t publish_time);
|
||||
void set_publish_time_heartbeat(uint16_t publish_time);
|
||||
bool get_publish_onchange(uint8_t device_type);
|
||||
|
||||
enum Operation : uint8_t { PUBLISH, SUBSCRIBE, UNSUBSCRIBE };
|
||||
@@ -94,7 +95,6 @@ class Mqtt {
|
||||
static void publish_ha_sensor_config(uint8_t type,
|
||||
uint8_t tag,
|
||||
const char * const fullname,
|
||||
const char * const en_name,
|
||||
const uint8_t device_type,
|
||||
const char * const entity,
|
||||
const uint8_t uom,
|
||||
@@ -180,6 +180,10 @@ class Mqtt {
|
||||
return nested_format_ == NestedFormat::NESTED;
|
||||
}
|
||||
|
||||
static bool multiple_instances() {
|
||||
return multiple_instances_;
|
||||
}
|
||||
|
||||
static void nested_format(uint8_t nested_format) {
|
||||
nested_format_ = nested_format;
|
||||
}
|
||||
@@ -289,6 +293,7 @@ class Mqtt {
|
||||
uint32_t last_publish_mixer_ = 0;
|
||||
uint32_t last_publish_other_ = 0;
|
||||
uint32_t last_publish_sensor_ = 0;
|
||||
uint32_t last_publish_heartbeat_ = 0;
|
||||
uint32_t last_publish_queue_ = 0;
|
||||
|
||||
static bool connecting_;
|
||||
@@ -312,9 +317,11 @@ class Mqtt {
|
||||
static uint32_t publish_time_mixer_;
|
||||
static uint32_t publish_time_other_;
|
||||
static uint32_t publish_time_sensor_;
|
||||
static uint32_t publish_time_heartbeat_;
|
||||
static bool mqtt_enabled_;
|
||||
static bool ha_enabled_;
|
||||
static uint8_t nested_format_;
|
||||
static bool multiple_instances_;
|
||||
static std::string discovery_prefix_;
|
||||
static bool publish_single_;
|
||||
static bool publish_single2cmd_;
|
||||
|
||||
@@ -151,10 +151,17 @@ void Shower::set_shower_state(bool state, bool force) {
|
||||
ha_configdone_ = true;
|
||||
|
||||
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
|
||||
|
||||
doc["name"] = "Shower Active";
|
||||
doc["uniq_id"] = "shower_active";
|
||||
doc["~"] = Mqtt::base();
|
||||
doc["stat_t"] = "~/shower_active";
|
||||
|
||||
char str[70];
|
||||
snprintf(str, sizeof(str), "%s_shower_active", Mqtt::basename().c_str());
|
||||
doc["uniq_id"] = str;
|
||||
doc["object_id"] = str;
|
||||
|
||||
char stat_t[50];
|
||||
snprintf(stat_t, sizeof(stat_t), "%s/shower_active", Mqtt::base().c_str()); // use base path
|
||||
doc["stat_t"] = stat_t;
|
||||
|
||||
// always render boolean as strings for HA
|
||||
char result[12];
|
||||
@@ -166,7 +173,7 @@ void Shower::set_shower_state(bool state, bool force) {
|
||||
ids.add("ems-esp");
|
||||
|
||||
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
|
||||
snprintf(topic, sizeof(topic), "binary_sensor/%s/shower_active/config", Mqtt::base().c_str());
|
||||
snprintf(topic, sizeof(topic), "binary_sensor/%s/shower_active/config", Mqtt::basename().c_str());
|
||||
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,12 +512,6 @@ void System::loop() {
|
||||
led_monitor(); // check status and report back using the LED
|
||||
system_check(); // check system health
|
||||
|
||||
// send out heartbeat
|
||||
uint32_t currentMillis = uuid::get_uptime();
|
||||
if (!last_heartbeat_ || (currentMillis - last_heartbeat_ > SYSTEM_HEARTBEAT_INTERVAL)) {
|
||||
last_heartbeat_ = currentMillis;
|
||||
send_heartbeat();
|
||||
}
|
||||
#ifndef EMSESP_STANDALONE
|
||||
|
||||
#if defined(EMSESP_DEBUG)
|
||||
@@ -536,23 +530,23 @@ void System::loop() {
|
||||
}
|
||||
|
||||
// send MQTT info topic appended with the version information as JSON, as a retained flag
|
||||
void System::send_info_mqtt(const char * event_str) {
|
||||
void System::send_info_mqtt(const char * event_str, bool send_ntp) {
|
||||
StaticJsonDocument<EMSESP_JSON_SIZE_MEDIUM> doc;
|
||||
doc["event"] = event_str;
|
||||
doc["version"] = EMSESP_APP_VERSION;
|
||||
|
||||
// if NTP is enabled send the boot_time in local time in ISO 8601 format (eg: 2022-11-15 20:46:38)
|
||||
// https://github.com/emsesp/EMS-ESP32/issues/751
|
||||
if (ntp_connected()) {
|
||||
if (send_ntp) {
|
||||
char time_string[25];
|
||||
time_t now = time(nullptr); // grab the current instant in unix seconds
|
||||
strftime(time_string, 25, "%F %T", localtime(&now));
|
||||
doc["boot_time"] = time_string;
|
||||
time_t now = time(nullptr) - uuid::get_uptime_sec();
|
||||
strftime(time_string, 25, "%FT%T%z", localtime(&now));
|
||||
doc["boot time"] = time_string;
|
||||
}
|
||||
|
||||
#ifndef EMSESP_STANDALONE
|
||||
if (EMSESP::system_.ethernet_connected()) {
|
||||
doc["connection"] = "ethernet";
|
||||
doc["network"] = "ethernet";
|
||||
doc["hostname"] = ETH.getHostname();
|
||||
doc["MAC"] = ETH.macAddress();
|
||||
doc["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask());
|
||||
@@ -562,7 +556,7 @@ void System::send_info_mqtt(const char * event_str) {
|
||||
doc["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6());
|
||||
}
|
||||
} else if (WiFi.status() == WL_CONNECTED) {
|
||||
doc["connection"] = "wifi";
|
||||
doc["network"] = "wifi";
|
||||
doc["hostname"] = WiFi.getHostname();
|
||||
doc["SSID"] = WiFi.SSID();
|
||||
doc["BSSID"] = WiFi.BSSIDstr();
|
||||
@@ -896,19 +890,19 @@ void System::show_system(uuid::console::Shell & shell) {
|
||||
shell.println("Network:");
|
||||
switch (WiFi.status()) {
|
||||
case WL_IDLE_STATUS:
|
||||
shell.printfln(" Network: Idle");
|
||||
shell.printfln(" Status: Idle");
|
||||
break;
|
||||
|
||||
case WL_NO_SSID_AVAIL:
|
||||
shell.printfln(" Network: Network not found");
|
||||
shell.printfln(" Status: Network not found");
|
||||
break;
|
||||
|
||||
case WL_SCAN_COMPLETED:
|
||||
shell.printfln(" Network: Network scan complete");
|
||||
shell.printfln(" Status: Network scan complete");
|
||||
break;
|
||||
|
||||
case WL_CONNECTED:
|
||||
shell.printfln(" Network: connected");
|
||||
shell.printfln(" Status: connected");
|
||||
shell.printfln(" SSID: %s", WiFi.SSID().c_str());
|
||||
shell.printfln(" BSSID: %s", WiFi.BSSIDstr().c_str());
|
||||
shell.printfln(" RSSI: %d dBm (%d %%)", WiFi.RSSI(), wifi_quality(WiFi.RSSI()));
|
||||
@@ -943,7 +937,7 @@ void System::show_system(uuid::console::Shell & shell) {
|
||||
// show Ethernet if connected
|
||||
if (ethernet_connected_) {
|
||||
shell.println();
|
||||
shell.printfln(" Ethernet Network: connected");
|
||||
shell.printfln(" Status: Ethernet connected");
|
||||
shell.printfln(" MAC address: %s", ETH.macAddress().c_str());
|
||||
shell.printfln(" Hostname: %s", ETH.getHostname());
|
||||
shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(ETH.localIP()).c_str(), uuid::printable_to_string(ETH.subnetMask()).c_str());
|
||||
@@ -1073,7 +1067,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp
|
||||
#ifndef EMSESP_STANDALONE
|
||||
node["free mem"] = ESP.getFreeHeap() / 1024; // kilobytes
|
||||
node["max alloc"] = ESP.getMaxAllocHeap() / 1024; // kilobytes
|
||||
node["free_app"] = EMSESP::system_.appFree(); // kilobytes
|
||||
node["free app"] = EMSESP::system_.appFree(); // kilobytes
|
||||
#endif
|
||||
node["reset reason"] = EMSESP::system_.reset_reason(0) + " / " + EMSESP::system_.reset_reason(1);
|
||||
|
||||
@@ -1081,7 +1075,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp
|
||||
// Network Status
|
||||
node = output.createNestedObject("Network Info");
|
||||
if (EMSESP::system_.ethernet_connected()) {
|
||||
node["connection"] = "Ethernet";
|
||||
node["network"] = "Ethernet";
|
||||
node["hostname"] = ETH.getHostname();
|
||||
node["MAC"] = ETH.macAddress();
|
||||
node["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask());
|
||||
@@ -1091,7 +1085,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp
|
||||
node["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6());
|
||||
}
|
||||
} else if (WiFi.status() == WL_CONNECTED) {
|
||||
node["connection"] = "WiFi";
|
||||
node["network"] = "WiFi";
|
||||
node["hostname"] = WiFi.getHostname();
|
||||
// node["SSID"] = WiFi.SSID();
|
||||
// node["BSSID"] = WiFi.BSSIDstr();
|
||||
@@ -1110,6 +1104,11 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp
|
||||
node["enable IPv6"] = settings.enableIPv6;
|
||||
node["low bandwidth"] = settings.bandwidth20;
|
||||
node["disable sleep"] = settings.nosleep;
|
||||
node["enable MDNS"] = settings.enableMDNS;
|
||||
node["enable CORS"] = settings.enableCORS;
|
||||
if (settings.enableCORS) {
|
||||
node["CORS origin"] = settings.CORSOrigin;
|
||||
}
|
||||
});
|
||||
#ifndef EMSESP_STANDALONE
|
||||
EMSESP::esp8266React.getAPSettingsService()->read([&](APSettings & settings) {
|
||||
@@ -1150,15 +1149,17 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp
|
||||
}
|
||||
EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) {
|
||||
node["enabled"] = settings.enabled;
|
||||
node["client_id"] = settings.clientId;
|
||||
node["client id"] = settings.clientId;
|
||||
node["keep alive"] = settings.keepAlive;
|
||||
node["clean session"] = settings.cleanSession;
|
||||
node["multiple instances"] = settings.multiple_instances;
|
||||
node["base"] = settings.base;
|
||||
node["discovery prefix"] = settings.discovery_prefix;
|
||||
node["nested format"] = settings.nested_format;
|
||||
node["ha enabled"] = settings.ha_enabled;
|
||||
node["mqtt qos"] = settings.mqtt_qos;
|
||||
node["mqtt retain"] = settings.mqtt_retain;
|
||||
node["publish time heartbeat"] = settings.publish_time_heartbeat;
|
||||
node["publish time boiler"] = settings.publish_time_boiler;
|
||||
node["publish time thermostat"] = settings.publish_time_thermostat;
|
||||
node["publish time solar"] = settings.publish_time_solar;
|
||||
@@ -1245,7 +1246,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp
|
||||
node["phy type"] = settings.phy_type;
|
||||
if (settings.phy_type != PHY_type::PHY_TYPE_NONE) {
|
||||
node["eth power"] = settings.eth_power;
|
||||
node["eth phy_addr"] = settings.eth_phy_addr;
|
||||
node["eth phy addr"] = settings.eth_phy_addr;
|
||||
node["eth clock_mode"] = settings.eth_clock_mode;
|
||||
}
|
||||
node["rx gpio"] = settings.rx_gpio;
|
||||
@@ -1264,6 +1265,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp
|
||||
node["enum format"] = settings.enum_format;
|
||||
node["analog enabled"] = settings.analog_enabled;
|
||||
node["telnet enabled"] = settings.telnet_enabled;
|
||||
node["web log buffer"] = settings.weblog_buffer;
|
||||
});
|
||||
|
||||
// Devices - show EMS devices if we have any
|
||||
@@ -1416,6 +1418,7 @@ void System::ntp_connected(bool b) {
|
||||
if (b != ntp_connected_) {
|
||||
LOG_INFO(b ? "NTP connected" : "NTP disconnected"); // if changed report it
|
||||
}
|
||||
|
||||
ntp_connected_ = b;
|
||||
ntp_last_check_ = b ? uuid::get_uptime_sec() : 0;
|
||||
}
|
||||
@@ -1426,6 +1429,7 @@ bool System::ntp_connected() {
|
||||
if ((uuid::get_uptime_sec() - ntp_last_check_ > 7201) && ntp_connected_) {
|
||||
ntp_connected(false);
|
||||
}
|
||||
|
||||
return ntp_connected_;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class System {
|
||||
bool check_upgrade();
|
||||
bool heartbeat_json(JsonObject & output);
|
||||
void send_heartbeat();
|
||||
void send_info_mqtt(const char * event_str);
|
||||
void send_info_mqtt(const char * event_str, bool send_ntp = false);
|
||||
|
||||
bool syslog_enabled() {
|
||||
return syslog_enabled_;
|
||||
@@ -250,7 +250,6 @@ class System {
|
||||
static constexpr uint32_t HEALTHCHECK_LED_FLASH_DUARATION = 150;
|
||||
static constexpr uint8_t HEALTHCHECK_NO_BUS = (1 << 0); // 1
|
||||
static constexpr uint8_t HEALTHCHECK_NO_NETWORK = (1 << 1); // 2
|
||||
static constexpr uint32_t SYSTEM_HEARTBEAT_INTERVAL = 60000; // in milliseconds, how often the MQTT heartbeat is sent (1 min)
|
||||
static constexpr uint8_t LED_ON = HIGH; // LED on
|
||||
|
||||
#ifndef EMSESP_STANDALONE
|
||||
@@ -263,7 +262,6 @@ class System {
|
||||
int8_t wifi_quality(int8_t dBm);
|
||||
|
||||
uint8_t healthcheck_ = HEALTHCHECK_NO_NETWORK | HEALTHCHECK_NO_BUS; // start with all flags set, no wifi and no ems bus connection
|
||||
uint32_t last_heartbeat_ = 0;
|
||||
uint32_t last_system_check_ = 0;
|
||||
|
||||
bool upload_status_ = false; // true if we're in the middle of a OTA firmware upload
|
||||
|
||||
@@ -268,6 +268,21 @@ void TxService::send_poll() const {
|
||||
}
|
||||
}
|
||||
|
||||
// get src id from next telegram to check poll in emsesp::incoming_telegram
|
||||
uint8_t TxService::get_send_id() {
|
||||
static uint32_t count = 0;
|
||||
if (!tx_telegrams_.empty() && tx_telegrams_.front().telegram_->src != ems_bus_id()) {
|
||||
if (++count > 500) { // after 500 polls (~3-10 sec) there will be no master poll for this id
|
||||
tx_telegrams_.pop_front();
|
||||
count = 0;
|
||||
return tx_telegrams_.empty() ? ems_bus_id() : tx_telegrams_.front().telegram_->src;
|
||||
}
|
||||
return tx_telegrams_.front().telegram_->src;
|
||||
}
|
||||
count = 0;
|
||||
return ems_bus_id();
|
||||
}
|
||||
|
||||
// Process the next telegram on the Tx queue
|
||||
// This is sent when we receive a poll request
|
||||
void TxService::send() {
|
||||
@@ -463,7 +478,7 @@ void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t lengt
|
||||
}
|
||||
|
||||
// build header. src, dest and offset have fixed positions
|
||||
uint8_t src = ems_bus_id(); // data[0]; we can only send data with own bus_id.
|
||||
uint8_t src = operation == Telegram::Operation::TX_RAW ? data[0] : ems_bus_id();
|
||||
uint8_t dest = data[1];
|
||||
uint8_t offset = data[3];
|
||||
|
||||
@@ -499,7 +514,9 @@ void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t lengt
|
||||
}
|
||||
|
||||
if (operation == Telegram::Operation::TX_RAW) {
|
||||
if (dest & 0x80) {
|
||||
if (src != ems_bus_id()) {
|
||||
operation = Telegram::Operation::NONE; // do not check reply/ack for other ids
|
||||
} else if (dest & 0x80) {
|
||||
operation = Telegram::Operation::TX_READ;
|
||||
} else {
|
||||
operation = Telegram::Operation::TX_WRITE;
|
||||
@@ -585,8 +602,8 @@ bool TxService::send_raw(const char * telegram_data) {
|
||||
}
|
||||
}
|
||||
|
||||
// check valid length and src
|
||||
if ((count < 4) || ((data[0] & 0x7F) != ems_bus_id())) {
|
||||
// check valid length
|
||||
if (count < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -290,6 +290,7 @@ class TxService : public EMSbus {
|
||||
|
||||
void start();
|
||||
void send();
|
||||
uint8_t get_send_id();
|
||||
void add(const uint8_t operation,
|
||||
const uint8_t dest,
|
||||
const uint16_t type_id,
|
||||
|
||||
@@ -1397,9 +1397,21 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
|
||||
// add a boiler
|
||||
add_device(0x08, 123); // Nefit Trendline
|
||||
|
||||
// add a thermostat
|
||||
add_device(0x18, 157); // Bosch CR100 - https://github.com/emsesp/EMS-ESP/issues/355
|
||||
// add some boiler data
|
||||
// Boiler -> Me, UBAMonitorFast(0x18), telegram: 08 00 18 00 00 02 5A 73 3D 0A 10 65 40 02 1A 80 00 01 E1 01 76 0E 3D 48 00 C9 44 02 00 (#data=25)
|
||||
uart_telegram({0x08, 0x00, 0x18, 0x00, 0x00, 0x02, 0x5A, 0x73, 0x3D, 0x0A, 0x10, 0x65, 0x40, 0x02, 0x1A,
|
||||
0x80, 0x00, 0x01, 0xE1, 0x01, 0x76, 0x0E, 0x3D, 0x48, 0x00, 0xC9, 0x44, 0x02, 0x00});
|
||||
|
||||
// Boiler -> Thermostat, UBAParameterWW(0x33), telegram: 08 97 33 00 23 24 (#data=2)
|
||||
uart_telegram({0x08, 0x98, 0x33, 0x00, 0x23, 0x24});
|
||||
|
||||
// Boiler -> Me, UBAParameterWW(0x33), telegram: 08 0B 33 00 08 FF 34 FB 00 28 00 00 46 00 FF FF 00 (#data=13)
|
||||
uart_telegram({0x08, 0x0B, 0x33, 0x00, 0x08, 0xFF, 0x34, 0xFB, 0x00, 0x28, 0x00, 0x00, 0x46, 0x00, 0xFF, 0xFF, 0x00});
|
||||
|
||||
// add a thermostat
|
||||
add_device(0x18, 157); // Bosch CR100
|
||||
|
||||
// add some thermostat data
|
||||
// RCPLUSStatusMessage_HC1(0x01A5) - HC1
|
||||
uart_telegram({0x98, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xCF, 0x21, 0x2E, 0x00, 0x00, 0x2E, 0x24,
|
||||
0x03, 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03});
|
||||
@@ -1412,11 +1424,13 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
|
||||
|
||||
char boiler_topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
|
||||
char thermostat_topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
|
||||
char thermostat_topic_hc1[Mqtt::MQTT_TOPIC_MAX_SIZE];
|
||||
char system_topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
|
||||
Mqtt::show_mqtt(shell); // show queue
|
||||
|
||||
strlcpy(boiler_topic, "ems-esp/boiler", sizeof(boiler_topic));
|
||||
strlcpy(thermostat_topic, "ems-esp/thermostat", sizeof(thermostat_topic));
|
||||
strlcpy(thermostat_topic_hc1, "ems-esp/thermostat/hc1", sizeof(thermostat_topic));
|
||||
strlcpy(system_topic, "ems-esp/system", sizeof(system_topic));
|
||||
|
||||
// test publishing
|
||||
@@ -1425,11 +1439,13 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
|
||||
// test receiving
|
||||
EMSESP::mqtt_.incoming(boiler_topic, ""); // test if ignore empty payloads, should return values
|
||||
|
||||
// these all should fail
|
||||
EMSESP::mqtt_.incoming(boiler_topic, "12345"); // error: invalid format
|
||||
EMSESP::mqtt_.incoming("bad_topic", "123456"); // error: no matching topic
|
||||
EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"garbage\",\"data\":22.52}"); // error: should report error
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"control\",\"data\":\"1\"}"); // RC35 only, should error
|
||||
|
||||
EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"comfort\",\"data\":\"eco\"}");
|
||||
// these all should pass
|
||||
EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"wwactivated\",\"data\":\"1\"}"); // with quotes
|
||||
EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"wwactivated\",\"data\":1}"); // without quotes
|
||||
EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"selflowtemp\",\"data\":55}");
|
||||
@@ -1438,19 +1454,22 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
|
||||
EMSESP::mqtt_.incoming("ems-esp/boiler/selflowtemp", "56");
|
||||
|
||||
EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"send\",\"data\":\"01 02 03 04 05\"}");
|
||||
EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"pin\",\"id\":12,\"data\":\"1\"}");
|
||||
// EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"pin\",\"id\":12,\"data\":\"1\"}");
|
||||
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"wwmode\",\"data\":\"auto\"}");
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"control\",\"data\":\"1\"}"); // RC35 only, should error
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"typo\",\"id\":2}"); // invalid mode
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"id\":2}");
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":2}"); // hc as number
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":19.5,\"hc\":1}"); // data as number
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":\"2\"}"); // hc as string. should error as no hc2
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":22.56}");
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":22}");
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":\"22.56\"}");
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"id\":2,\"data\":22}");
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"data\":19.5,\"hc\":1}"); // data as number
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"data\":\"auto\",\"hc\":\"2\"}"); // hc as string. should error as no hc2
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"data\":22.56}");
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"data\":22}");
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"data\":\"22.56\"}");
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"seltemp\",\"id\":2,\"data\":22}");
|
||||
|
||||
// test with hc
|
||||
EMSESP::mqtt_.incoming("ems-esp/thermostat/hc1/seltemp", "30");
|
||||
EMSESP::mqtt_.incoming("ems-esp/thermostat/hc2/seltemp", "32");
|
||||
|
||||
// test single commands
|
||||
EMSESP::mqtt_.incoming(thermostat_topic, "auto");
|
||||
|
||||
@@ -30,8 +30,8 @@ namespace emsesp {
|
||||
// #define EMSESP_DEBUG_DEFAULT "solar"
|
||||
// #define EMSESP_DEBUG_DEFAULT "mixer"
|
||||
// #define EMSESP_DEBUG_DEFAULT "web"
|
||||
// #define EMSESP_DEBUG_DEFAULT "mqtt"
|
||||
#define EMSESP_DEBUG_DEFAULT "general"
|
||||
#define EMSESP_DEBUG_DEFAULT "mqtt"
|
||||
// #define EMSESP_DEBUG_DEFAULT "general"
|
||||
// #define EMSESP_DEBUG_DEFAULT "boiler"
|
||||
// #define EMSESP_DEBUG_DEFAULT "mqtt2"
|
||||
// #define EMSESP_DEBUG_DEFAULT "mqtt_nested"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#define EMSESP_APP_VERSION "3.5.0b10"
|
||||
#define EMSESP_APP_VERSION "3.5.0b11"
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
#define EMSESP_PLATFORM "ESP32-C3";
|
||||
|
||||
@@ -102,6 +102,14 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject & input) {
|
||||
|
||||
// output json buffer
|
||||
auto * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXLARGE_DYN);
|
||||
if (!response->getSize()) {
|
||||
delete response;
|
||||
response = new PrettyAsyncJsonResponse(false, 256);
|
||||
response->setCode(507); // Insufficient Storage
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
JsonObject output = response->getRoot();
|
||||
|
||||
// call command
|
||||
@@ -136,7 +144,7 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject & input) {
|
||||
|
||||
// send the json that came back from the command call
|
||||
// FAIL, OK, NOT_FOUND, ERROR, NOT_ALLOWED = 400 (bad request), 200 (OK), 400 (not found), 400 (bad request), 401 (unauthorized)
|
||||
int ret_codes[5] = {400, 200, 400, 400, 401};
|
||||
int ret_codes[6] = {400, 200, 400, 400, 401, 400};
|
||||
response->setCode(ret_codes[return_code]);
|
||||
response->setLength();
|
||||
response->setContentType("application/json; charset=utf-8");
|
||||
|
||||
@@ -201,6 +201,14 @@ void WebCustomizationService::devices(AsyncWebServerRequest * request) {
|
||||
void WebCustomizationService::device_entities(AsyncWebServerRequest * request, JsonVariant & json) {
|
||||
if (json.is<JsonObject>()) {
|
||||
auto * response = new MsgpackAsyncJsonResponse(true, EMSESP_JSON_SIZE_XXXLARGE_DYN);
|
||||
if (!response->getSize()) {
|
||||
delete response;
|
||||
response = new MsgpackAsyncJsonResponse(true, 256);
|
||||
response->setCode(507); // Insufficient Storage
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
for (const auto & emsdevice : EMSESP::emsdevices) {
|
||||
if (emsdevice->unique_id() == json["id"]) {
|
||||
#ifndef EMSESP_STANDALONE
|
||||
|
||||
@@ -44,8 +44,8 @@ class AnalogCustomization {
|
||||
public:
|
||||
uint8_t gpio;
|
||||
std::string name;
|
||||
float offset;
|
||||
float factor;
|
||||
double offset;
|
||||
double factor;
|
||||
uint8_t uom; // 0 is none
|
||||
int8_t type; // -1 is for deletion
|
||||
|
||||
|
||||
@@ -166,6 +166,14 @@ void WebDataService::sensor_data(AsyncWebServerRequest * request) {
|
||||
void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant & json) {
|
||||
if (json.is<JsonObject>()) {
|
||||
auto * response = new MsgpackAsyncJsonResponse(false, EMSESP_JSON_SIZE_XXXLARGE_DYN);
|
||||
if (!response->getSize()) {
|
||||
delete response;
|
||||
response = new MsgpackAsyncJsonResponse(false, 256);
|
||||
response->setCode(507); // Insufficient Storage
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
for (const auto & emsdevice : EMSESP::emsdevices) {
|
||||
if (emsdevice->unique_id() == json["id"]) {
|
||||
// wait max 2.5 sec for updated data (post_send_delay is 2 sec)
|
||||
@@ -284,8 +292,8 @@ void WebDataService::write_analog(AsyncWebServerRequest * request, JsonVariant &
|
||||
|
||||
uint8_t gpio = analog["gpio"]; // this is the unique key, the GPIO
|
||||
std::string name = analog["name"];
|
||||
float factor = analog["factor"];
|
||||
float offset = analog["offset"];
|
||||
double factor = analog["factor"];
|
||||
double offset = analog["offset"];
|
||||
uint8_t uom = analog["uom"];
|
||||
int8_t type = analog["type"];
|
||||
ok = EMSESP::analogsensor_.update(gpio, name, offset, factor, uom, type);
|
||||
|
||||
Reference in New Issue
Block a user